一、微前端开发的特性
1、技术无关:各个开发团队都可以自行选择技术栈,不受同一项目中其它团队影响;
2、业务独立:各个交付产物都可以被独立使用,避免和其它交付产物耦合;
3、样式隔离:各个交付产物中的样式不会污染其它组件;
4、原生支持:各个交付产物都可以自由使用原生API,而非要求使用封装后的API。这第四点一般来说是很难实现的。
二、架构微前端要做哪些事情
1、微服务平台和微前端对比:
微前端是一种架构,它不是一种技术。当你去架构微前端的时候,需要做哪些事情呢,我们可以类比着微服务来找出要做的事情。
| 微服务 | 微前端 | 备注 | |
|---|---|---|---|
| 服务 | 独立服务,比如交易服务 | 应用或者模块,比如导航 | 服务是不会相互影响的 |
| 服务治理 | 服务注册/发现/依赖管理/跟踪/降级/限流/日志/监控/运维 | 应用的发现/路由/鉴权/监控/降级/运行/注销/聚合等 | 需要一个/多个系统统一处理一些上层的事情 |
| 服务通信 | HTTP/RPC/中间件 | eventBus/sharedWorker/BroadcastChannel/LocalStorage |
从上表中可以看到,架构微前端的时候,至少你可以分出来两个层,一个是服务层,一个是服务治理层。应用层中就是一个个的具体的应用或模块,而服务治理层是统一处理应用的一些基本问题,如应用的发现、路由、鉴权、监控、降级、运行、注销、聚合等等。
架构是与技术无关的。
2、微前端的基本结构:
上层是应用层,包含N个应用,下层是应用治理层,是个主工程。
- 中心化路由,服务注册中心
- 标识化应用
- 设计一定的生命周期
- 部署和配置自动化
其中,主框架的作用如下:
- 应用的发现和调度;
- 转场动画/日志/上报;
- 应用隔离影响(应用1挂了要求不影响应用2)/CSS隔离影响;
- 应用监控/降级(当正常的方案不可用时,需要能够快速切换到备选方案)/鉴权等;
- 应用间通信机制。
如果主框架挂了怎么办?
一是要如何保证主工程不挂(优质的人力、重点Code Review等);
二是主框架挂了之后如何快速检测出来(监控),并修复(一个是为主框架提供备份,主框架挂了切到备份上;热修复能力)。
三、微服务的常见应用场景
1、将一个大型的单个应用拆分成多个独立的应用,通过导航和动态加载来实现无缝的切换。
2、将单个页面上内容拆分成不同的模块交给不同的团队去维护。
四、如何实施微前端
其实主要做两件事——拆分和聚合。
1、常见的拆分方式:
- 大仓库拆分成独立的模块,分成不同的目录,但还是在一个大仓库中,采用统一的构建。
- 大仓库通过monorepo methodology拆分多个npm包,npm包发布到npm仓库,或者通过git submodule的方式,集成到主项目。可以做到独立构建,但没有做到独立发布。
- 大仓库拆分多个子仓库,构建部署出独立的在线服务/应用,通过iframe方式,或者WebComponent的方式聚合成一个完整的应用,这种方式既可以做到独立构建,也可以做到独立发布。
- 大仓库拆分成多个子仓库,构建后生成JS/CSS/manifest.json等文件,主应用中去动态加载它们,集成到主框架中。这种方式既可以做到独立构建,也可以做到独立发布。
2、常见的技术方案:
1)通过iframe的方式聚合;
对于一些共用UI组件而言,仍然需要重复加载。这也就是iframe模式下的问题 iframe嵌套是一个比较大的问题; iframe在移动端适配的时候会存在一些兼容问题。 iframe会阻塞onload、占用连接池、多层嵌套页面崩溃。
iframe的优势也比较明显: 改造成本低,可以快速上线; 沙箱模型,各个模块天然隔离,不需要考虑样式污染和应用之间的相互影响。
iframe方式实现的Demo:
constructor(props) {
super(props);
this.gotoApp = this.gotoApp.bind(this);
window.addEventListener('message', this.handleMessage.bind(this));
}
state = {
currentApp: null
}
handleMessage(e) {
// 定义一套标准的通信机制,比如,父子页面传递的数据含有type、data等字段
const { data } = e;
// 这里type定义为domReady,load,unload,openLink
if (data.type === 'openLink') {
// 处理子页面里面打开链接
alert(data.data.url);
// 将URL push到state里面
window.history.pushState({}, '', data.data.url);
}
}
render() {
let { currentApp } = this.state;
return (
<div className="app">
<header className="app-header">
{ apps.map((app) => {
return (
<div className="item" onClick={() => {this.gotoApp(app)}}></div>
)
}) }
</header>
<div className="content">
{currentApp ? <iframe src="currentApp.link" onError="{this.handleException}" border=""></iframe>}
</div>
</div>
);
}
handleException() {
// 处理异常、降级方案等
}
gotoApp(app) {
// 这里可以显示转场动画
// 也可以添加日志,记录应用的使用情况
this.setState({
currentApp: app
})
}
urlParse() {
// 处理URL模块,解决URL无法记录的问题
}
// App.js
export default class extends Compoent {
constructor(props) {
super(props);
}
state = {
currentApp: null
}
render() {
return (
<div className="app">
<header className="app-header">
<div onClick={() => { openLink('/a/b') }}Learn React</div>
</header>
</div>
)
}
}
// client.js
// 定义一个SDK,负责进行父子页面的通信
// TODO: 应用之间的通信如何处理;如何解决子应用使用a标签,而不是通过代理到主应用的方式
let postMessage = function(data) {
window.top.window.postMessage(data, 'http://localhost:3001')
}
window.addEventListener('DOMContentLoaded', () => {
postMessage({
type: 'domReady',
data: {
time: Date.now()
}
})
})
export function openLink(url) {
postMessage({
type: 'openLink',
data: {
url: url
}
})
}