微前端要点:
-
微服务架构,可以解耦后端服务间依赖。而微前端,则关注于聚合前端应用。
-
而不论哪种方式,都需要提供一个查找应用的机制,在微前端中称为服务的注册表模式。和微服务架构相似,不论是哪种微前端方式,也都需要有一个应用注册表的服务,它可以是一个固定值的配置文件,如 JSON 文件,又或者是一个可动态更新的配置,又或者是一种动态的服务
-
业务拆分
-
技术方式
从技术实践上,微前端架构可以采用以下的几种方式进行:
路由分发式。通过 HTTP 服务器的反向代理功能,来将请求路由到对应的应用上;微应用。通过软件工程的方式,在部署构建环境中,组合多个独立应用成一个单体应用;微件化。开发一个新的构建系统,将部分业务功能构建成一个独立的 chunk 代码,使用时只需要远程加载即可;前端容器化。通过将 iFrame 作为容器,来容纳其它前端应用;应用组件化。借助于 Web Components 技术,来构建跨框架的前端应用。实施的方式虽然多,但是都是依据场景而采用的。有些场景下,可能没有合适的方式;有些场景下,则可以同时使用多种方案。 -
有效治理:
- 应用组合:组合时机,在构建时组合,还是在运行时组合;应用路由,如何根据 URL 加载/导航到不同的页面,如何根据子应用界面的变化切换 URL;应用加载,确定加载应用的版本,依赖于框架的加载机制,还是采用 AMD 或者 SystemJS 异步加载
- 应用隔离:应用容错,某个应用的崩溃不应影响到其他应用或容器应用;样式隔离,避免 CSS 相互污染;DOM 隔离,避免子应用操作非自身作用域内的节点
- 应用协调与治理:统一配置与切换,主题,利用 CSS Variables 等方式动态换肤;应用的生命周期,规范化子应用的生命周期,并且在不同生命周期中执行不同的操作;数据共享,子应用间数据共享;服务共享,跨应用数据共享与服务调用;组件共享,可能将某个纯界面组件或者业务组件以插件(Plugin)或者部件(Widget)的方式共享出去;提供某个计算能力。
- 开发环境:跨技术栈支持;统一的构建流程与规范;打桩、埋点与 Hijack。
- 具体实现
-
portal工程
portal,顾名思义,就是入口。这也就是上面所说的微前端加载器。当用户打开浏览器,首次进入我们的页面时,不管是什么 URL,首先加载的就是 portal。portal 里会配置所有业务工程的地址、匹配哪些 URL、需要加载哪些资源。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 来实现,但原理就是这么简单。 -
业务工程
业务工程就是普通的微前端工程,一般一个模块一个工程。业务工程要扮演两个角色,一个是可独立运行的前端工程,一个是受 portal 控制的运行时。前者主要用于我们本地开发,后者则是线上集成时使用。在独立运行时,它跟原来的前端工程没有什么区别。以 Vue 工程为例,照样使用 new Vue({el: ‘#app’}) 来启动、渲染页面。
而当受控运行时,则是利用 UMD 方式输出几个钩子函数,包括初始化、加载、卸载。
线上环境的 Webpack 配置:
output: {
libraryTarget: “umd”,
library: ‘mfe:customer’
}
而区分是否受控,则可以通过判断一个全局变量来实现。如 window.IS_IN_MFE,portal 工程在运行时会将其设置为 true。
为了支持本地多个工程同时开发,我们需要为每个微前端工程指定一个确定的、独占的端口号。比如从 8100 开始,逐一递增。同时,为了支持线上部署,我们还需要给每个微前端工程指定一个确定的、独占的基础路径(前缀)。这样相同域名下可以用不同路径进行独立访问。路径统一以 /mfe- 开头,如 /mfe-customer。这也就是上面 portal 里业务工程的配置示例里所展现的那样。 -
common工程
,每一个业务工程都是一个独立的前端工程,所以里面会有一些相同的依赖,如 Vue、moment、lodash 等。如果将这些内容都打包到各自的 vendor.js 里,则势必会导致代码冗余太多,浏览器运行内存压力增大。我们把这些公共依赖、公共组件、CSS、Fonts 等都放到一个工程里,由该工程进行打包,将依赖、组件 export,并以 UMD 的方式注入到全局。 -
特殊业务工程:mfe-navs
我们产品的页面结构分为顶部栏、侧边栏、中间内容区三大块。顶部栏和侧边栏在页面跳转过程中,基本上保持不变。所以我们也将它们剥离出来作为一个独立的微前端业务工程,叫做 mfe-navs。它会匹配所有的 URL,也就是说访问任意 URL 时,都会加载它,而且还要保证先加载它。当它加载完毕后,会在页面内提供一个中间内容区的锚点 DOM(#app:),供其他业务工程加载时挂载。
实践中的问题:
Stitching layer 作为主框架的核心成员,充当调度者的角色,由它来决定在不同的条件下激活不同的子应用。因此主框架的定位则仅仅是:导航路由 + 资源加载框架。
而具体要实现这样一套架构,我们需要解决以下几个技术问题:
路由系统及 FutureState
我们在一个实现了微前端内核的产品中,正常访问一个子应用的页面时,可能会有这样一个链路:
由于我们的子应用都是 lazy load 的,当浏览器重新刷新时,主框架的资源会被重新加载,同时异步 load 子应用的静态资源,由于此时主应用的路由系统已经激活,但子应用的资源可能还没有完全加载完毕,从而导致路由注册表里发现没有能匹配子应用 /subApp/123/detail 的规则,这时候就会导致跳 NotFound 页或者直接路由报错。
这个问题在所有 lazy load 方式加载子应用的方案中都会碰到,早些年前 angularjs 社区把这个问题统一称之为 Future State。
解决的思路也很简单,我们需要设计这样一套路由机制:
主框架配置子应用的路由为subApp: { url: ‘/subApp/**’, entry:’./subApp.js’ },则当浏览器的地址为 /subApp/abc 时,框架需要先加载 entry 资源,待 entry 资源加载完毕,确保子应用的路由系统注册进主框架之后后,再去由子应用的路由系统接管 url change 事件。同时在子应用路由切出时,主框架需要触发相应的destroy 事件,子应用在监听到该事件时,调用自己的卸载方法卸载应用,如 React 场景下 destroy = () => ReactDOM.unmountAtNode(container) 。
要实现这样一套机制,我们可以自己去劫持 url change 事件从而实现自己的路由系统,也可以基于社区已有的 ui router library,尤其是 react-router 在 v4 之后实现了 Dynamic Routing 能力,我们只需要复写一部分路由发现的逻辑即可。这里我们推荐直接选择社区比较完善的相关实践single-spa。
总结:
微前端关注于聚合前端应用。微前端的实现有多种,但是具体实施还需按照业务需求来定制。依据不同的业务场景而采用不同的解决方案。