个推前端微服务化:突破传统SPA瓶颈
本文作者:个推高级前端开发工程师 沈创
目前的前端领域,单页面应用(SPA)大行其道。而随着时间的推移以及应用功能的丰富,这些应用变得越来越庞大也越来越难以维护。于是“微前端”这一概念应运而生。
“微前端”出自2016 年的 ThoughtWorks 技术雷达,指将项目拆分成一个个可独立运行、独立开发、独立部署的前端微应用,这些微应用可以并行开发、共享组件。
而微前端的实现方式也分很多种:服务器路由重定向、组合多个独立应用、iFrame、通过Web Components构建等。
微前端的相关概念也在个推前端中的部分项目(基于Vue框架)中得到应用。
为什么要进行前端微服务化
之所以强调“部分项目”,是因为任何一种技术或者概念都有其适用场景,微前端也不例外。针对中小型的项目,使用微前端反而会将事情复杂化,因为微前端对项目的开发并不友好。
以个推的业务场景为例:
在A项目线中有10-20个模块,每一个模块中有5-15个不等的页面。而A项目线中所有的产品都是基于这些模块来自由组合的,也就是说:如果按照普通的SPA开发路线,我们可能需要很多分支或者repo来维护这些产品,因为每个产品所需的模块版本会有细微区别。
演变的过程
为了不出现分支混乱、项目庞大、代码冲突、打包麻烦等一系列的问题,借着后端微服务拆分的机会,我们开始对A项目线前端开发和部署方式进行了调整。
最初,我们并没有使用前端微服务的开发和部署方式,而是先把项目中的各个模块拆分成了许多独立的repo,避免团队内的工程师在开发的过程中出现需要pull代码并解决冲突的情况(一个模块一个迭代一般由1-2人完成)。
因此,我们的问题是:模块拆分后,如何解决开发、打包部署,以及项目中的公共依赖和组件复用的问题。
拆分后的模块项目目录结构大致如下:
项目中的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的实践
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工具也很好地解决了模块单独开发运行的问题。
当然,我们的微服务化方案也存在局限。它比较适合模块之间联系比较紧密的大型项目,且没有微前端概念中强调的技术无关性以及团队代码隔离性。
在不久的将来,除了微服务化方案的继续升级,我们还会接入新的框架,迎接新的挑战。