文章首发于个人博客 https://github.com/mcuking/bl...
项目地址:javascript
async-routeshtml
对于大型前端项目,好比公司内部管理系统(通常包括 OA、HR、CRM、会议预定等系统),若是将全部业务放在一个前端项目里,随着业务功能不断增长,就会致使以下这些问题:前端
preload-routes 和 async-routes 是目前笔者所在团队使用的微前端方案,最终会将整个前端项目拆解成一个主项目和多个子项目,其中二者做用以下:vue
结合以前的分层架构实现复用非视图代码的方式,完整的方案以下:java
如图所示,将整个前端项目按照业务线拆分出多个子项目,每一个子项目都是独立的仓库,只包含了单个业务线的代码,能够进行独立开发和部署,下降了项目维护的复杂度。webpack
采用这套方案,使得咱们的前端项目不只保有了横向上(多个子项目)的扩展性,又拥有了纵向上(单个子项目)的复用性。那么这套方案具体是怎么实现的呢?下面就详细说明方案的实现机制。nginx
在讲解以前,首先明确下这套方案有两种实现方式,一种是预加载路由,另外一种是懒加载路由,接下来就分别介绍这两种方式的实现机制。git
preload-routesgithub
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 等资源,须要进行代理转发
const PROXY = { '/app-a/': { target: 'http://localhost:10241/' } };
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 上
懒加载路由,顾名思义,就是说等到用户点击要进入子项目模块,经过解析即将跳转的路由肯定是哪个子项目,而后再异步去加载该子项目的入口文件 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 注册等和上面的预加载路由彻底一致。
下面谈下这套方案的优缺点:
优势
缺点:
不须要更新部署主项目。这里有个 trick 上文忘记说起,就是子项目打包后的入口文件并无加上 chunkhash,直接就是 main.js(子项目其余的 js 都有 chunkhash)。也就是说主项目只须要记住子项目的名字,就能够经过 subapp-name/main.js 找到子项目的入口文件,因此子项目打包部署后,主项目并不须要更新任何东西。
能够在静态资源服务器端针对子项目入口文件设置强制缓存为不缓存,下面是服务器为 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 技术栈,也是能够按照这种思想去实现相似的方案的。