共计 4538 个字符,预计需要花费 12 分钟才能阅读完成。
文章首发于我的博客 https://github.com/mcuking/bl…
我的项目地址:
preload-routes
async-routes
背景
对于大型前端我的项目,比方公司外部管理系统(个别包含 OA、HR、CRM、会议预约等零碎),如果将所有业务放在一个前端我的项目里,随着业务性能一直减少,就会导致如下这些问题:
- 代码规模宏大,导致编译工夫过长,开发、打包速度越来越慢
- 我的项目文件越来越多,导致查找相干文件变得越来越艰难
- 某一个业务的小改变,导致整个我的项目的打包和部署
技术计划
preload-routes 和 async-routes 是目前笔者所在团队应用的微前端计划,最终会将整个前端我的项目拆解成一个主我的项目和多个子项目,其中两者作用如下:
- 主我的项目:用于治理子项目的路由切换、注册子项目的路由和全局 Store 层、提供全局库和办法
- 子项目:用于开发子业务线业务代码,一个子项目对应一个子业务线,并且蕴含两端(PC + Mobile)代码和复用层代码(我的项目分层中的非视图层)
联合之前的分层架构实现复用非视图代码的形式,残缺的计划如下:
如图所示,将整个前端我的项目依照业务线拆分出多个子项目,每个子项目都是独立的仓库,只蕴含了单个业务线的代码,能够进行独立开发和部署,升高了我的项目保护的复杂度。
采纳这套计划,使得咱们的前端我的项目不仅保有了横向上(多个子项目)的扩展性,又领有了纵向上(单个子项目)的复用性。那么这套计划具体是怎么实现的呢?上面就具体阐明计划的实现机制。
在解说之前,首先明确下这套计划有两种实现形式,一种是预加载路由,另一种是懒加载路由,接下来就别离介绍这两种形式的实现机制。
预加载路由
preload-routes
1. 子项目 依照 vue-cli 3 的 library 模式进行打包,以便后续主我的项目援用
注:在 library 模式中,Vue 是外置的。这意味着包中不会有 Vue,即使你在代码中导入了 Vue。如果这个库会通过一个打包器应用,它将尝试通过打包器以依赖的形式加载 Vue;否则就会回退到一个全局的 Vue 变量。
2. 在编译主我的项目的时候,通过 InsertScriptPlugin 插件将子项目的入口文件 main.js 以 script 标签模式插入到主我的项目的 html 中
注:务必将子项目的入口文件 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);
}
);
});
3. 主我的项目的 html 要拜访子项目里的编译后的 js / css 等资源,须要进行 代理转发
- 如果是本地开发时,能够通过 webpack 提供的 proxy,例如:
const PROXY = {
'/app-a/': {target: 'http://localhost:10241/'}
};
- 如果是线上部署时,能够通过 nginx 转发或者将打包后的主我的项目和子项目放在一个文件夹中依照相对路径援用。
4. 当浏览器解析 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;
5. 持续向下解析 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-a'
}
])
});
到此就实现了单页面利用依照业务拆分成多个子项目,直白来说子项目的入口文件 main.js 就是将主我的项目和子项目分割起来的桥梁。
另外如果须要应用 vuex,则和 vue-router 的程序恰好相反(先主我的项目后子项目):
1. 首先在主我的项目的入口文件中初始化一个 store 实例 new Vuex.Store,而后挂在到 Vue.__share__.store 上
2. 而后在子项目的 App.vue 中获取到 Vue.__share__.store 并调用 store.registerModule(‘app-x’, store),将子项目的 store 作为子模块注册到 store 上
懒加载路由
async-routes
懒加载路由,顾名思义,就是说等到用户点击要进入子项目模块,通过解析行将跳转的路由确定是哪一个子项目,而后再异步去加载该子项目的入口文件 main.js(能够通过 systemjs 或者本人写一个动态创建 script 标签并插入 body 的办法)。加载胜利后就能够将子项目的路由动静增加到主我的项目总的路由里了。
1. 主我的项目 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;
}
});
2. 子项目的入口文件 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
局部问题解答
1. 如果子项目代码更新后,除了打包部署子项目之外,还须要打包部署主我的项目吗?
不须要更新部署主我的项目。这里有个 trick 上文遗记提及,就是子项目打包后的入口文件并没有加上 chunkhash,间接就是 main.js(子项目其余的 js 都有 chunkhash)。也就是说主我的项目只须要记住子项目的名字,就能够通过 subapp-name/main.js 找到子项目的入口文件,所以子项目打包部署后,主我的项目并不需要更新任何货色。
2. 针对第二个问题中子我的项目入口文件 main.js 不应用 chunkhash 的话,如何避免该文件始终被缓存呢?
能够在动态资源服务器端针对子项目入口文件设置强制缓存为不缓存,上面是服务器为 nginx 状况的相干配置:
location / {
set $expires_time 7d;
...
if ($request_uri ~* \/(contract|meeting|crm)-app\/main.js(\?.*)?$) {
# 针对入口文件设置 expires_time -1,即 expire 是服务器工夫的 -1s,始终过期
set $expires_time -1;
}
expires $expires_time;
...
}
结束语
如果没有在一个大型前端我的项目中应用多个技术栈的需要,还是很举荐笔者目前团队实际的这个计划的。另外如果是 React 技术栈,也是能够依照这种思维去实现相似的计划的。