目前的前端领域,单页面应用(SPA)大行其道。而随着时间的推移以及应用功能的丰富,这些应用变得愈来愈庞大也愈来愈难以维护。因而“微前端”这一律念应运而生。css
“微前端”出自2016 年的 ThoughtWorks 技术雷达,指将项目拆分红一个个可独立运行、独立开发、独立部署的前端微应用,这些微应用能够并行开发、共享组件。前端
而微前端的实现方式也分不少种:服务器路由重定向、组合多个独立应用、iFrame、经过Web Components构建等。vue
微前端的相关概念也在个推前端中的部分项目(基于Vue框架)中获得应用。node
之因此强调“部分项目”,是由于任何一种技术或者概念都有其适用场景,微前端也不例外。针对中小型的项目,使用微前端反而会将事情复杂化,由于微前端对项目的开发并不友好。webpack
以个推的业务场景为例:web
在A项目线中有10-20个模块,每个模块中有5-15个不等的页面。而A项目线中全部的产品都是基于这些模块来自由组合的,也就是说:若是按照普通的SPA开发路线,咱们可能须要不少分支或者repo来维护这些产品,由于每一个产品所需的模块版本会有细微区别。vue-router
为了避免出现分支混乱、项目庞大、代码冲突、打包麻烦等一系列的问题,借着后端微服务拆分的机会,咱们开始对A项目线前端开发和部署方式进行了调整。vuex
最初,咱们并无使用前端微服务的开发和部署方式,而是先把项目中的各个模块拆分红了许多独立的repo,避免团队内的工程师在开发的过程当中出现须要pull代码并解决冲突的状况(一个模块一个迭代通常由1-2人完成)。vue-cli
所以,咱们的问题是:模块拆分后,如何解决开发、打包部署,以及项目中的公共依赖和组件复用的问题。npm
拆分后的模块项目目录结构大体以下:
项目中的main.js入口和公共组件被抽离成了一个单独的项目,这里称为main项目。
因为各个子模块项目中仅有当前模块的页面代码和路由、菜单配置,因此dev子模块没法被直接开发。因而咱们开发了一个名为lego的CLI工具。开发模块时,开发人员只须要在模块根目录运行“lego dev”命令便可启动一个当前模块的开发服务,开发好的模块都会被发布到咱们本身的npm源进行版本的管理。
若是仅仅是对模块进行拆分,那么开发人员单独对模块进行开发时,须要给模块配置对应的运行环境,而且模块与模块之间的相互调用也很麻烦。而“lego”CLI解决了模块运行环境的问题,运行环境由CLI自动加载,模块开发人员只须要关注模块自身的业务逻辑便可。
此外,模块还提供了一个config.js文件,能够从npm源配置其余依赖模块,帮助开发人员在开发时更便捷地调用不一样模块。使用“lego dev”命令还支持“@self/”路径引入,“@self/”路径指向当前模块的src/文件夹,而“@/”指向main项目的src/文件夹,从而避免了模块开发时import路径的问题。
经过模块的拆分改造,解决了项目庞大、分支混乱的问题,代码冲突的状况也显著减小。可是对于单个产品的打包部署,咱们仍然须要从各个模块获取源码,并经过main项目打包成一个独立的产品。即便只修改了某一个模块的一行代码,整个系统也须要从新打包,打包后的整个产品也须要进行回归测试。
针对这一问题,咱们思考是否能够直接把模块打包成应用以供调用。
咱们的理想状况是:各个模块能够独立开发和部署,而后由产品自身决定加载的模块。
效果以下:
所以咱们须要在模块打包以后,入口(index.js)能够按照须要被注入到main项目中,而且被main项目加载(路由)。
一方面,使用webpack进行打包的项目,代码是基于CommonJS规范的。因为umd规范兼容于CommonJS规范,这使得开发人员能够直接在项目中使用基于umd规范打包后的模块。
另外一方面,vue-router和vuex库,都支持动态加载addRouter/registerModule的API。
咱们采用过两种方案:
第一种:main项目在Vue实例初始化时,将vue-router和vuex的实例暴露到全局(window),将子模块的路由前缀存储在项目中的路由表。当页面跳转到匹配的子模块的路由时,main项目加载子模块umd.js文件并动态注册router和vuex module,进而渲染页面。
简单DEMO以下图所示:
第二种:子模块umd.js文件先加载,向全局(window)暴露该子模块的路由和vuex信息。Vue实例从window获取路由信息和vuex module、菜单信息等,造成一个独立的产品。
简单DEMO以下图所示:
固然,两种方案都存在必定的缺点:
第一种方案:首先,子模块js文件是在页面跳转以后再进行加载,所以,在404跳转和路由权限校验的实现上会遇到一些问题;其次,在子模块文件加载完成以前以及子模块渲染以前都存在较长的页面白屏时间。
第二种方案:不管子模块用户是否会访问到umd入口文件,该文件都须要事先加载。这就要求入口文件须要足够小,意味着子模块没法使用min-chunk-size-plugin插件来对chunk进行合并,须要开发人员采用手写webpackChunkName或者使用其余工具进行合并。
Vue-cli3.x对子模块的打包提供了比较好的支持,使用"vue-cli-service build - target=lib"便可将子模块代码打包成umd规范格式。
可是,须要注意如下几个问题:
“--target=lib”的初衷是给发布到npmjs的组件使用,因此打包出的文件是不带hash值的(即便在vue.config.js中配置了chunkName)。咱们采起的办法是在执行lego脚手架的打包命令前,修改vue-cli-service源码。
使用“--target=lib”打包子模块时,若是没有配置css-in-js,打包出的css文件中的background-image路径有问题。基于此,咱们给出两个解决办法:配置css-in-js,或者修改node_modules中vue-cli-service源码再打包。
以上即是个推前端微服务化的开发及部署的实践状况。
在实践中咱们发现,微服务化的接入,很好地解决了项目中遇到的维护难、产品编译部署麻烦等问题。在模块化拆分时,咱们开发的CLI工具也很好地解决了模块单独开发运行的问题。
固然,咱们的微服务化方案也存在局限。它比较适合模块之间联系比较紧密的大型项目,且没有微前端概念中强调的技术无关性以及团队代码隔离性。
在不久的未来,除了微服务化方案的继续升级,咱们还会接入新的框架,迎接新的挑战。