背景
早期基于yaf搭建的php后台管理系统,属于SSR,前端采用传统的jquery + bootstrap架构。 目前要在这个基础上需要开发新功能。考虑到后期可能重构和维护性。
- 希望把SSR架构调整为前后端分离
- UI希望使用更丰富的elementPlus
- 希望代码编写 基于数据驱动,不在操作dom如vue3
- 希望代码可以复用和兼容浏览器,如引入脚手架webpack vite等
- 数据获取从php输出改成resful风格访问
方案1: php + vue3多页面应用
直接在对应的php页面上写vue3的多页面应用
分析
优点:
- 粗暴直接,直接引入cdn即可使用
- 调试快,不用搭脚手架,不用执行命令构建,刷新页面即可
- 使用cdn多次渲染,也不用重复再加载原来资源
缺点:
- 复用的代码只能过用js引入,容易导致全局污染
- 当页面多了,很难维护,无法做到组件化开发
- 编写html的标签必须有闭合标签,不能简写,如
<el-table-column label="姓名" align="center" prop="name" />将不会有意想不到的效果。 - 没有路由,后期如果要整体切换到spa应用调整成本也大
实战
1.项目结构
以yaf结构为列:
- application/controllers 转发控制器
- application/views 编写view视图代码
- public/js/plguin 存放自第三方js库
2.controller
一般php使用mvc架构,controller 我们只需要做转发即可 如下:
application/controllers/test.php
<?php
/**
* 测试
*/
use Model\AclModel;
use Model\AreaModel;
use Model\MenuModel;
use Mvc\AbstractController;
class ResourceController extends AbstractController
{
/**
* 测试vue响应式
* @throws Exception
*/
public function testvue3Action()
{
}
/**
* 测试 增删改查
* @throws Exception
*/
public function listAction()
{
}
}
一般一个action对应一个phtml页面
3. vue3和elementui库添加
public/js/plugin
4. view视图
直接引入vue和编写vue
application/views/test/testvue3.phtml (做vue3测试响应式)
<!-- vue3 JS & CSS files -->
<script type="text/javascript" src="<?= $this->helper->basePath('/js/plugin/vue3/vue.global.min.js'); ?>"></script>
<div id="app">
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
<script type="module">
const {
createApp,
ref
} = Vue
const app = createApp({
setup() {
const count = ref(0);
const increment = () => {
count.value++;
};
return {
count,
increment
};
}
})
app.mount('#app')
</script>
<style scoped>
</style>
application/views/test/list.phtml (集成vue3和elementuiPlus 常规的增删改,包括分页)
<!-- vue3 JS & CSS files -->
<script type="text/javascript" src="<?= $this->helper->basePath('/js/plugin/vue3/vue.global.min.js'); ?>"></script>
<link href="<?= $this->helper->basePath('/js/plugin/elementplus/index.min.css'); ?>" rel="stylesheet" type="text/css">
<script type="text/javascript" src="<?= $this->helper->basePath('/js/plugin/elementplus/index.full.min.js'); ?>"></script>
<script type="text/javascript" src="<?= $this->helper->basePath('/js/plugin/elementplus/zh-cn.min.js'); ?>"></script>
<script type="text/javascript" src="<?= $this->helper->basePath('/js/plugin/axios/axios.min.js'); ?>"></script>
<script type="text/javascript" src="<?= $this->helper->basePath('/js/plugin/moment/moment.min.js'); ?>"></script>
<script type="text/javascript" src="<?= $this->helper->basePath('/js/plugin/vue3/hooks/list.js'); ?>"></script>
<script type="text/javascript" src="<?= $this->helper->basePath('/js/plugin/functional/forecast.js'); ?>"></script>
<div id="app">
<el-form ref="queryRef" :model="queryForm" inline label-width="100px">
<el-row>
<el-col :xl="25" :lg="6" :md="8" :sm="12">
<el-form-item label="名称" prop="name">
<el-input v-model="queryForm.name" />
</el-form-item>
</el-col>
<el-col :xl="25" :lg="6" :md="8" :sm="12">
<div class="searchOpreaBox">
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
</el-col>
</el-row>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain :disabled="!multiple" @click="handleDelete">删除</el-button>
</el-col>
</el-row>
</el-form>
<el-row class="">
<el-col>
<el-table :data="tableData" v-loading="loading" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center"> </el-table-column>
<el-table-column label="姓名" align="center" prop="name"></el-table-column>
<el-table-column label="年龄" align="center" prop="age"></el-table-column>
</el-table>
</el-col>
</el-row>
<el-pagination background layout="total,sizes,->,prev, pager, next" :current-page="page.currentPage" :page-size="page.pageSize" :page-sizes="page.pageSizes" :pager-count="page.pagerCount" :total="page.total" @size-change="handleSizeChange" @current-change="handleCurrentChange"></el-pagination>
<el-dialog title="新增/修改" v-model="open" width="500px" :close-on-click-modal="false">
<el-form v-loading="postLoading" ref="postRef" :model="postForm" :rules="postRules" label-width="80px">
<el-row>
<el-col>
<el-form-item label="姓名" prop="name">
<el-input v-model="postForm.name" />
</el-form-item>
</el-col>
<el-col>
<el-form-item label="年龄" prop="range">
<el-input v-model="postForm.age" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="cancelForm">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
<script type="module">
const list2 = (params) => {
return new Promise((resolve) => {
let res = {
data: {
code:200,
rows:[
{id:1 , name:'张三1',age:16},
{id:2 , name:'张三2',age:20},
{id:3 , name:'张三3',age:16},
{id:4 , name:'张三4',age:20},
{id:5 , name:'张三5',age:20},
{id:6 , name:'张三6',age:16},
{id:7 , name:'张三7',age:20},
{id:8 , name:'张三8',age:16},
{id:9 , name:'张三9',age:16},
{id:10 , name:'张三10',age:21},
],
total: 11
}
}
resolve(
res
)
})
}
const {
createApp,
ref,
toRefs,
reactive,
onMounted,
getCurrentInstance
} = Vue
const app = createApp({
setup() {
//列表查询
const queryLisPromise = (query) => {
let queryClone = {
...state.queryForm,
page: useListObj.page.value.currentPage,
per_page: useListObj.page.value.pageSize,
}
return new Promise((resolve, reject) => {
state.loading = true
// axios.get('/xxxx/list', {
// params: queryClone
// })
list2(queryClone).then(res => {
state.loading = false
if(res.data && res.data.code === 200) {
state.tableData = res.data.rows;
resolve({
tableData: res.data.rows,
total: res.data.total,
});
} else {
reject({
tableData: [],
total: 0,
});
}
})
.catch(error => {
state.loading = false
reject({
tableData: [],
total: 0,
});
})
})
}
const useListObj = useList(queryLisPromise, item => item.id);
const state = reactive({
queryForm: {
},
loading: false,
postLoading: false,
tableData: [],
postForm: {
name: "",
age: 0,
},
postRules: {
name: [{
required: true,
message: '姓名不能为空',
trigger: 'blur'
}],
},
open: false,
operate: 'add', // add or edit
queryRef: null,
postRef: null
});
const funObj = {
resetQuery() {
state.queryRef.resetFields();
useListObj.resetPage()
useListObj.handleQuery();
},
handleAdd() {
if (state.postRef) {
state.postRef.resetFields();
}
state.operate = 'add'
state.open = true;
},
submitForm() {
state.postRef.validate( (baseValid) => {
ElementPlus.ElMessage.success("新增成功")
})
},
cancelForm() {
state.open = false;
},
handleDelete () {
console.log(useListObj.ids.value)
ElementPlus.ElMessage.success(`删除${useListObj.ids.value}`)
}
}
onMounted(() => {
useListObj.handleQuery();
})
return {
...toRefs(state),
...funObj,
...useListObj
}
}
})
app.use(ElementPlus, {
locale: window.ElementPlusLocaleZhCn
});
app.mount('#app')
</script>
<style scoped>
.app-container {
padding: 20px 0px 0 0px;
background-color: #fff;
}
.el-table .el-table__header-wrapper th,
.el-table__fixed-header-wrapper th {
word-break: break-word;
background-color: rgb(245, 245, 245) !important;
color: #515a6e;
height: 1.5384616;
font-size: 13px;
border: 1px solid #ebeef5 !important;
}
.el-table .el-table__body-wrapper {
.el-button [class*="el-icon-"]+span {
margin-left: 1px;
}
}
.table-responsive {
margin-top: 10px;
}
.el-form-item {
margin-right: 0 !important;
width: 100%
}
.el-form-item__content {
width: calc(100% - 130px) !important
}
.el-form-item__label {
line-height: 42px;
}
.el-pagination {
margin-top: 10px;
margin-bottom: 10px;
margin-left: 10px;
margin-right: 10px;
}
.searchOpreaBox {
padding: 4px 0;
padding-left: 5px;
}
</style>
自定义引入分页hooks文件list.js
public/js/plugin/vue3/hooks/list.js
function useList(queryLisPromise,handleSelectionChangeFun) {
const state = Vue.reactive({
page: {
currentPage: 1, //服务端参数 当前几页
pageSize: 10, //服务端参数 一页多少条
total: 300, //服务端参数 一共有多少条
pageSizes: [10, 20, 30, 50], //每页显示个数选择器的选项设置 客户端参数
pagerCount: 6, //最大页码按钮数 客户端参数
},
//恢复默认设置 使用
pageBase: { },
ids: [],
single: false,
multiple: false,
}) ;
state.pageBase = {...state.page} //复制一份
const funObj = {
async handleQuery() {
let tableData = []
let total = 0
try {
let obj = await queryLisPromise()
tableData = obj.tableData
total = obj.total
} catch (error) {
}
state.page.total = total;
state.single = false
state.multiple = false
return tableData
},
handleSizeChange(val) {
state.page.pageSize = val
if (state.page.currentPage * val > state.page.total) {
state.page.currentPage = 1
}
funObj.handleQuery();
},
handleCurrentChange(val) {
state.page.currentPage = val
funObj.handleQuery();
},
resetPage() {
state.page = {...state.pageBases}
},
handleSelectionChange(selection) {
state.ids = selection.map(handleSelectionChangeFun); //item => item.id
state.single = selection.length === 1;
state.multiple = !!selection.length;
},
}
return {...Vue.toRefs(state)
,...funObj
};
}
输出效果
xxxxx.cn/test/testvu… (http://xxxxx.cn改成你php访问的地址)
源码参考
方案2: php + vite 单页面应用
分析
优点:
- 解决多页面应用的各个缺点
- 组件化,工程化,代码规范,单闭合标签问题,包含路由,后期项目成熟可以整体切换。
缺点:
- 前期构建花费时间长,需要动态生成对应的phtml文件,替换对应的请求文件路径,实现热更等
- 每次调试虽然也是刷新页面,但是需要等待构建,时间长一点。
思路
- 通过vite打包工具,每次修改代码都触发钩子函数,同步代码到php项目
- 把输出的index.html 同步到对应的 php项目views文件夹下对应phtml
- 在不同的phtml页面根据配置,在全局window里面输出不同的
__vue__router_path变量
- 在不同的phtml页面根据配置,在全局window里面输出不同的
- 把资源文件js和css同步到php的public的vite文件夹下
- 页面加载时候,获取通过router.push(window.__vue__router_path)跳转到对应的路由模块
实战
1. 整体项目结构
2.php项目结构
- application/controllers 转发控制器
- application/views 编写view视图代码
- public/js/plugin 存放自第三方js库
- public/vite 存放vite生成的js和css
1 .controller
<?php
/**
* 测试
*/
use Model\AclModel;
use Model\AreaModel;
use Model\MenuModel;
use Mvc\AbstractController;
class ResourceController extends AbstractController
{
/**
* 测试vue响应式
* @throws Exception
*/
public function testvue3Action()
{
}
/**
* 测试 增删改查
* @throws Exception
*/
public function listAction()
{
}
}
2 .view试图
3 .资源文件
3.vite工程结构
由于php项目也是工程化,为了避免文件混杂,把vite工程单独一个文件夹vite在根目录
- vite/ 存放整个vite工程项目代码
- vite/plugin 构建同步的代码和配置
- vite/src 具体的业务代码
同步构建的逻辑
每次构建
- 都会把vite/dist中的assets复制到 public/vite/assets路径下,
- 根据vite/plugins/syncToPhpList.json的配置,把vite/dist中的index.html复制到对应的php项目中的application/views/xxx/xxx.phtml
生成的 xxx.phtml 代码
<script>window.__vue__router_path = 'list'</script>
<script src="http://xxxx.cn/js/plugin/vue3/vue.global.min.js" crossorigin="anonymous"></script>
<script src="http://xxxx.cn/js/plugin/axios/axios.min.js" crossorigin="anonymous"></script>
<script src="http://xxxx.cn/js/plugin/elementplus/index.full.min.js" crossorigin="anonymous"></script>
<link href="http://xxxx.cn/js/plugin/elementplus/index.min.css" rel="stylesheet" crossorigin="anonymous">
<script type="module" crossorigin src="http://xxxx.cn/vite/assets/index-z7wQGBOr.js"></script>
<link rel="stylesheet" crossorigin href="http://xxxx.cn/vite/assets/index-BqLeIERi.css">
<div id="app"></div>
vite代码
核心配置与代码
vite/vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import {ElementPlusResolver} from 'unplugin-vue-components/resolvers'
import cdnImport from 'vite-plugin-cdn-import';
import syncToPhp from './plugins/syncToPhp';
import { isProd, loadEnv } from './src/utils/vite'
import { resolve } from 'path'
import { viteMockServe } from 'vite-plugin-mock'
import vueJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig(({ mode, command }) => {
const { VITE_BASE_PATH } = loadEnv(mode)
const pathResolve = (dir: string): any => {
return resolve(__dirname, '.', dir)
}
const alias: Record<string, string> = {
'@': pathResolve('./src/'),
'~': pathResolve('./'),
assets: pathResolve('./src/assets'),
}
console.log("VITE_BASE_PATH",VITE_BASE_PATH)
let baseUrl = VITE_BASE_PATH
return {
resolve: { alias },
plugins: [vue(),vueJsx(),
AutoImport({
resolvers: [ElementPlusResolver()],
}), Components({
resolvers: [ElementPlusResolver()],
}),
viteMockServe({
mockPath: './src/mock',
}),
// 添加其他库的 CDN 配置
cdnImport({
modules: [
{
name: 'vue',
var: 'Vue',
path: `${baseUrl}/js/plugin/vue3/vue.global.min.js`,
},
{
name: "axios",
var: "axios",
path: `${baseUrl}/js/plugin/axios/axios.min.js`
},
{
name: "element-plus",
var: "ElementPlus",
path: `${baseUrl}/js/plugin/elementplus/index.full.min.js`,
css: `${baseUrl}/js/plugin/elementplus/index.min.css`
},
],
}),
syncToPhp(), // 在每次构建都执行同步逻辑
],
base: `${baseUrl}/vite/`,
server: {
// 端口号
// port: VITE_PORT,
host: "0.0.0.0",
// 本地跨域代理 暂不使用
proxy: {
"/api": {
target: "http://xxxxxx.cn",
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, "")
}
},
},
build: {
minify: false,
},
css: {
// CSS 预处理器
preprocessorOptions: {
//define global scss variable
scss: {
javascriptEnabled: true,
additionalData: `@import "src/styles/index.scss";`
}
}
}
}
})
vite/plugins/syncToPhp.ts 同步php资源与文件代码
import { Plugin } from 'vite';
import fs from 'fs';
import path from 'path';
import syncToPhpList from './syncToPhpList.json';
// 确保目录存在的函数
const ensureDirectoryExistence = (filePath:string) => {
const dirname = path.dirname(filePath);
if (fs.existsSync(dirname)) {
return true;
}
ensureDirectoryExistence(dirname);
fs.mkdirSync(dirname);
};
// 复制 HTML 文件的函数
const copyHtmlFiles = (sourceHtml:string, targetBaseHtmlDir:string, htmlContent:string) => {
syncToPhpList.forEach(o => {
const newHtmlContent = `<script>window.__vue__router_path = '${o.name}'</script>\n${htmlContent}`;
const targetHtmlPath = path.join(targetBaseHtmlDir, o.path);
ensureDirectoryExistence(targetHtmlPath);
fs.writeFileSync(targetHtmlPath, newHtmlContent);
});
};
// 复制 Assets 文件的函数
const copyAssetFiles = (sourceAssetDir:string, targetAssetDir:string) => {
const files = fs.readdirSync(sourceAssetDir);
files.forEach(file => {
const sourcePath = path.join(sourceAssetDir, file);
const targetPath = path.join(targetAssetDir, file);
ensureDirectoryExistence(targetPath);
fs.copyFileSync(sourcePath, targetPath);
});
};
export default function syncToPhp(): Plugin {
return {
name: 'syncToPhp',
apply: 'build', // 仅在构建时应用此插件
closeBundle: async () => {
console.log('同步代码到php项目-->>开始');
const sourceHtml = './dist/index.html';
const targetBaseHtmlDir = './../application/views';
const sourceAssetDir = './dist/assets';
const targetAssetDir = '../public/vite/assets';
if (syncToPhpList.length === 0) {
console.error('构建的列表不能为空');
return;
}
try {
const htmlContent = fs.readFileSync(sourceHtml, 'utf-8');
copyHtmlFiles(sourceHtml, targetBaseHtmlDir, htmlContent);
copyAssetFiles(sourceAssetDir, targetAssetDir);
} catch (error) {
console.error('构建异常', error);
}
console.log('同步代码到php项目-->>结束');
}
};
}
vite/plugins/syncToPhpList.json 根据文件配置映射生成
[
{ "name": "testvue3" , "path": "/test/testvue3.phtml"},
{ "name": "list" , "path": "/test/list.phtml"}
]
vite/src/App.vue
<script setup lang="ts">
import { zhCn } from "element-plus/es/locale/index.mjs";
import { onMounted } from "vue";
import { useRouter } from 'vue-router';
const router = useRouter();
onMounted(() => {
//这里做vue的路由跳转
console.log("window.__vue__router_path",window.__vue__router_path)
if(window.__vue__router_path) {
router.push(`/${window.__vue__router_path}`);
}
})
</script>
<template>
<el-config-provider :locale="zhCn">
<router-view></router-view>
</el-config-provider>
</template>
<style scoped lang="scss">
</style>
业务代码
vite/src/router/index.ts 路由
import { createRouter,createWebHashHistory } from 'vue-router';
const testvue3 = () => import('../views/testvue3.vue')
const list = () => import('../views/list.vue')
const routes = [
{ path: '/testvue3', component: testvue3 }, //
{ path: '/list', component: list }, //
];
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes,
});
export default router;
vite/src/views/testvue3.vue
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup lang="ts">
//列表查询
import { ref} from 'vue'
const count = ref(0);
const increment = () => {
count.value++;
};
</script>
<style lang="scss">
</style>
vite/src/views/list.vue
<template>
<div>
<el-form ref="queryRef" :model="queryForm" inline label-width="100px">
<el-row>
<el-col :xl="25" :lg="6" :md="8" :sm="12">
<el-form-item label="名称" prop="name">
<el-input v-model="queryForm.name" />
</el-form-item>
</el-col>
<el-col :xl="25" :lg="6" :md="8" :sm="12">
<div class="searchOpreaBox">
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
</el-col>
</el-row>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain :disabled="!multiple" @click="handleDelete">删除</el-button>
</el-col>
</el-row>
</el-form>
<el-row class="">
<el-col>
<el-table :data="tableData" v-loading="loading" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center"> </el-table-column>
<el-table-column label="姓名" align="center" prop="name"></el-table-column>
<el-table-column label="年龄" align="center" prop="age"></el-table-column>
</el-table>
</el-col>
</el-row>
<Pagination :pagination="pagination" />
<el-dialog title="新增/修改" v-model="open" width="500px" :close-on-click-modal="false">
<el-form v-loading="postLoading" ref="postRef" :model="postForm" :rules="postRules" label-width="80px">
<el-row>
<el-col>
<el-form-item label="姓名" prop="name">
<el-input v-model="postForm.name" />
</el-form-item>
</el-col>
<el-col>
<el-form-item label="年龄" prop="range">
<el-input v-model="postForm.age" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="cancelForm">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
//列表查询
import { reactive, toRefs ,onMounted} from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus/es'
import useList from '@/utils/list.ts'
import Pagination from '@/components/Pagination.vue';
import {cloneDeep} from 'lodash'
import api from '@/api/test'
onMounted(() => {
handleQuery();
})
const state = reactive({
queryForm: {
},
loading: false,
postLoading: false,
tableData: [],
postForm: {
name: "",
age: 0,
},
postRules: {
name: [{
required: true,
message: '姓名不能为空',
trigger: 'blur'
}],
},
open: false,
operate: 'add', // add or edit
queryRef: null,
postRef: null
});
const { queryForm, loading, postLoading, tableData, postForm, postRules, open, operate, queryRef, postRef } = toRefs(state)
const queryLisPromise = () => {
let queryClone = {
...queryForm.value,
page: page.value.currentPage,
per_page: page.value.pageSize,
}
return new Promise((resolve, reject) => {
loading.value = true
api.list2(queryClone).then(res => {
loading.value = false
tableData.value = cloneDeep(res.rows);
resolve({
tableData: res.rows,
total: res.total,
});
})
.catch(_ => {
loading.value = false
reject({
tableData: [],
total: 0,
});
})
})
}
const pagination = useList(queryLisPromise, item => item.id);
const { handleQuery, resetPage, ids,page,multiple,handleSelectionChange } = pagination;
const resetQuery = () => {
queryRef.value!.resetFields();
resetPage()
handleQuery();
};
const handleAdd = () => {
if (postRef.value) {
postRef.value.resetFields();
}
operate.value = 'add'
open.value = true;
};
const handleDelete = () => {
//开始删除
ElMessage.success(`删除成功${ids.value}`)
};
const submitForm = () => {
postRef.value.validate((baseValid) => {
//开始提交
ElMessage.success("提交成功")
})
};
const cancelForm = () => {
postLoading.value = false
open.value = false;
}
</script>
<style lang="scss">
</style>
测试
- 构建
cd vite #进入前端目录
npm install # 安装依赖
npm run build:dev # 构建开发环境
npm run build:dev-watch # 构建开发环境-同时实时更新构建
npm run build:test # 构建测试
npm run build:staging # 构建预发布
npm run build:prod # 构建正式
- 无论访问那个路由,都会先响应php的路由即 test(controller) 中的list方法,
- 然后访问到application/views/test/list.phtml
- 该页面会加载整个vue,然后根据提前输出的全局变量
__vue__router_path,通过hash模式跳到vue路由里面的list - 最后url会变成 xxxxx.cn/test/list#l…
补充
有些资源可能需要用到php的标签访问 可以通过transformIndexHtml钩子使用正则替换处理
vite/plugins/sysncToPhp.ts
import { Plugin } from 'vite';
import fs from 'fs';
import path from 'path';
import syncToPhpList from './syncToPhpList.json'
export default function syncToPhp(): Plugin {
return {
name: 'syncToPhp',
apply: 'build', // 仅在构建时应用此插件
//待处理 这里替换有bug
transformIndexHtml(html) {
let result = html;
console.log("html",html)
result = html.replace(
/<script\b[^>]*src="(\/assets\/[^"]+)"[^>]*><\/script>/g,
(match, p1) => {
const newPath = `/vite${p1}`;
return `<script type="module" crossorigin src="<?= $this->helper->basePath('${newPath}'); ?>"></script>`;
}
);
result = result.replace(
/<link\b[^>]*href="(\/assets\/[^"]+)"[^>]*\/?>/g,
(match, p1) => {
const newPath = `/vite${p1}`;
return `<link rel="stylesheet" href="<?= $this->helper->basePath('${newPath}'); ?>" />`;
}
);
console.log('转化html结束');
return result
},
closeBundle: async () => {
console.log('同步代码到php项目-->>开始');
//...
console.log('同步代码到php项目-->>结束');
}
};
}
使用第三方cdn 可以修改 vite配置
vite/vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import {ElementPlusResolver} from 'unplugin-vue-components/resolvers'
import cdnImport from 'vite-plugin-cdn-import';
import syncToPhp from './plugins/syncToPhp';
import { isProd, loadEnv } from './src/utils/vite'
import { resolve } from 'path'
import { viteMockServe } from 'vite-plugin-mock'
import vueJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig(({ mode, command }) => {
const { VITE_BASE_PATH } = loadEnv(mode)
const pathResolve = (dir: string): any => {
return resolve(__dirname, '.', dir)
}
const alias: Record<string, string> = {
'@': pathResolve('./src/'),
'~': pathResolve('./'),
assets: pathResolve('./src/assets'),
}
console.log("VITE_BASE_PATH",VITE_BASE_PATH)
let baseUrl = VITE_BASE_PATH
return {
resolve: { alias },
plugins: [vue(),vueJsx(),
AutoImport({
resolvers: [ElementPlusResolver()],
}), Components({
resolvers: [ElementPlusResolver()],
}),
viteMockServe({
mockPath: './src/mock',
}),
// 添加其他库的 CDN 配置
cdnImport({
prodUrl: "https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}",
modules: [
{
name: 'vue',
var: 'Vue',
path: 'vue.global.prod.min.js',
},
{
name: "axios",
var: "axios",
path: "axios.min.js"
},
{
name: "element-plus",
var: "ElementPlus",
path: "index.full.min.js",
css: "index.min.css"
},
],
}),
syncToPhp(), // 在每次构建都执行同步逻辑
],
//...
}
})