文章首发于本人博客https://github.com/mcuking/bl...项目地址:preload-routesasync-routes背景为大-前端项目规模化,比如公司内部管理系统(一般包括OA、HR、CRM、会议预订系统等),如果所有业务都放在一个前端项目中,随着业务功能的不断增加,会导致以下问题:代码量巨大,导致编译时间过长,开发打包速度越来越慢。工程文件越来越多,查找相关文件越来越难。某个业务的一个小改动,会导致整个项目的打包部署。Preload-routes和async-routes是目前笔者团队使用的微前端方案,最终会将整个前端项目拆解成一个主项目和多个子项目,其功能如下:mainproject:用于管理子项目的路由切换,注册子项目的路由和全局Store层,提供全局库和方法subprojects:用于开发子线业务代码,一个子项目对应到一个子业务线,包括两端(PC+Mobile)代码和复用层代码(项目分层在非视图层)结合前面的分层架构实现非视图代码的复用,完整解决方案如下:如图所示,将整个前端项目按照业务线拆分成多个子项目,每个子项目都是独立的仓库,只包含单个业务线的代码,这可以独立ntly开发和部署,降低项目维护的复杂性。通过这个方案,我们的前端项目既保持了水平方向(多个子项目)的可扩展性,又具备了垂直方向(单个子项目)的复用性。那么这个计划如何实现呢?下面将详细描述该方案的实现机制。在解释之前,首先明确一下这个方案有两种实现方式,一种是预加载路由,一种是懒加载路由。下面分别介绍这两种方法的实现机制。预加载路由preload-routes1。子项目按照vue-cli3的库方式打包,方便后面引用主项目。注意:在库模式下,Vue是外部的。这意味着包中不会有Vue,即使您在代码中导入Vue。如果库将通过捆绑器使用,它将尝试通过捆绑器将Vue作为依赖项加载;否则它将退回到全局Vue变量。2、编译主工程时,通过InsertScriptPlugin插件将子工程的入口文件main.js以script标签的形式插入到主工程的html中。注意:一定要把子项目入口文件main.js对应的script标签放在主项目入口文件app.js的script标签上面,这是为了保证子项目入口文件代码是在主工程的入口文件代码之前执行的,后面的步骤就明白为什么要这样做了。再注意:本地开发环境中项目的入口文件编译出来的main.js存放在内存中,所以在磁盘上是不可见的,但是可以访问。InsertScriptPlugin的核心代码如下:js}}=htmlPluginData;//将传入的js以脚本标签的形式插入到html中//注意:需要将子项目的入口文件main.js放在主项目的入口文件app.js之前,因为子项目需要将其注册路由列表添加到全局js.unshift(...self.files);});});3.如果主项目的html要访问子项目编译好的js/css等资源,需要代理转发如果在本地开发,可以使用webpack提供的代理,例如:constPROXY={'/app-a/':{目标:'http://localhost:10241/'}};如果是在线部署,可以通过nginx转发或者将打包后的主工程和子工程放在一个文件夹中,按照相对路径进行引用。4.浏览器解析html时,解析并执行子项目的入口文件main.js,并将子项目的路由列表注册到Vue.__share__.routes中,以便后续主项目合并它进入整体路线。子项目main.js代码如下:(为了尽量减少主项目页面第一次渲染时加载的资源,子项目的入口文件建议只路由挂载)importVue来自'vue';从'./routes'导入路线;constshare=(Vue.__share__=Vue.__share__||{});constroutesPool=(share.routes=share.routes||{});//将子项目的路由列表挂载到Vue.__share__.routes中,以便后续的主项目将其合并到总路由中。routesPool[process.env.VUE_APP_NAME]=路由;5.继续往下解析html,在解析执行主工程main.js的时候,从Vue.__share__.routes中获取所有子工程的路由列表,合并到总路由表中,然后初始化一个vue-路由器实例,并将其传递给新的Vue。相关关键代码如下//从Vue.__share__.routes中获取所有子项目的路由列表,合并到总路由表中constroutes=Vue.__share__.routes;exportdefaultnewRouter({routes:Object.values(routes).reduce((acc,prev)=>acc.concat(prev),[{path:'/',redirect:'/app-a'}])});至此,单页应用根据业务拆分成多个子项目。说白了,子项目的入口文件main.js是连接主项目和子项目的一座桥梁。另外,如果需要使用vuex,vue-router的顺序正好相反(先主项目,后子项目):1.首先在主的入口文件中初始化一个store实例newVuex.Store项目,然后挂在Vue.__share__.store上2.然后在子项目的App.vue中获取Vue.__share__.store调用store.registerModule('app-x',store),将子项目的store注册为asubmoduleonthestoreforlazyloadingRoutingasync-routes懒加载路由,顾名思义就是等到用户点击进入子项目模块,通过解析即将到的路由来判断是哪个子项目跳转,然后异步加载子项目的入口文件main.js(可以用systemjs或者写一个方法动态创建script标签插入到body中)。加载成功后,可以将子项目的路由动态添加到主项目的总路由中。1.主工程的router.js文件定义了vue-router中的beforeEach钩子来拦截路由,根据要跳转的路由分析需要哪个子工程,然后加载对应的子工程入口文件异步地。以下是核心代码:constcachedModules=newSet();router.beforeEach(async(to,from,next)=>{const[,module]=to.path.split('/');if(Reflect.has(modules,module)){//如果对应子-project已经加载完毕,不需要重复加载,直接跳转即可(application&&application.routes){//动态添加子项的路由列表router.addRoutes(application.routes);}cachedModules.add(module);next(to.path);}else{next();}返回;}});2、子项目的入口文件main.js只需要将子项目的路由暴露给主项目即可。代码如下:importroutesfrom'./routes';exportdefault{name:'javascript',routes,beforeEach(from,to,next){console.log('javascript:',from.path,to.path);下一个();},}注意:除了暴露routes方法外,还暴露了beforeEach方法。其实就是通过路由守卫来支持子项目的页面权限限制。主项目获取这个子项目的beforeEach,可以在vue-router的beforeEach钩子中执行。具体代码参考async-routes,只是主项目和子项目的交互不同,代理转发子项目资源,vuexstore注册等与上述预加载路由完全一样。优缺点说说这个方案的优缺点:优点子项目可以单独打包部署,提高了开发打包的速度。子项目的开发相互独立,互不影响。可以在不同的仓库维护,减少了单个项目的规模,维护了单页应用的体验,子项目之间的切换不刷新。改造成本低,对现有项目侵入性低,业务线迁移成本低,保证了整个项目拥有统一的技术栈。缺点:主项目需要和子项目共享一个Vue实例,所以不可能单独为一个子项目使用最新版本的Vue(比如Vue3)或者React。一些问题的解答1.如果更新子项目的代码,除了打包部署子项目外,还需要打包部署主项目?无需更新部署主项目。这里有一个我上面忘记说的trick,就是子项目包的入口文件不加chunkhash,直接是main.js(子项目其他js都有chunkhash)。也就是说主工程只需要记住子工程的名字,通过subapp-name/main.js就可以找到子工程的入口文件,所以子工程打包部署后,主工程就做不需要更新任何东西。2、对于第二个问题,如果子项目入口文件main.js没有使用chunkhash,如何防止文件一直被缓存?对于子项目入口文件,静态资源服务器端可以设置强制缓存为不缓存。以下是服务器为nginx情况下的相关配置:location/{set$expires_time7d;...if($request_uri~*\/(contract|meeting|crm)-app\/main.js(\?.*)?$){#为入口文件设置expires_time-1,即expire为服务器时间-1s,总是过期set$expires_time-1;}到期$expires_time;...}结论如果在大型前端项目中不需要使用多个技术栈,我还是推荐作者目前团队实践的这种方案。另外,如果是React技术栈,也可以按照这个思路实现类似的方案。
