在 toB 的前端开发工做中,咱们每每就会遇到以下困境:css
不一样的团队可能有不一样的方法去解决这些问题。在前端开发突飞猛进、前端工程化蓬勃发展的今天,我想给你们介绍下另外一种尝试——微前端。html
那什么是微前端?微前端主要是借鉴后端微服务的概念。简单地说,就是将一个巨无霸(Monolith)的前端工程拆分红一个一个的小工程。别小看这些小工程,它们也是“麻雀虽小,五脏俱全”,彻底具有独立的开发、运行能力。整个系统就将由这些小工程协同合做,实现全部页面的展现与交互。前端
能够跟微服务这么对比着去理解:vue
微服务 | 微前端 |
---|---|
一个微服务就是由一组接口构成,接口地址通常是 URL。当微服务收到一个接口的请求时,会进行路由找到相应的逻辑,输出响应内容。 | 一个微前端则是由一组页面构成,页面地址也是 URL。当微前端收到一个页面 URL 的请求时,会进行路由找到相应的组件,渲染页面内容。 |
后端微服务会有一个网关,做为单一入口接收全部的客户端接口请求,根据接口 URL 与服务的匹配关系,路由到对应的服务。 | 微前端则会有一个加载器,做为单一入口接收全部页面 URL 的访问,根据页面 URL 与微前端的匹配关系,选择加载对应的微前端,由该微前端进行进行路由响应 URL。 |
这里要注意跟 iframe 实现页面嵌入机制的区别。微前端没有用到 iframe,它很纯粹地利用 JavaScript、MVVM 等技术来实现页面加载。后面咱们将介绍相关的技术实现。webpack
在介绍具体的改造方式以前,我想跟你们先说明下咱们当时面临的问题,以及改造后的对比,以便你们以此为对照,评判或决定使用。主要包括打包速度、页面加载速度、多人多地协做、SaaS 产品定制化、产品拆分这几个角度。web
首先是打包速度。在 6 个月前,咱们的 B 端工程那会儿仍是一个 Monolith。当时已经有 20 多个依赖、60 多个公共组件、200 多个页面,对接 700 多个接口。咱们使用了 Webpack 2,并启用 DLL Plugin、HappyPack 4。在个人我的主机上使用 4 线程编译,大概要 5 分钟。而若是不拆分,算下来如今咱们已经有近 400 个页面,对接1000 多个接口。
这个时间意味着什么?它不只会耽误咱们开发人员的时间,还会影响整个团队的效率。上线时,在 Docker、CI 等环境下,耗时还会被延长。若是部署后出几个 Bug,要线上当即修复,那就不知道要熬到几点了。
在使用微前端改造后,目前咱们已经有 26 个微前端工程,平均打包时间在 30-45 秒之间(注意,这里尚未应用 DLL + HappyPack)。vue-router
页面加载速度其实影响到并非很大,由于通过 CDN、gzip 后,资源的大小还能接受。这里只是给你们看一些直观的数据变化。6 个月前,打包生成的 app.js 有 5MB(gzip 后 1MB),vendor.js 有 2MB(gzip 后 700KB),app.css 有 1.5MB(gzip 后 250KB)。这样首屏大概要传输 2MB 的内容。拆分后,目前首屏只须要传输 800KB 左右。bootstrap
在协做上,咱们在全国有三个地方的前端团队,这么多人在同一个工程里开发,遭遇代码冲突的几率会很频繁,并且冲突的影响面比较大。若是代码中出现问题,致使 CI 失败,全部其余人的代码提交与更新也都会被阻塞。使用微前端后,这样的风险就平摊到各个工程上去了。windows
再者就是定制化了。咱们作的额是一款 toB 的产品,作成 SaaS 标准版产品大概是全部从业者的愿望。但总体市场环境与产品功能所限,常常要面临一些客户要求作本地化与定制化的要求。本地化就会有代码安全方面的考量,最好是不给客户源代码,最差则是只给客户购买功能的源代码。而定制化从易到难则能够分为独立新模块、改造现有模块、替换现有模块。
经过微前端技术,咱们能够很容易达到本地化代码安全的下限——只给客户他所购买的模块的前端源码。定制化里最简单的独立新模块也变得简单:交付团队增长一个新的微前端工程便可,不须要揉进现有研发工程中,不占用研发团队资源。而定制化中的改造现有模块也能够比较好地实现:好比说某个标准版的页面中须要增长一个面板,则能够经过一个新的微前端工程,一样响应该页面的 URL(固然要控制好顺序),在页面的恰当位置插入一个新的 DOM 节点便可。后端
最后就是产品拆分方面的考量了。咱们的产品比较大,有几块功能比较独立、有特点。若是说未来须要独立成一个子产品,有微前端拆分做为铺垫,腾挪组合也会变得更加容易些。
有了以上的一些缘由与诉求,在决定进行微前端改造前,还须要设定一些额外的小目标:
“Talk is cheap,show me the code“。下面就让咱们一块儿来看看具体的改造吧!咱们的微前端工程能够划分为 portal 工程、业务工程、common 工程这几类。
portal,顾名思义,就是入口。这也就是上面所说的微前端加载器。当用户打开浏览器,首次进入咱们的页面时,不论是什么 URL,首先加载的就是 portal。portal 里会配置全部业务工程的地址、匹配哪些 URL、须要加载哪些资源。如:
// 业务工程的名称 customer: { // URL 匹配模式 matchUrlHash: ['^/customer'], // 微前端地址 target: 'http://localhost:8101/mfe-customer/index.html', // 资源匹配模式 resourcePatterns: ['/app.*.css$', '/vendor.*.css$', '/manifest.*.js$', '/vendor.*.js$', '/app.*.js$'], } 复制代码
portal 会定时、异步、并发地下载业务工程的资源,并将它们进行注册,此时并不会加载这些业务工程。这里之因此要业务工程的地址(target)、资源(resourcePatterns),是为了加载时肯定地知道其所包含的 app.js、vendor.js、app.css 等资源的路径。由于业务工程每次有变动,app.js 等资源路径上都会带有新的文件内容哈希值(Hash),致使路径不可预测。而它的 index.html 的路径是固定的。咱们读取该 HTML,解析其内容,经过正则就能匹配到 app.js 等资源的路径。
portal 在运行时,会监听 URL 变化。目前咱们只支持 URL Hash(如 #/customer)。当 Hash 发生变动时,匹配到业务工程,而后执行卸载、加载的工做。这个机制主要是利用 single-spa
来实现,但原理就是这么简单。
import { registerApplication } from 'single-spa'; registerApplication('customer', // 下载微前端工程,获取三个函数钩子:bootstrap、mount、unmount () => { const html = fetch(mfeConfig.target); const {cssUrls, jsUrls} = match(html, mfeConfig.resourcePatterns); loadCss(cssUrls); loadJs(jsUrls); return windows['mfe:customer']; }, // 对当前浏览器 URL Hash 进行匹配,若是匹配(返回 true),则加载该微前端(调用 mount);不然卸载(调用 unmount) () => { return match(window.location.hash, mfeConfig.matchUrlHash); }, mfeConfig.customProps ); 复制代码
业务工程就是普通的微前端工程,通常一个模块一个工程。业务工程要扮演两个角色,一个是可独立运行的前端工程,一个是受 portal 控制的运行时。前者主要用于咱们本地开发,后者则是线上集成时使用。在独立运行时,它跟原来的前端工程没有什么区别。以 Vue 工程为例,照样使用 new Vue({el: '#app'})
来启动、渲染页面。
new Vue({ el: '#app', i18n, router, store, template: '<App/>', components: { App } }); 复制代码
而当受控运行时,则是利用 UMD 方式输出几个钩子函数,包括初始化、加载、卸载。
if(!window.IS_IN_MFE){ // 独立运行时 new Vue({...}) } else { // 受控运行时 module.exports = { bootstrap(){ // 注册时执行 }, mount(customProps){ // 加载时执行 return Promise.resolve().then(()=>{ instance = new Vue({...}) }) }, unmount(){ // 卸载时执行 return Promise.resolve().then(()=>{ instance.$destroy() }) } } } 复制代码
线上环境的 Webpack 配置:
output: { libraryTarget: "umd", library: 'mfe:customer' } 复制代码
而区分是否受控,则能够经过判断一个全局变量来实现。如 window.IS_IN_MFE
,portal 工程在运行时会将其设置为 true
。
为了支持本地多个工程同时开发,咱们须要为每一个微前端工程指定一个肯定的、独占的端口号。好比从 8100 开始,逐一递增。同时,为了支持线上部署,咱们还须要给每一个微前端工程指定一个肯定的、独占的基础路径(前缀)。这样相同域名下能够用不一样路径进行独立访问。路径统一以 /mfe-
开头,如 /mfe-customer
。这也就是上面 portal 里业务工程的配置示例里所展示的那样。
咱们产品的页面结构分为顶部栏、侧边栏、中间内容区三大块。顶部栏和侧边栏在页面跳转过程当中,基本上保持不变。因此咱们也将它们剥离出来做为一个独立的微前端业务工程,叫作 mfe-navs
。它会匹配全部的 URL,也就是说访问任意 URL 时,都会加载它,并且还要保证先加载它。当它加载完毕后,会在页面内提供一个中间内容区的锚点 DOM(#app
:),供其余业务工程加载时挂载。
上面能够看到,每个业务工程都是一个独立的前端工程,因此里面会有一些相同的依赖,如 Vue、moment、lodash 等。若是将这些内容都打包到各自的 vendor.js 里,则势必会致使代码冗余太多,浏览器运行内存压力增大。咱们把这些公共依赖、公共组件、CSS、Fonts 等都放到一个工程里,由该工程进行打包,将依赖、组件 export,并以 UMD 的方式注入到全局。
main.js:
import Vue from 'vue'; // 公共依赖 import VueRouter from 'vue-router'; import VueI18n from 'vue-i18n'; import '@/css/icon-font/iconfont.css'; import ContentSelector from '@/components/ContentSelector'; // 公共组件 Vue.use(VueI18n); // 你们都要这么作,咱们就代劳吧! module.exports = { 'vue': Vue, 'vue-router': VueRouter, 'content-selector': ContentSelector, }; 复制代码
Webpack 配置:
output: { libraryTarget: "umd", library: 'mfe:common' } 复制代码
业务工程则经过 Webpack 外部依赖(external)的方式引入到工程中。这样业务工程打包时就不会包含这些公共代码了。
var externalModules = ['vue', 'vue-router', 'content-selector']; module.exports = { // webpack 配置项 // ... externals: (context, request, callback)=>{ if(externalModules.includes(request)){ callback(null, 'root window["mfe:common"]["'+request+'"]') } else { callback(); } }, } 复制代码
以上就是咱们微前端改造与实践方面的一些经验。前路漫漫,这里面还存在不少待完善的地方,如 History 模式支持、i18n 更好地集成、各个业务工程的加载顺序优化及个性化等。除了这些纯粹技术上的探索,在拥有微前端、微服务这些架构的基础上,团队也能够考虑进行垂直拆分:一个小组独立负责一块业务,它有本身的微前端工程和微服务工程。从技术管理到人员管理,将它们糅合在一块儿统一考虑,这也是咱们软件工程的探索方向。期待这些可以对你们带来一些思考和帮助!