写点什么

微前端实践

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:08717

评论

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

并发队列:ArrayBlockingQueue实际运用场景和原理

叫练

阻塞队列 LinkedBlockingQueue 并发队列 阻塞List ArrayBlockingQueue

利益相关者的问题及方案

梁媛

即兴演讲的几种实用脚本

熊斌

读书笔记 28天写作

漫话递归与迭代

Justin

算法 方法论 成长 心灵鸡汤 28天写作

超好用的文件转换神器!拿走不谢~

白色蜗牛

程序员 软件工具 生产工具

深入理解 ProtoBuf 原理与工程实践(概述)

vivo互联网技术

数据结构 序列化 protobuf

工业互联网的“第一高地”,在哪?

浪潮云

工业互联网

【CSS】css控制鼠标点击事件(pointer-events)

学习委员

html/css CSS小技巧 28天写作 纯CSS 2月春节不断更

GitHub 标星 167k!你要的优质书籍这都有,还开源!

沉默王二

GitHub 开源项目 电子书

Elasticsearch Document 查询内部原理

escray

七日更 28天写作 死磕Elasticsearch 60天通过Elastic认证考试 2月春节不断更

开发质量提升系列:日常重视好投产,运维拍肩也不怕

罗小龙

最佳实践 方法论 28天写作 2月春节不断更

产品训练营·第三周作业 & 总结

tiu

浏览器同源策略,听说过么?

华为云开发者社区

浏览器 jsonp CORS 同源策略 跨域

KubeEdge@MEC:Kubernetes容器生态与5G的结合

华为云开发者社区

5G 边缘计算 网络 kubeedge 5G MEC

第 3 周作业

老元宵

offline app

lidaobing

28天写作 offline app

智汇华云 | ArSDN之多集群简介

华云数据

华云数据

区块链矿机挖矿游戏开发,区块链矿机游戏开发

v16629866266

为您收录的操作系统系列-进程管理(上篇)

Arvin

操作系统 进程

手机里什么APP都没有,一个很无趣的人 | 视频号28天(27)

赵新龙

28天写作

数据中心网络技术新贵:VXLAN与园区网络虚拟化

华为云开发者社区

网络 数据中心 虚拟化 VXLAN 二层网络

从JNOS商业操作系统,看京东零售商业化之路新探索

京东科技开发者

零售 数字化转型

火出圈的Clubhouse,究竟有什么奥秘?

拍乐云Pano

flutter RTC 语音聊天室 社交APP出海 clubhouse

28天瞎写的第二百三十六天:emacs 党的没落

树上

28天写作

车载操作系统 (28天写作 Day26/28)

mtfelix

28天写作 车载操作系统 AOS QNX

如果创意也可以被设计「幻想短篇 26/28」

道伟

28天写作

浅谈OKR工作法

一笑

管理 OKR 28天写作

考前复习必备MySQL数据库(关系型数据库管理系统)

魔王哪吒

MySQL 程序员 面试 后端 2月春节不断更

熬夜肝了个IDEA插件整合程序员常用的工具,总有你能用上的

Silently9527

IDEA idea插件 java程序员

VUCA时代-不敏捷就得死

Ian哥

28天写作

【经验分享】如何融合CMMI与企业需求,自定义推进数字化转型

嘉为蓝鲸

DevOps 敏捷 持续交付 CMMI 能力成熟度模型

微前端实践-InfoQ