写点什么

微前端实践

2020 年 1 月 17 日

微前端实践

2018 年底 2019 年初,我在国双内部前端技术沙龙上分享了《微前端实践》。一年过去,微前端在前端技术圈逐步被更多人所认识和接受。回顾自分享以后这一年来的继续探索与实践,可以说收获满满。它启发并奠定了微应用架构的基础,助力实现分布式、共生式、插件式的多租户、多应用的 Web 开发平台(https://developer.engage-all.com/ ),为我司后续各产品线的融合、客户定制化乃至混合部署(SaaS + 本地化)打下了坚实的技术基础。在此,略作修改并通过 InfoQ 平台再次分享此文章,期待能给更多人带来一点思考和帮助!


在 ToB 业务的前端开发工作中,我们往往会遇到如下问题:


  1. 功能越来越多,工程越来越大,打包越来越慢

  2. 团队人员众多,代码冲突频繁,改动影响难测

  3. 内心想做 SaaS 标准产品,客户却要本地化、定制化,代码管理走向混乱的边缘

  4. 客户并不想要所有功能,无法很好地拆分售卖

  5. 不同的团队可能有不同的方式去解决以上问题。在国双,结合最新的前端技术以及自己的实践,我们进行了一种新的尝试——微前端。


什么是微前端

那什么是微前端?微前端主要是借鉴后端微服务的概念。简单地说,就是将一个巨无霸(Monolith)的前端工程拆分成一个一个的小工程。别小看这些小工程,它们也是“麻雀虽小,五脏俱全”,完全具备独立的开发、运行能力。整个系统就将由这些小工程协同合作,实现所有页面、控件的展示与交互。


对比微服务,我们可以这么去看:


微服务 微前端


一个微服务就是由一组接口构成,接口地址一般是 URL。当微服务收到一个接口的请求时,会进行路由找到相应的方法,输出响应内容。 一个微前端则是由一组页面构成,页面地址也是 URL。当微前端收到一个页面 URL 的请求时,会进行路由找到相应的组件,渲染页面内容。


后端微服务会有一个网关,作为单一入口接收所有的客户端接口请求,根据接口 URL 与服务的匹配关系,路由到对应的服务。 微前端则会有一个加载器,作为单一入口接收所有页面 URL 的访问,根据页面 URL 与微前端的匹配关系,选择加载对应的微前端,由该微前端进行进行路由响应 URL。


这里要注意跟 iframe 实现页面嵌入机制的区别。微前端没有用到 iframe,它很纯粹地利用 JavaScript、MVVM 等技术来实现页面加载。后面我们将介绍相关的技术实现。


为什么要用微前端

在介绍具体的改造方式之前,我想跟大家先说明下我们当时面临的具体问题,以及改造后的效果对比。这里主要从打包速度、页面加载速度、多人多地协作、SaaS 产品定制化、产品拆分这几个角度来说一下。


首先是打包速度。在 6 个月前(注:2018 年 6 月份),我们的 B 端那会儿还是一个工程。当时已经有 20 多个依赖、60 多个公共组件、200 多个页面,对接 700 多个接口。我们使用了 Webpack 2,并启用 DLL Plugin、HappyPack 4。在我的个人 Mac 主机上使用 4 线程编译,大概要 5 分钟。而如果不拆分,算下来现在(注:2018 年 12 月份)我们已经有近 400 个页面,对接 1000 多个接口。


这个时间意味着什么?它不仅会耽误我们开发人员的时间,还会影响整个团队的效率。上线时,在 Docker、CI 等环境下,耗时还会被延长。如果部署后出几个 Bug,要线上立即修复,那就不知道要熬到几点了。


在使用微前端改造后,目前我们已经有 26 个微前端工程,平均打包时间在 30-45 秒之间(注意,这里还没有应用 DLL + HappyPack)。


页面加载速度其实影响倒并不是很大,因为经过 CDN、gzip 后,资源的大小还能接受。这里只是给大家看一些直观的数据变化。6 个月前,打包生成的 app.js 有 5MB(gzip 后 1MB),vendor.js 有 2MB(gzip 后 700KB),app.css 有 1.5MB(gzip 后 250KB)。这样首屏大概要传输 2MB 的内容。拆分后,目前首屏只需要传输 800KB 左右。


在协作上,我们在全国有三个地方的前端团队,这么多人在同一个工程里开发,遭遇代码冲突的概率会很频繁,而且冲突的影响面比较大。如果代码中出现问题,导致 CI 失败,所有其他人的代码提交与更新也都会被阻塞。使用微前端后,这样的风险就平摊到各个工程上去了。


再者就是定制化了。我们做的是一款 ToB 的产品,做成 SaaS 标准版产品大概是所有从业者的愿望。但整体市场环境与产品功能所限,经常要面临一些客户要求做本地化与定制化的要求。本地化就会有代码安全方面的考虑,最好是不给客户源代码,最差则是只给客户所购买功能的源代码。而定制化从易到难则可以分为独立新模块、改造现有模块、替换现有模块。


通过微前端技术,我们可以很容易达到本地化代码安全的下限——只给客户他所购买的模块的前端源码。定制化里最简单的独立新模块也变得简单:交付团队增加一个新的微前端工程即可,不需要揉进现有研发工程中,不占用研发团队资源。而定制化中的改造现有模块也可以比较好地实现:比如说某个标准版的页面中需要增加一个面板,则可以通过一个新的微前端工程,同样响应该页面的 URL(当然要控制好顺序),在页面的恰当位置插入一个新的 DOM 节点即可。


最后就是产品拆分方面的考虑了。我们的产品比较大,有几块功能比较独立、有特色。如果说将来需要独立成一个子产品,有微前端拆分作为铺垫,腾挪组合也会变得更加容易些。


其他目标

有了以上的一些原因与诉求,在决定进行微前端改造前,还需要设定一些额外的小目标:


• 不能对现有的前端开发方式带来太大变化,至少要有平滑过渡的机制。


• 每个为前端工程都要求可以独立运行,至少在本地开发时要能做到。


• 微前端在加载时,要实现预加载,并可以自由调整预加载顺序,甚至是根据用户的偏好来实现智能化、个性化的加载顺序。


如何改造现有工程

“Talk is cheap,show me the code“。下面就让我们一起来看看具体的改造吧!我们的微前端工程可以划分为 portal 工程、业务工程、common 工程这几类。


portal 工程


portal,顾名思义,就是入口。这也就是上面所说的微前端加载器。当用户打开浏览器,首次进入我们的页面时,不管是什么 URL,首先加载的就是 portal。portal 里会配置所有业务工程的地址、匹配哪些 URL、需要加载哪些资源。如:


{    // 业务工程的名称    order: {        // URL Hash 匹配模式(正则)        matchUrlHash: ['^/order'],        // 微前端 index.html 的地址,用于获取所有资源(JS、CSS)的路径        indexHtml: 'http://localhost:8101/mfe-order/index.html',        // 资源匹配模式(正则)        resourcePatterns: ['/app.*.css$', '/vendor.*.css$', '/manifest.*.js$', '/vendor.*.js$', '/app.*.js$'],    },    // ....}
复制代码


portal 会定时、异步、并发地下载业务工程的资源,并将它们进行注册,此时并不会加载这些业务工程。这里之所以要业务工程微前端 index.html 的地址、资源(resourcePatterns),是为了加载时确定地知道其所包含的 app.js、vendor.js、app.css 等资源的路径。因为业务工程每次有变更,app.js 等资源路径上都会带有新的文件内容哈希值(Hash)(如 app.436e74094d4d555b1c81.js),导致路径不可预测。而它的 index.html 的路径是固定的。我们读取该 HTML,解析其内容,通过正则就能匹配到 app.js 等资源的路径。


portal 在运行时,会监听 URL 变化。目前我们只支持 URL Hash(如 #/order)(注:后来我们自己重写了底层,现在能支持 History 模式了,但是整体原理还是一致的)。当 Hash 发生变更时,匹配到业务工程,然后执行业务工程的卸载、加载。这个机制主要是利用 single-spa 来实现,但原理就是这么简单。


import { registerApplication } from 'single-spa';registerApplication('order',     // 下载微前端工程,获取三个函数钩子:bootstrap、mount、unmount    async () => {        const html = await fetch(mfeConfig.target);        const {cssUrls, jsUrls} = matchResources(html, mfeConfig.resourcePatterns);        await loadCss(cssUrls); // 动态创建 link 标签        await loadJs(jsUrls); // 动态创建 script 标签        return windows['mfe:order']; // 包含 bootstrap、mount、unmount 三个函数,见下方 module.exports 与 webpack 配置    },    // 对当前浏览器 URL Hash 进行匹配,如果匹配(返回 true),则加载该微前端(调用 mount);否则卸载(调用 unmount)    () => {        return matchUrl(window.location.hash, mfeConfig.matchUrlHash);    },    mfeConfig.customProps);
复制代码


在初次分享时,又拆分了 navs、common 工程。在后来我们的实践中,把这三个工程合在了一起,这样尽最大地优化开发体验,降低维护难度。


一般产品的页面结构分为导航栏、内容区两大块。导航栏可能包括顶部栏、侧边栏或者两者都有。在页面跳转过程中,导航部分基本上保持不变(不用全局重新渲染)。所以可以将它们也集成到 portal 工程中。这个时候,要注意调整内容区的锚点 DOM(#app)的位置,它将会用来挂载(mount)所有的业务工程(见下方描述)。


公共依赖、公共组件的处理见下方内容。


业务工程

业务工程就是普通的微前端工程,一般一个模块一个工程。以 Vue 工程为例,在微前端改造之前,我们使用 new Vue({el: ‘#app’}) 来启动、渲染页面。


new Vue({    el: '#app',    i18n,    router,    store,    template: '<App/>',    components: { App }});
复制代码


而当以微前端的方式集成时,则是利用 UMD 方式输出几个钩子函数,即初始化、加载、卸载。


var instance = null;module.exports = {    bootstrap(){ // 注册时执行    },    mount(customProps){ // 加载时执行        return Promise.resolve().then(()=>{            instance = new Vue({...}); // new Vue 在这里了,参数还是一样的        })    },    unmount(){ // 卸载时执行        return Promise.resolve().then(()=>{            instance.$destroy()        })    }}Webpack 配置:{    output: {        libraryTarget: "umd",        library: 'mfe:order'    }}
复制代码


为了支持本地多个工程同时开发,我们需要为每个微前端工程指定一个确定的、独占的端口号。比如从 8100 开始,逐一递增。同时,为了支持线上部署,我们还需要给每个微前端工程指定一个确定的、独占的基础路径(前缀)。这样相同域名下可以用不同路径进行独立访问。路径统一以 /mfe- 开头,如 /mfe-order。这也就是上面 portal 里业务工程的 indexHtml 配置示例里所展现的那样。


如果还需要本地独立开发业务工程(即不启动本地 portal 工程),则还需要在业务工程的 index.html 文件中引入 portal 工程的资源,以模拟线上环境用户访问时先加载 portal 后加载业务工程的方式。具体方式堵着可以自行摸索一下。


公共依赖处理

大部分的业务工程可能都会有一些共同的依赖,比如 Vue、moment、lodash 等。如果将这些内容都打包到各自业务工程的 vendor.js 里,则势必会导致代码冗余太多,浪费带宽,还可能导致浏览器运行内存压力增大。我们可以把这些公共依赖、公共组件、CSS、Fonts 等都放到 portal 工程里,将依赖、组件 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:portal'}
复制代码


业务工程则通过 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:portal"]["'+request+'"]')        } else {            callback();        }    },}
复制代码


结语

以上就是我们微前端改造与实践方面的一些经验。前路漫漫,这里面还存在很多待完善的地方,如 History 模式支持、i18n 更好地集成、各个业务工程的加载顺序优化及个性化等。除了这些纯粹技术上的探索,在拥有微前端、微服务这些架构的基础上,团队也可以考虑进行垂直拆分:一个小组独立负责一块业务,它有自己的微前端工程和微服务工程。从技术管理到人员管理,将它们糅合在一起统一考虑,这也是我们软件工程的探索方向。期待这些能够对大家带来一些思考和帮助!


从 2019 年 3 月份开始,我们逐步解决了此处展望中的所有问题与期望。我们构建了一套全新的多应用、多租户的平台,也同时遇到了一些新的问题。感兴趣的读者可以打开 https://developer.engage-all.com/ 简单了解一下。从微前端的实践开始,到现在微应用的实践,这一条路并不是很轻松,还有很多问题也亟待去优化、去解决。期待更多人在这些方面提供更好的解决方案。


2020 年 1 月 17 日 18:08579

评论

发布
暂无评论
发现更多内容

架构师训练营第三周作业

Wee权

关于国际化语言 Intl

西贝

Java 前端 国际化 格式化

LeetCode题解:111. 二叉树的最小深度,递归,JavaScript,详细注释

Lee Chen

前端进阶训练营

产品分析

时间是一个人最好的证明

产品经理 产品设计

记一次MySQL日期范围查询优化

墨凡

MySQL SQL优化

4年Java经验,备战两月成功拿到美团、京东、字节offer

Java成神之路

Java 面试 算法 编程语言 面试程序员

区块链教育 丨 首批区块链专业新生正式入学

CECBC区块链专委会

区块链技术 区块链教育

31道Java核心面试题,一次性打包送给你

小Q

Java 学习 程序员 架构 面试

建筑行业区块链应用场景是怎样的

CECBC区块链专委会

区块链 行业资讯

“海外同步优惠”与“中国专享折扣”十大必败榜抢先放送

爱极客侠

数字货币交易所源码开发,交易所APP搭建

135深圳3055源中瑞8032

程序员去外包真的不可取吗?

Java架构师迁哥

从联想ThinkStation工作站,窥见工具文明的新纪元

脑极体

用NOSql给高并发系统加速

架构师修行之路

nosql redis 分布式 微服务

MySQL事务隔离级别

长沙造纸农

MySQL 事务隔离级别 mysql事务 事务 MySQL 运维

开源数据库这么香,为什么我们还要下功夫自研?

华为云开发者社区

数据库 开源 数据

架构1期第四周作业1-大型互联网系统技术梳理

道长

极客大学架构师训练营

JDK 中的栈竟然是这样实现的?

王磊

Java 数据结构和算法

区块链支付系统开发公司,USDT承兑支付

135深圳3055源中瑞8032

架构师训练营第四章 系统架构总结

郎哲158

Apache Doris在云真信智能决策分析平台的应用实践

DorisDB

数据库 数据仓库 金融科技

震精,京东T8工程师每天熬夜到天明,竟只是为一套编程实战文档

周老师

Java 编程 程序员 架构 面试

古北水镇的夜

张晓楠

生活 摄影

手把手教你AspNetCore WebApi:Serilog(日志)

AI代笔

ASP.NET Core web api serilog

内存条的讲解

亚兰—硅的传奇official

原创 内存 硬件 计算机 哔哩哔哩

架构师训练营第四周作业

郎哲158

做好分库分表其实很难之二

架构师修行之路

微服务 分库分表

架构师训练营第1期第四周作业二

道长

极客大学架构师训练营

架构师作业第三周学习总结

Wee权

区块链是一个不知道要解决什么问题的解决方案吗?

CECBC区块链专委会

比特币 区块链 银行

Spring Cloud 微服务实践(7) - 日志

xiaoboey

kafka 微服务 Spring Cloud 日志 spring cloud stream

Leader修炼指“北”:管理路上的大小Boss

Leader修炼指“北”:管理路上的大小Boss

微前端实践-InfoQ