预加载微服务框架 https://github.com/niuyueyang/weifuku
预加载路由
解释点
- 子项目按照 vue-cli 3 的 library 模式进行打包,以便后续主项目引用
注:在 library 模式中,Vue 是外置的。这意味着包中不会有 Vue,即便你在代码中导入了 Vue。如果这个库会通过一个打包器使用,它将尝试通过打包器以依赖的方式加载 Vue;否则就会回退到一个全局的 Vue 变量。 - 在编译主项目的时候,通过 InsertScriptPlugin 插件将子项目的入口文件 main.js 以 script 标签形式插入到主项目的 html 中
class InsertScriptWebpackPlugin {
constructor(options = {}) {
const { files = [] } = options;
this.files = files;
}
apply(compiler) {
const self = this;
compiler.hooks.compilation.tap(
'InsertScriptWebpackPlugin',
(compilation) => {
if (compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing) {
compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing.tap(
'InsertScriptWebpackPlugin',
(htmlPluginData) => {
const {
assets: { js },
} = htmlPluginData;
js.unshift(...self.files);
},
);
} else {
console.log('\n');
console.log(
'\x1b[41m%s\x1b[0m',
'Error:',
'`insert-script-webpack-plugin` dependent on `html-webpack-plugin`',
);
}
},
);
}
}
module.exports = InsertScriptWebpackPlugin;
注:务必将子项目的入口文件 main.js 对应的 script 标签放在主项目入口文件 app.js 的 script 标签之上,这是为了确保子项目的入口文件先于主项目的入口文件代码执行,接下来的步骤就会明白为什么这么做。
再注:本地开发环境下项目的入口文件编译后的 main.js 是保存在内存中的,所以磁盘上看不见,但是可以访问。
InsertScriptPlugin 核心代码如下:
compiler.hooks.compilation.tap('InsertScriptWebpackPlugin', (compilation) => {
compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing.tap(
'InsertScriptWebpackPlugin',
(htmlPluginData) => {
const {
assets: { js }
} = htmlPluginData;
// 将传入的 js 以 script 标签形式插入到 html 中
// 注意:需要将子项目的入口文件 main.js 放在主项目入口文件 app.js 之前,因为需要子项目提前将自己的 route list 注册到全局上
js.unshift(...self.files);
}
);
});
- 主项目的 html 要访问子项目里的编译后的 js / css 等资源,需要进行代理转发
- 如果是本地开发时,可以通过 webpack 提供的 proxy,例如
const PROXY = {
'/app-typescript/': {
target: 'http://localhost:10241/'
},
'/app-javascript/': {
target: 'http://localhost:10242/'
},
'/test/': {
target: 'http://localhost:10243/'
}
}
- 如果是线上部署时,可以通过 nginx 转发或者将打包后的主项目和子项目放在一个文件夹中按照相对路径引用。
- 当浏览器解析 html 时,解析并执行到子项目的入口文件 main.js,将子项目的 route list 注册到 Vue.share.routes 上,以便后续主项目将其合并到总的路由中。
子项目 main.js 代码如下:(为了尽量减少首次主项目页面渲染时加载的资源,子项目的入口文件建议只做路由挂载)
import Vue from 'vue';
import routes from './routes';
const share = (Vue.__share__ = Vue.__share__ || {});
const routesPool = (share.routes = share.routes || {});
// 将子项目的 route list 挂载到 Vue.__share__.routes 上,以便后续主项目将其合并到总的路由中
routesPool[process.env.VUE_APP_NAME] = routes;
- 继续向下解析 html,解析并执行到主项目 main.js 时,从 Vue.share.routes 获取所有子项目的 route list,合并到总的路由表中,然后初始化一个 vue-router 实例,并传入到 new Vue 内
// 从 Vue.__share__.routes 获取所有子项目的 route list,合并到总的路由表中
const routes = Vue.__share__.routes;
export default new Router({
routes: Object.values(routes).reduce((acc, prev) => acc.concat(prev), [
{
path: '/',
redirect: '/app-typescript'
}
])
});
另外如果需要使用 vuex,则和 vue-router 的顺序恰好相反(先主项目后子项目):
1.首先在主项目的入口文件中初始化一个 store 实例 new Vuex.Store,然后挂在到 Vue.share.store 上
2.然后在子项目的 App.vue 中获取到 Vue.share.store 并调用 store.registerModule(‘app-x’, store),将子项目的 store 作为子模块注册到 store 上
懒加载微服务框架
框架 https://github.com/niuyueyang/weifuwuyibu
定义
懒加载路由,顾名思义,就是说等到用户点击要进入子项目模块,通过解析即将跳转的路由确定是哪一个子项目,然后再异步去加载该子项目的入口文件 main.js(可以通过 systemjs 或者自己写一个动态创建 script 标签并插入 body 的方法)。加载成功后就可以将子项目的路由动态添加到主项目总的路由里了。
解释
- 主项目 router.js 文件中定义了在 vue-router 的 beforeEach 钩子去拦截路由,并根据即将跳转的路由分析出需要哪个子项目,然后去异步加载对应子项目入口文件,下面是核心代码:
const cachedModules = new Set();
router.beforeEach(async (to, from, next) => {
const [, module] = to.path.split('/');
if (Reflect.has(modules, module)) {
// 如果已经加载过对应子项目,则无需重复加载,直接跳转即可
if (!cachedModules.has(module)) {
const { default: application } = await window.System.import(modules[module])
if (application && application.routes) {
// 动态添加子项目的 route-list
router.addRoutes(application.routes);
}
cachedModules.add(module);
next(to.path);
} else {
next();
}
return;
}
});
- 子项目的入口文件 main.js 仅需要将子项目的 routes 暴露给主项目即可,代码如下:
import routes from './routes';
export default {
name: 'javascript',
routes,
beforeEach(from, to, next) {
console.log('javascript:', from.path, to.path);
next();
},
}
注意:这里除了暴露 routes 方法外,另外又暴露了 beforeEach 方法,其实就是为了支持通过路由守卫对子项目进行页面权限限制,主项目拿到这个子项目的 beforeEach,可以在 vue-router 的 beforeEach 钩子执行,具体代码请参考 async-routes。
除了主项目和子项目的交互方式不同,代理转发子项目资源、vuex store 注册等和上面的预加载路由完全一致。
优缺点
下面谈下这套方案的优缺点:
优点
-
子项目可单独打包、单独部署上线,提升了开发和打包的速度
-
子项目之间开发互相独立,互不影响,可在不同仓库进行维护,减少的单个项目的规模
-
保持单页应用的体验,子项目之间切换不刷新
-
改造成本低,对现有项目侵入度较低,业务线迁移成本也较低
-
保证整体项目统一一个技术栈
缺点:
主项目和子项目需要共用一个 Vue 实例,所以无法做到某个子项目单独使用最新版 Vue(例如 Vue3)或者 React
部分问题解答
-
如果子项目代码更新后,除了打包部署子项目之外,还需要打包部署主项目吗?
不需要更新部署主项目。这里有个 trick 上文忘记提及,就是子项目打包后的入口文件并没有加上 chunkhash,直接就是 main.js(子项目其他的 js 都有 chunkhash)。也就是说主项目只需要记住子项目的名字,就可以通过 subapp-name/main.js 找到子项目的入口文件,所以子项目打包部署后,主项目并不需要更新任何东西。
-
针对第二个问题中子项目入口文件 main.js 不使用 chunkhash 的话,如何防止该文件始终被缓存呢?
可以在静态资源服务器端针对子项目入口文件设置强制缓存为不缓存,下面是服务器为 nginx 情况的相关配置:
location / {
set e x p i r e s t i m e 7 d ; . . . i f ( expires_time 7d; ... if ( expirestime7d;...if(request_uri ~* /(contract|meeting|crm)-app/main.js(?.*)?$) {
# 针对入口文件设置 expires_time -1,即expire是服务器时间的 -1s,始终过期
set $expires_time -1;
}
expires $expires_time;
…
}