技术圈是比较爱炒概念的,中台、微服务、DDD一时都变成比较热门的话题,微前端作为微服务的一种自然也免不了俗。在我看来,大多数情况下大多数公司都用不上微前端。当然微前端不是什么新技术,只是为了解决实际问题的一种方法。微前端这三个字听起来不明所以,实际上只是将项目打散,变成若干小项目的合集,使用一种方案使得在多个项目之前平滑切换的方法。
为什么需要微前端
前面说过,大多数情况下我们是不需要微前端的,通过清晰的组件划分,部分情况下使用 iframe 嵌入就能解决绝大部分问题。但是微前端对下面的问题解决起来更有优势:
迭代日积月累导致维护困难
一个生命周期超长的软件,必然产生出一个体积庞大的软件。软件会囊括各种交错复杂的业务逻辑,阅读维护起来特别困难。前端项目体积滋长还会使编译时间越来越长,本地开发热更新时电脑吃力,卡机。技术栈多样性
公司在逐步扩大业务,永远有新的业务需要上线,永远有新的技术栈在尝试。庞大的单体软件很难同时运行多个技术栈,很难在业务中尝试使用新的技术,举步维艰。跨团队开发
若同一项目交给多个团队同时开发,由于是同一项目同一仓库,团队开发的资源并没有隔离经常会导致代码冲突,互相影响业务功能,造成开发风险。
技术方案
- iframe 嵌套多个子项目
- MPA 多子项目之前通过链接跳转
- 整合多个子项目资源,由主项目动态导航不同的子项目
这里重点介绍第三种
Single-SPA
这种方案的重点是加载子应用资源,并根据路由进行资源导航,实现的方式市面上的工具各有不同
比较知名的开源方案有:
- Single-SPA
- Qiankun
- icestark
qiankun 使用了 single-SPA 的路由系统,且实现了一个 sandbox,用来隔离 js 运行环境。icestark 是飞冰团队的微前端方案,整体思路和前两者也比较接近。这里以 single-SPA 介绍其实现思路。
Single-SPA 将项目分为了主应用和子应用,主应用负责子应用导航,加载 JS、挂载应用等功能,而我们的主要业务代码放在子应用内。
主应用注册子应用示例
1 | import { registerApplication, start } from "single-spa"; |
子应用导出示例
1 | import "./set-public-path"; |
上面的两个代码示例介绍了 single-spa 的使用方法。那 single-spa 怎么实现微前端的呢?整体思路为:
子应用导出生命周期函数
子应用需导出bootstrap
、mount
、unmount
、update(可选)
函数,用于在single-spa
生命周期中调用来挂载和卸载子应用single-spa 提供
registerApplication
注册子应用。
该方法第二个参数applicationOrLoadingFn
用于异步导入子应用的资源包,示例中使用了 system.js 提供的 import 方法导入,当然别的异步导入方法也是支持的,只要返回一个 Promise 就行。
该方法第三个参数activityFn
用于确认是否激活该子应用。这是 single-spa 路由系统的关键,只有激活状态的子应用才会 mount。
方法调用后会生成一个 app 对象并将 name, applicationOrLoadingFn, activityFn 等参数挂载到 app。调用
reroute
方法挂载应用
如果子应用还未调用start
方法则会调用loadApps
将所有激活状态(通过 activityFn)的子应用的 load 下来,并将子应用生命周期方法挂到 app 对象中。
否则,调用performAppChanges
。该方法会先后调用需激活的子应用的 bootstrap、mount 方法完成子组件的挂载。监听
hashchange
,popstate
事件拦截路由变化
路由变化时 single-spa 会重新调用reroute
决定哪些子应用需要 unmount,哪些应用需要 mount。
不仅如此,还会重写window.addEventListener
和window.removeEventListener
拦截所有hashchange
和popstate
的事件注册,
使事件处理器在子应用在 single-spa 处理完子应用的load
、挂载或卸载之后才被调用。
Single-SPA 与 Qiankun
qiankun 实现了一个 JS sandbox,避免子应用之间的环境污染
实现逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const rawWindow = window;
const fakeWindow = Object.create(null) as Window;
const sandbox: WindowProxy = new Proxy(fakeWindow, {
set(_: Window, p: PropertyKey, value: any): boolean {
// 省略...
},
get(_: Window, p: PropertyKey): any {
// 省略...
},
has(_: Window, p: string | number | symbol): boolean {
return p in rawWindow;
},
});qiankun 通过
Proxy
拦截了fakeWindow
对象,在使用他们编写的插件import-html-entry
load 子应用 js 时将其作为子应用的 window,并且每个子应用生成的对象都不同。这样,就实现了每个子应用的环境独立,避免变量污染。1
2
3
4
5
6
7
8
9
10
11// get the entry html content and script executor
const { template: appContent, execScripts, assetPublicPath } = await importEntry(entry, {
// compose the config getTemplate function with default wrapper
getTemplate: flow(getTemplate, getDefaultTplWrapper(appName)),
...settings,
});
//省略部分代码...
// get the lifecycle hooks from module exports
let { bootstrap: bootstrapApp, mount, unmount } = await execScripts(jsSandbox);JS Entry vs HTML Entry
qiankun 采用 HTML Entry 作为资源注入方式,single-SPA 采用 JS Entry 方式注入。JS Entry 方式要求主应用必须给子应用提供一个挂载的 DOM 节点;子应用需要将资源(js、css 等)打包到一个文件,或者打包时将所有子应用的资源路径单独保存配置,在主应用引用;
HTML Entry 方式则不需要单独提供挂载点,也不需要单独处理资源加载的问题,无论是挂载点还是资源都在 HTML 中,都能一次性全部解析到。qiankun 拉取和解析 html 还是使用
import-html-entry
完成的。