项目组开发了一款基于Vue的项目命名为项目A,多个项目组需要在A基础上进行二次开发,用于满足不同客户的个性化需求。为满足A前端升级时不影响二开新增或修改的内容,并保证A的源码不暴露给客户,考虑将A的前端代码完全打包。
经过一番思考,考虑将A项目以插件的形式引入到二开项目中,通过在二开环境中的特定配置,实现A项目与二开项目间的通讯。
如何将整个项目打包成一个库?这是实现目标的关键。考虑到整个项目渲染的关键在于App.vue。加载了App.vue,就相当于加载了整个项目。因此,我们决定,将App.vue组件打包为插件,大致思路有了,接下来要考虑的便是如何满足二开的各种场景。
首先假设二开项目中使用npm install 安装了A项目,项目名称为project。
main.js文件如下所示
import Vue from 'vue' import Project from 'project' import 'project/lib/project.css' Vue.use(TestBase, options)
需要注意的是,在A项目打包的入口文件中已经执行了挂载,因此二开环境main.js中便没有再写new Vue()。
接下来就是去满足二开的各种场景了:
- 如何获取二开的环境变量 ?
1.1. 二开配置
options中添加processEnv属性配置:{processEnv: process.env}
1.2. A项目修改
在我们的A项目中,环境变量大部分是为了获取baseUrl,这些调用都是在挂载之后执行。因此,添加了一个base.js文件。
const base = { processEnv: process.env, changeEnv: function(env) { base.processEnv = env } } export default base
将之前代码中涉及到process.env的位置均替换为base.processEnv,
在A项目打包入口文件的install方法中,执行base.changeEnv(options.processEnv),
如此,便将二开的环境变量传递至A环境中。
- 如何加载二开新增的路由?
2.1. 二开配置
options中添加routes配置:{routes: [二开添加的路由数组]}
2.2. A项目修改
根据二开添加的路由,A项目需要添加至当前的路由当中使其能够正常加载,在打包的入口文件install方法中执行如下代码:
// 添加二开注册的路由 const routes = options && options.routes || [] const routeAll = await store.dispatch('permission/pushRoutes', routes) store.dispatch('permission/changeAsyncRoutes', routeAll)
其中, store.dispatch('permission/pushRoutes', routes) 为添加路由并返回新旧所有路由数组的方法。store.dispatch('permission/changeAsyncRoutes', routeAll) 为修改权限路由数组asyncRoutes的方法。asyncRoutes中的路由在加载前都会经过一次权限过滤。
大家都可根据自己项目权限控制的方法来新增路由。
方法写好了,总得自测一下。来个简单的吧,路由写好,页面组件写好。启动,登录项目,修改url,哎,怎么空白一片?
权限!自定义的静态界面,要正常显示,需要加入到白名单。
好的,options中添加pathWhiteList属性配置:{ pathWhiteList: '/custom'},A项目中控制权限的whiteList添加上二开传递过来的path,配置好了,启动,打开界面,自定义组件中的内容出来啦!
- 二开如何修改A项目中已有路由对应的界面?
二开提出,可能会对A项目中的静态页面添加功能或修改布局。A项目都打包为js了,如何能修改到这些页面呢,思来想去,决定用二开定义的路由去替换原来的路由。那就开始实现吧!二开配置需要替换的路由,A项目install方法中根据二开提供的路由对象执行替换。
3.1. 二开配置
options中添加replaceRoutes属性配置
{ replaceRoutes: [{ // 二开要替换A项目中的哪些路由 route: routes[1], // 需要替换的路由对象 isAsync: false, // 是静态路由还是需要鉴权的路由 isReplaceAll: false, // 是替换所有的还是只替换第一个匹配到的【一般设为false,请谨慎设为true】 filterProp: 'path' // path或者name等属性值作为唯一的筛选条件 }] }
3.2. A项目端执行替换
// 替换二开需要替换的路由 if (options && options.replaceRoutes && options.replaceRoutes.length) { // 项目中的路由分为静态路由数组与需要鉴权的路由数组 let asyncRoutes = [] // 用于放置二开需替换的鉴权路由 let syncRoutes = [] // 用于放置二开需替换的静态路由 options.replaceRoutes.forEach(item => { item.isAsync ? asyncRoutes.push(item) : syncRoutes.push(item) }) // 1. 替换固定的静态路由 let syncRouteAll = router.options.routes syncRoutes.forEach(item => { // 查找目标路由并执行替换的递归方法,此处省略该方法的书写 replaceTargetRoute(syncRouteAll, item) // 查找目标路由并执行替换的递归方法 }) router.createNewRouter(syncRouteAll) // 2. 替换异步加载的路由 let asyncRouteAll = await store.dispatch('permission/pushRoutes', []) asyncRoutes.forEach(item => { replaceTargetRoute(asyncRouteAll, item) // 查找目标路由并执行替换的递归方法 }) store.dispatch('permission/changeAsyncRoutes', asyncRouteAll) }
- 二开如何调用A项目内部组件?
有了二开替换路由的方法,那就开使写页面吧,问题来了,页面从零开始写?A中那么多组件都已经写好了,直接提供给二开来用就可以,当然,如果二开坚持自己从零写,我们也是不反对的。
根据组件的功能,A项目提供了以下几种方式给二开提供组件:
4.1 layout组件:各个界面的父组件,包含了顶部栏,左侧菜单栏,菜单标签栏,界面容器等组件。
二开使用场景:根据客户的需求,修改layout的布局方式或者向其顶栏添加修改功能,考虑到这种情况,采用了如下策略
4.1.1. A项目端修改:
将现有的A项目layout组件复制出一个layoutCustom.vue,layoutCustom.vue中添加判断,如果有二开提供的CustomLayout,则加载二开的CustomLayout组件,否则,加载A项目默认的layout组件。
在打包入口文件将CustomLayout组件放入export default的对象中。
import Layout from '@/layout/layout' const components = { Layout } install = function (Vue, options) { Vue.component('layout', Layout) } export default { install, ...components }
4.1.2. 二开使用:
install方法中已经全局注册了该组件,因此可直接使用。如果获取其中的子组件配置,可使用如下方法:
import Project from 'project' export default { components: { ...Project.Layout.components } }
4.2 A项目封装的页面建模组件
A项目封装的建模组件均为公用组件,因此会在install方法中注册为全局组件,二开可直接使用,包括A项目中用到的组件库中的组件。
4.3 静态页面组件
由于页面组件过多,入口文件中无法预测二开时需要的页面组件,因此,给二开提供了一个属性,用来配置所需组件,A项目根据配置提供给二开。
4.3.1 二开配置与使用:
配置:options中添加projectComponents属性配置,配置示例:
projectComponents: [{ fileName: 'dashbord.vue', componentName: 'Dashbord' }]
使用:由于A项目将所需页面组件绑定至Vue.prototype.projectComponents中,并进行了全局注册,因此,界面组件可直接调用,其子组件可通过this.projectComponents[xxx组件名称].components获取。
4.3.2 A项目获取组件提供给二开:
A项目通过配置中的fileName获取views文件夹中的目标组件,进行全局注册,并存入一个对象中,以便于获取其内部子组件。
代码示例如下:
if (options.projectComponents && options.projectComponents.length) { let comobj = {} const context = require.context('@/views', true, /.vue$/) const keys = context.keys() || [] const fileNameArr = options.projectComponents.map(v => v.fileName) fileNameArr.forEach((fileName, idx) => { const targetKey = keys.find(v => { const len = v.split('/').length const realFileName = v.split('/')[len - 1] return fileName === realFileName || fileName + '.vue' === realFileName }) const comName = options.apsComponents[idx].componentName if (targetKey && comName) { Vue.component(comName, context(targetKey).default) comobj[comName] = context(targetKey).default // 将组件挂载到一个变量上面 } }) Vue.prototype.projectComponents = comobj }
- 二开关于vuex模块的添加与调用?
A项目中已经引入了vuex,二开时无需再次引用,而且二开与A项目全局的vue实例是同一个,因此,只需要提供扩展的方法即可。
5.1. 二开配置
在options添加stores属性,stores: {}, // json格式,key值为模块名称,value为模块对象
5.2. A项目添加
实现该功能利用了vuex中动态注册模块的方法:store.registerModule()
上代码:
const store = require('./store/index.js').default if (options.stores) { const customModule = options.stores const keys = Object.keys(customModule) keys.forEach(item => { store.registerModule(item, customModule[item]) }) }
- A项目页面设计器中如何添加二开的业务组件?
6.1. 二开配置
NO.1 新增业务组件配置:
options中添加componentInDesignConfig属性,该属性值为业务组件json对象组成的数组,用于添加多个组件配置。
NO.2 创建业务组件
创建目标业务组件Test.vue,Test.vue中组件属性来源于对组件配置的解析。
options中添加components属性配置,用于在A项目中注册该组件。
NO.3 二开业务组件添加属性配置
在A项目的建模设计器中,左侧为控件列表,中间为画布,右侧为控件属性配置。
在属性配置栏加入该组件特定的属性,需要配置一个属性配置组件,该组件中书写业务组件特有的属性配置。
如果需要配置一些A项目已有的公共属性,只需要将json对象中添加对应的属性名称即可。
options中添加components属性配置,用于在A项目中注册该组件,该组件名称需要固定,A项目根据组件名称,会将该组件用在特定 的位置,如固定组件名称为customProp: () => import('@/business/prop')。
6.2. A项目根据配A项目置添加组件
NO.1 注册二开组件
根据options.components属性,将components中的组件进行全局注册,如果发现存在customProp组件,就在根实例中添加一个hasCustomProp = true
NO.2 组件列表追加组件
在设计器左侧的组件列表中,追加options.componentInDesignConfig中的组件配置
NO.3 属性配置栏追加属性
在属性配置的组件中,寻找特定的位置添加,根据hasCustomProp的值进行判断是否存在自定义的属性组件,如果有则将propName赋值为'customProp'。如此,便可以将自定义的属性组件添加至右侧的属性栏中。
以上这些已经基本满足二开的使用场景,这种使用方式既可以满足升级的便利性(直接修改package.json中的版本号,执行npm install即可升级),又避免了源码暴露给客户。
最后,也欢迎大家提出宝贵的意见和建议,共同讨论学习!