前端项目中很多时候会遇到这样的业务需求:
- 所有的客户都有共同的一些业务需求,即基础需求
- 基础需求需要根据业务发展进行升级
- 在共同需求的基础上,每个客户又有不同的需求(客制化)
如果只是一两个客户还好,在基础版本上分两份前端文件,分别维护。但是客户达到一定数量级后,比如5个、8个甚至更多,这时候这种机械的本办法就费时耗力了,如果再考虑到基础功能的版本升级需求,那这种方法就完全不行了(升级一个基础功能就要分别复制到每个客户的项目中,同步代码)。
以上情况就是我司的实际情况,下面介绍一下我司用的方法,不一定是最好的解决方案,如读者有更好的方法,欢迎留言交流。
- 针对第一点,首先根据产品对行业的需求了解,指定出基础版本的功能,基础功能涵盖大部分客户的最基本需求,开发出一套前端对应的版本,即基础版。
- 针对第二点,借助git版本管理工具,每次大的升级都创建一个分支,基础版为1.0.1,升级版本后,历史版本便封版,不再做改动(bug除外),后续可以根据客户对基础功能的需求决定在哪个版本的基础上做定制化开发。
- 针对第三点,客制化开发主要通过前端动态生成vue组件统一放到一个文件里的方式满足,也是本文的重点。
思路:通过node动态创建默认路由文件和路由所需的组件列表文件
在package.json的script的dev和build命令中配置CLIENT字段标识当前启动项的客户,如果不加CLIENT字段则视为标准版本开发或构件
启动项目的时候同时运行指定js文件buildViews.js,该文件负责动态生成后续router需要的文件
buildViews.js中通过CLIENT字段拉取对应的客户路由配置文件./anxin/index,和标准文件的模块进行对比,以客制化文件为准,覆盖、替换或删除标准文件里的模块,组合为新的路由数据
"scripts": {
"serve": "vue-cli-service serve",
"buildViews": "node build/buildViews.js",
"buildViews:landq": "cross-env CLIENT=landq node build/buildViews.js",
"dev": "npm run buildViews && npm run serve",
"dev:landq": "npm run buildViews:landq && npm run serve",
"build": "npm run buildViews && vue-cli-service build",
"build:landq": "npm run buildViews:landq && vue-cli-service build",
"lint": "vue-cli-service lint"
},
通过node命令fs.writeFile()使用对比更新后的配置数据,在指定文件夹下创建后续路由所需要的的视图模块文件,动态生成的文件大致长这样
const Home = resolve => require(['view/home/index'], resolve);
、、、、、
export {
Home,
、、、
}
下面来看具体代码层面的实现
首先安装依赖json-templater/tring用于生成文件模板,chalk用于在控制待打印信息,cross-env用于设置客制化信息
目录结构:
这是初始的目录结构,client专门存放客制化的路由文件:客户landq和anxin,比如版本信息和info.js、用于生成路由的index.js,standerd文件夹存放标准版本的相关信息;buildViews.js用于动态创建路由数据
先在info.js中把客户名、版本信息等添加上
standerd/info.js
module.exports = {
clientName: 'standerd',
clientFullName: '标准版本',
clientSystemName: '系统名称',
version: '1.0.0',
packageTime: new Date().toLocaleString(),
}
client/landq/info.js
module.exports = {
clientName: 'landq',
clientFullName: '客户landq',
clientSystemName: '系统名称',
version: '1.0.0',
packageTime: new Date().toLocaleString(),
}
重点是buildViews.js,整合标准版本和客制化版本的代码都在这里,对组件的添加、替换、删除操作
console.log(process.env.CLIENT)
const fs = require('fs');
const path = require('path');
const endOfLine = require('os').EOL;
const render = require('json-templater/string'); // 模板渲染
const chalk = require('chalk'); // 控制台打印带颜色信息
// 获取标准版配置
let { STANDERD_IMPORT, STANDERD_ROUTERS } = require('./standerd')
// 获取客制化配置
let CLIENT = process.env.CLIENT || ''
if (CLIENT) {
var { CLIENT_IMPORT, CLIENT_ROUTERS } = require(`./client/${CLIENT}`)
} else {
var { CLIENT_IMPORT, CLIENT_ROUTERS } = {
CLIENT_IMPORT: {},
CLIENT_ROUTERS: {}
}
}
// 获取版本信息
let versionConfig
if (CLIENT) {
versionConfig = require(`./client/${CLIENT}/info`)
} else {
versionConfig = require(`./standerd/info`)
}
console.log(chalk.yellow(`当前执行版本信息:${versionConfig.clientFullName}-${versionConfig.version}`))
console.log(chalk.yellow(`编译时间:${versionConfig.packageTime}`))
// 生成router/importViews.js
const IMPORT_VIEWS_PATH = path.join(__dirname, '../src/router/importViews.js')
const IMPORT_VIEWS_IMPORT_TEMPLATE = 'const {{name}} = resolve => require([\'{{path}}\'], resolve)'
const IMPORT_VIEWS_MAIN_TEMPLATE =
`/* 由build/buildViews.js自动生成 */
// 引入所有页面组件
{{include}}
// 导出所有页面
export {
{{list}}
}`
// 以客制化为标准,更新标准文件,替换或新增
let useViews = {}
if (CLIENT) {
// 新增客制化组件,组合为新的文件信息
useViews = Object.assign(STANDERD_IMPORT, CLIENT_IMPORT.update)
// 删除不需要的标准组件
CLIENT_IMPORT.delete.forEach(name => {
if (useViews[name]) delete useViews[name]
})
} else {
useViews = STANDERD_IMPORT
}
let importAllViewsArr = []
let importAllViewsNameArr = []
// 生成const [Login = resolve => require(['./src/login'], resolve),Home = resolve => require(['./src/home'], resolve)]
Object.keys(useViews).forEach(name => {
importAllViewsArr.push(render(IMPORT_VIEWS_IMPORT_TEMPLATE, {
name: name,
path: useViews[name]
}))
importAllViewsNameArr.push(` ${name}`)
})
// 生成主模板
console.log(useViews)
let importMainTemplate = render(IMPORT_VIEWS_MAIN_TEMPLATE, {
include: importAllViewsArr.join(endOfLine),
list: importAllViewsNameArr.join(`,${endOfLine}`)
})
fs.writeFileSync(IMPORT_VIEWS_PATH, importMainTemplate)
// 生成router/router.js
const CLIENT_ROUTER_PATH = path.join(__dirname, '../src/router/router.js')
const CLIENT_ROUTER_IMPORT_TEMPLATE = 'import {{name}} from \'{{path}}\';'
// 默认路由模板
const CLIENT_ROUTER_MAIN_TEMPLATE =
`/* 由build/buildViews.js自动生成 */
import { Layout } from './importViews.js';
{{include}}
export default {
path: '/',
meta: {
name: '首页'
},
redirect: '/home',
component: Layout,
children: [
{{list}}
]
}`
// 以客制化为标准,更新标准文件
let useRouters = {}
if (CLIENT) {
// 新增客制化组件,组合为新的文件信息
useRouters = Object.assign(STANDERD_ROUTERS, CLIENT_ROUTERS.update)
// 删除不需要的标准组件
CLIENT_ROUTERS.delete.forEach(name => {
if (useRouters[name]) delete useRouters[name]
})
} else {
useRouters = STANDERD_ROUTERS
}
let importAllRoutersArr = []
let importAllRoutersNameArr = []
// 生成const [import System from './standerd/system';]
Object.keys(useRouters).forEach(name => {
importAllRoutersArr.push(render(CLIENT_ROUTER_IMPORT_TEMPLATE, {
name: name,
path: useRouters[name]
}))
importAllRoutersNameArr.push(` ${name}`)
})
// 生成主模板
let importMainRouterTemplate = render(CLIENT_ROUTER_MAIN_TEMPLATE, {
include: importAllRoutersArr.join(endOfLine),
list: importAllRoutersNameArr.join(`,${endOfLine}`)
})
fs.writeFileSync(CLIENT_ROUTER_PATH, importMainRouterTemplate)
// 生成模板信息
const VERSION_CONFIG_OUTPUT_PATH = path.join(__dirname, '../static/version.js')
fs.writeFileSync(VERSION_CONFIG_OUTPUT_PATH, JSON.stringify(versionConfig))
现在来看一个简单的标准版本需求
要有一个layout页面,一个home页面,login页面。视图页面已经创建好了,标准版本根路径路由指向home页
配置build/standerd/index.js
/**
* 针对引入组件页面的配置(router/importViews.js)
*/
const STANDERD_IMPORT = {
Layout: '@/layout/index',
Home: '@/views/home',
Login: '@/views/login'
}
/**
* 针对引入组件页面的配置(router/router.js)
*/
const STANDERD_ROUTERS = {
BaseRouter: './standerd/baseRouter.js',
}
module.exports = {
STANDERD_IMPORT,
STANDERD_ROUTERS
}
此时运行npm run dev便会在router文件夹下生成router.js和importViews.js
此时src/router/importViews.js
/* 由build/buildViews.js自动生成 */
// 引入所有页面组件
const Layout = resolve => require(['@/layout/index'], resolve)
const Home = resolve => require(['@/views/home'], resolve)
const Login = resolve => require(['@/views/login'], resolve)
// 导出所有页面
export {
Layout,
Home,
Login
}
src/router/router.js
/* 由build/buildViews.js自动生成 */
import { Layout } from './importViews.js';
import BaseRouter from './standerd/baseRouter.js';
export default {
path: '/',
meta: {
name: '首页'
},
redirect: '/home',
component: Layout,
children: [
BaseRouter
]
}
此时配置src/router/standerd/baseRouter.js
import { Home, Login } from '../importViews'
export default {
path: '/home',
meta: {
name: 'home'
},
component: Home,
children: [{
path: '/login',
meta: {
name: 'login'
},
component: Login,
}]
}
配置完成,页面效果如下
点击按钮,调整到login页面
现在加入有个客户landq想要定制化一个客户列表页面,用来展示客户,那么就需要进行客制化配置了:
home页和login页是基础页面,我想要增加一个客户列表页面customerList
新建空页面views/landq/customerList/index.vue
配置build/client/landq/index.js
/**
* 针对引入组件页面的配置(router/importViews.js)
* update:需要更新的页面地址
* delete:需要删除的引用地址
*/
const CLIENT_IMPORT = {
update: {
CustomerList: '@/views/landq/customerList', // 在标准版本的基础上替换或者新增Home文件
},
delete: [
'PageList' // 需要删除标准版本的组件名,没有则为空
]
}
/**
* 针对引入组件页面的配置(router/router.js)
*/
const CLIENT_ROUTERS = {
update: {
CustomerList: './client/landq/customerList.js' // 替换标准版本的system模块路由文件
},
delete: [
'Customer' // 需要删除标准版本的路由模块名称,没有则为空
]
}
module.exports = {
CLIENT_IMPORT,
CLIENT_ROUTERS
}
此时生成的src/router/importViews.js
/* 由build/buildViews.js自动生成 */
// 引入所有页面组件
const Layout = resolve => require(['@/layout/index'], resolve)
const Home = resolve => require(['@/views/home'], resolve)
const Login = resolve => require(['@/views/login'], resolve)
const CustomerList = resolve => require(['@/views/landq/customerList'], resolve)
// 导出所有页面
export {
Layout,
Home,
Login,
CustomerList
}
多了一个CustomerList客制化组件
src/router/router.js
/* 由build/buildViews.js自动生成 */
import { Layout } from './importViews.js';
import BaseRouter from './standerd/baseRouter.js';
import CustomerList from './client/landq/customerList.js';
export default {
path: '/',
meta: {
name: '首页'
},
redirect: '/home',
component: Layout,
children: [
BaseRouter,
CustomerList
]
}
比标准版本多了个CustomerList的路由
然后手动新建src/router/client/landq/customerList.js
import { CustomerList } from '../../importViews'
export default {
path: '/customerList',
meta: {
name: 'customerList'
},
component: CustomerList,
}
点击首页的to customerList按钮即可跳转到客制化页面
这样就完成了一个基于标准功能的客制化开发的项目搭建。运行npm run build:landq可以定制化构建生产包。
用node在本地8000端口起一个服务(搜索一下,挺简单的),将客制化的压缩包放进指定目录就可以看到页面效果了。
运行npm run dev构建基础版本包,放到本地服务目录下对比效果
可以看到基础版是没有customerList页面的
git地址:https://github.com/LandQ123/client-vue
这种客制化的方法避免了每一个客户都维护一个分支的复杂,对于这种业务需求,整体来说维护起来相对来说更轻松,当然也有其本身的一些局限性和问题,比如后期以及迭代了多个版本了,发现最前面的版本有bug需要修复,或者说有客户想要在之前的某个版本基础上定制功能,就需要切换到指定版本开发或修改,蛋疼的是要从前往后同步代码,基础版本必须逐条分支合并同步,难免会有冲突的地方,会耗时费力;客制化分支也需要把之前改动了的同步到后面该客户有客制化功能的分支。
不过分支的管理是每一个项目必须面对的,只要有迭代就有维护和管理,只能在实践中慢慢总结优化。