做者:defghycss
随着项目的成长,单页spa
逐渐包含了许多业务线html
当项目页面超过必定数量(150+)以后,会产生一系列的问题前端
项目编译的时间(启动server,修改代码)愈来愈长,而每次调试关注的可能只是其中一、2个页面vue
全部的需求都定位到当前git,需求过多致使测试环境常常排队webpack
基于以上问题有了对git进行拆分的技术需求。具体以下git
spa
因为改善的是开发环境,固然不但愿拆分项目影响用户体验。若是彻底将业务线拆分红2个独立页面,那么用户在业务线之间跳转时将再也不流畅,由于全部框架以及静态资源都会在页面切换的时候重载。所以要求跳转业务线的时候依然停留在spa内部,
不刷新页面
,共用同一个页面入口;web
由于大部分业务线须要用到的框架(
vue
,vuex
...), 公共组件(dialog
,toast
)都已经在spa入口加载过了,不但愿业务线重复加载这些资源。业务线项目中应该只包含本身独有的资源,并能使用公共资源;vue-router
业务线之间应该能用router互相跳转,能访问其余业务线包括全局的storevuex
需求如上,下面介绍的实现方式express
假设要从主项目拆分一个业务线 hello
出来
#/hello/index
;*
处理;bundle js
;chunk
(js,css)页面跳转成功;须要的功能就是这些,下面分步骤看看具体实现
第一次请求#/hello/index
时,此时router中全部路由没法匹配,会走公共*
处理
/** 主项目 **/
const router = new VueRouter({
routes: [
...
// 不一样路由默认跳转连接不一样
{
path: '*',
async beforeEnter(to, from, next) {
// 业务线拦截
let isService = await service.handle(to, from, next);
// 非业务线页面,走默认处理
if(!isService) {
next('/error');
}
}
}
]
});
复制代码
首先须要一个全局的业务线配置,存放各个业务线的入口js文件
const config = {
"hello": {
"src": [
"http://local.aaa.com:7000/dist/dev/js/hellobundle.js"
]
},
"其余业务线": {...}
}
复制代码
此时须要利用业务线配置,判断当前路由是否属于业务线,是的话就请求业务线,不是返回false
/** 主项目 **/
// 业务线接入处理
export const handle = async (to, from, next) => {
let path = to.path || "";
let paths = path.split('/');
let serviceName = paths[1];
let cfg = config[serviceName];
// 非业务线路由
if(!cfg) {
return false;
}
// 该业务线已经加载
if(cfg.loaded) {
next();
return true;
}
for(var i=0; i<cfg.src.length; i++) {
await loadScript(cfg.src[i]);
}
cfg.loaded = true;
next(to); // 继续请求页面
return true;
}
复制代码
有几点须要注意
loaded
为断定条件。加载过的话直接进行next#/hello/index
的路由,此时next能够正常跳转。缘由见下一节为了节省资源,hello业务线再也不重复打包vue
,vuex
等主项目已经加载的框架。
那么为了hello能正常工做,须要主项目将以上框架传递给hello,方法为直接将相关变量挂在到window
:
/** 主项目 **/
import Vue from 'vue';
import { default as globalRouter } from 'app/router.js'; 2个须要动态赋值
import { default as globalStore } from 'app/vuex/index.js';
import Vuex from 'vuex'
// 挂载业务线数据
function registerApp(appName, {
store,
router
}) {
if(router) {
globalRouter.addRoutes(router);
}
if(store) {
globalStore.registerModule(appName, Object.assign(store, {
namespaced: true
}));
}
}
window.bapp = Object.assign(window.bapp || {}, {
Vue,
Vuex,
router: globalRouter,
store: globalStore,
util: {
registerApp
}
});
复制代码
注意registerApp
这个方法,此方法为hello与主项目融合的挂载方法,由业务线调用。
上一步已经正常运行了hello的entry.js,那咱们看看hello在entry中干了什么:
/** hello **/
import App from 'app/pages/Hello.vue'; // 路由器根实例
import {APP_NAME} from 'app/utils/global';
import store from 'app/vuex/index';
let router = [{
path: `/${APP_NAME}`,
name: 'hello',
meta: {
title: '页面测试',
needLogin: true
},
component: App,
children: [
{
path: 'index',
name: 'hello-index',
meta: {
title: '商品列表'
},
component: resolve => require.ensure([], () => resolve(require('app/pages/goods/Goods.vue').default), 'hello-goods')
},
{
path: 'newreq',
name: 'hello-newreq',
meta: {
title: '新品页面'
},
component: resolve => require.ensure([], () => resolve(require('app/pages/newreq/List.vue').default), 'hello-newreq')
},
]
}]
window.bapp && bapp.util.registerApp(APP_NAME, {router, store});
复制代码
注意几点
APP_NAME
是业务线的惟一标识,也就是hellorouter
和store
registerApp
,将本身的router和store与主项目融合namespace: true
,由于此时整个hello业务线store成为了globalStore的一个moduleaddRoutes
和registerModule
是router与store的动态注册方法name
须要和主项目保持惟一业务线配置须要在hello每次编译完成后更新,更新分为本地调试更新
和线上更新
。
本地调试更新
只须要更新一个本地配置文件service-line-config.json
,而后在请求业务线config时由主项目读取该文件返回给js。线上更新
更为简单,每次发布编译后,将当前入口js+md5的完整url更新到后端以上,看到使用webpack-plugin
比较适合当前场景,实现以下
class ServiceUpdatePlugin {
constructor(options) {
this.options = options;
this.runCount = 0;
}
// 更新本地配置文件
updateLocalConfig({srcs}) {
....
}
// 更新线上配置文件
uploadOnlineConfig({files}) {
....
}
apply(compiler) {
// 调试环境:编译完毕,修改本地文件
if(process.env.NODE_ENV === 'dev') {
// 本地调试没有md5值,不须要每次刷新
compiler.hooks.done.tap('ServiceUpdatePlugin', (stats) => {
if(this.runCount > 0) {
return;
}
let assets = stats.compilation.assets;
let publicPath = stats.compilation.options.output.publicPath;
let js = Object.keys(assets).filter(item => {
// 过滤入口文件
return item.startsWith('js/');
}).map(path => `${publicPath}${path}`);
this.updateLocalConfig({srcs: js});
this.runCount++;
});
}
// 发布环境:上传完毕,请求后端修改
else {
compiler.hooks.uploaded.tap('ServiceUpdatePlugin', (upFiles) => {
let entries = upFiles.filter(file => {
return file &&
file.endsWith('js') &&
file.includes('js/');
});
this.uploadOnlineConfig({files: entries});
return;
})
}
}
}
复制代码
注意,uploaded
事件由咱们项目组的静态资源上传plugin发出,会传递当前全部上传文件完整路径。须要等文件上传cdn完毕才可更新业务线
以后在webpack中使用便可
/** hello **/
{
...
plugins: [
// 业务线js md5更新
new McServiceUpdatePlugin({
app_name,
configFile: path.resolve(process.cwd(), '../mainProject/app/service-line-config.json')
})
],
...
}
复制代码
注意本地调试时业务线config是主项目
才会用到的,所以直接更新主项目目录下的配置文件
基于上面的plugin,有如下效果
7777
);7000
),此时启动成功会同时更新本地文件service-line-config.json
;7000
端口提供的静态资源(如http://local.aaa.com:7000/dist/dev/js/hellobundle.js)npm run test
能够看到hello发布是比主项目更加轻量的,这是由于业务线只更新接口,可是主项目要发布还须要更新html的web服务
至此已经完成了一开始的主体需求,访问业务线页面后,业务线页面会和主项目页面合并成为1个新的spa,spa内部store和router彻底共享。
能够看到主要利用了vue家族的动态注册方法。下面是一些过程当中遇到的问题和解决思路
bundle
重命名,增长了业务线名称前缀vendor
: 主要第三方库由主项目加载dll
: dll资源由主项目加载manifest
)配置: 各业务线将各自处理依赖加载/** hello **/
{
...
entry: {
[app_name + 'bundle']: path.resolve(SRC, `entry.js`)
},
output: {
publicPath: `http://local.aaa.com:${PORT}${devDefine.publicPath}`,
library: app_name // 业务线命名空间
},
...
optimization: {
runtimeChunk: false, // 依赖处理与bundle合并
splitChunks: {
cacheGroups: false // 业务线不分包
}
},
...
}
复制代码
注意library
的设置隔离了各个业务线 入口文件
依赖
最开始使用/:name来作公共处理。
可是发现router的优先级按照数组的插入顺序,那么后插入的hello路由优先级将一直低于/:name路由。
以后使用*
作公共处理,将一直处于兜底,问题解决。
hello的store作为globalStore的一个module注册,须要标注 namespaced: true
,不然拿不到数据;
store使用基本和主项目一致:
/** hello **/
let { Vuex } = bapp;
// 全局store获取
let { mapState: gmapState, mapActions: gmapActions, createNamespacedHelpers } = Vuex;
// 本业务线store获取
const { mapState, mapActions } = createNamespacedHelpers(`${APP_NAME}/feedback`)
export default {
...
computed: {
...gmapState('userInfo', {
userName: state => state.userName
}),
...gmapState('hello/feedback', {
helloName2: state => state.helloName
}),
...mapState({
helloName: state => state.helloName
})
},
}
复制代码
虽然前端工程拆分了,可是后端接口依然是走相同的域名,所以能够给hello暴露一个生成接口参数的公共方法,而后由hello本身组织。
能够直接使用全局组件
,mixins
,directives
,能够直接使用font
。 局部的相关内容须要拷贝到hello或者暴露给hello才可用。 图片彻底没法复用
主项目因为须要对request有比较精细的操做,所以是咱们本身实现的express
来本地调试。
可是hello工程的惟一做用是提供本地当前的js与css,所以使用官方devServer
就够了。
以上,感谢阅读
原文连接: tech.meicai.cn/detail/75, 也可微信搜索小程序「美菜产品技术团队」,干货满满且每周更新,想学习技术的你不要错过哦。