最近工作重心在平台这块,恰好做了一些微前端的改造,在这里总结一些遇到的挑战和踩过的坑。
缘起
目前负责的工程是这种经典的页面布局,通过左侧菜单打开一个个页面,并可以随时切换
以前的实现基本上是通过iframe技术实现的,打开一个标签页其实是加载一个单独的网址。
通过iframe打开标签页其实已经是一种较完美的方案。iframe天生提供了硬隔离,不用关心任何污染与互相影响。
但是iframe仍有些不足。在我们项目场景中,有三点不满意:
1.资源加载多份,打开多个iframe标签意味着加载多次react等较大的三方库,电脑配置低会卡顿
2.每次关闭再打开标签都是一次浏览器窗口资源重建的过程,有明显白屏
3.多路由的子应用,两个iframe页签打开的是同一个子应用的不同路由,无法优化
经过内部讨论,决定使用微前端方案。
微前端框架选择了qiankun作为基础。emp的话需要子应用升级webpack5,现有场景是有大量其他部门的研发,极难做到协同。
qiankun官网地址qiankun.umijs.org/zh/guide
以下改造过程默认对qiankun和微前端有些了解
思路
1.多页签来回切换时,都要秒开对应页签的内容,至少要赶上keep-alive的速度,这要求我们对子应用做一定的缓存或者直接不卸载,目前的话我们现有标签先做不卸载的处理。多子应用共存的话我们可以使用qiankun2.0的手动加载微应用方法,打开一个标签页就手动加载一个子应用,关闭就卸载。
2.如果基座和众多子应用都有自己路由并监听地址栏的话会非常混乱,这里需要把接入的子应用项目中的路由统统改造成虚拟路由
vue的router使用 abstract mode
react的router使用 MemoryRouter
基座改造
现有的基座是一个React项目,改造起来比较轻松
-
引入qiankun库
-
新建一个微前端组件容器(示例如下)
import { MicroApp, loadMicroApp } from 'qiankun';
import React from 'react';
export default class MicroAppWapper extends React.Component<any> {
private containerRef: React.RefObject<HTMLDivElement> = React.createRef();
private microApp: MicroApp = null;
componentDidMount() {
const { name, entry, ...props } = this.props;
this.microApp = loadMicroApp({ name, entry, container: this.containerRef.current, props }, {
fetch(url, ...args) {
// 给指定的微应用 entry 开启跨域请求
if (url === 'http://app.alipay.com/entry.html') {
return window.fetch(url, {
...args,
mode: 'cors',
credentials: 'include',
});
}
return window.fetch(url, ...args);
},
sandbox: {
// 开启shadowdom会导致适配成本,请慎重,下面会详细讲解
strictStyleIsolation: true
}
});
}
componentWillUnmount() {
this.microApp.unmount();
}
componentDidUpdate() {
// 可以做一些自定义操作
}
render() {
return <div ref={this.containerRef}></div>;
}
}
复制代码
- 替换原来的iframe(示例如下)
if (type === "microApp") {
return <MicroAppWapper
name={title}
entry={domain}
initialUrl={domain}
/>
}
if (type === "service") {
return <Iframe url={url} />
}
复制代码
子应用改造
基本改造
打包工具
微应用的打包工具需要增加如下配置:
// package.json里的name尽量不要大众化,不然不容易分辨
const packageName = require('./package.json').name;
module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
};
复制代码
微应用入口
微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
// 基本不用做什么
}
/**
* 应用每次进入都会调用 mount 方法,通常我们走一部分entry的代码,并在这里触发应用的渲染方法
*/
export async function mount(props) {
// react示例代码 ? (下面的只是样板代码)
ReactDOM.render(<App/>, props.container.querySelector('#root'));
// vue示例代码 ?
const { container } = props;
instance = new Vue({
render: h => h(App)
}).$mount(container ? container.querySelector('#root') : '#root')
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container.querySelector('#root'),
);
}
export async function update(props) {
// 目前基本不用做什么
}
if (!window.__POWERED_BY_QIANKUN__) {
// 之前的入口样板代码⤵️ 请用上面的if逻辑⤴️包裹起来,这样子应用就不会加载两次
ReactDOM.render(<App/>, props.container.querySelector('#root'));
}
复制代码
非webpack情况
非webpack构建的应用请看??
qiankun.umijs.org/zh/guide/tu…
路由改造
目前由于多页签模式,会加载许多子应用,子应用路由如果都监听地址栏进行响应的话,会不可避免的引发混乱。所以我们需要改造子应用为 模拟路由的形式
mount阶段透传参数
mount的时候会传入initialUrl,我们只需要透传给App组件即可
ReactDOM.render(<App
initialUrl={props.initialUrl}
/>, props.container.querySelector('#root'));
复制代码
app阶段示例代码
vue的router使用 abstract mode
react的router使用 MemoryRouter
import { MemoryRouter, withRouter, Route, Switch } from 'react-router'
const App = (props) => {
let initialEntries;
if (props.initialUrl) {
// 处理它的哈希值
const { hash } = new URL(props.initialUrl)
initialEntries = [hash.replace(/^#/g, '')]
} else {
// 这段代码是为了能独立打开
const hash = window.location.hash;
const rehash = hash.replace(/^#/g, '');
if (rehash && rehash !== '/') {
initialEntries = [rehash]
} else {
initialEntries = ['/']
}
}
// 替换原来的Router为MemoryRouter
return <MemoryRouter
initialEntries={initialEntries}
>
<Root />
</MemoryRouter>
}
export default App
复制代码
split chunk的处理
如果打包出多个chunk.js,可能会拉取不到chunk.js,需要配置一下webpack的public_path。
在入口的entry.js上顶部加上如下代码:
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
复制代码
shadow_DOM适配
这个工作可能是一个较大的工作,如果子应用的页面大多数都在自己部门 或 整个项目只是一个中后台内部项目,我认为没必要上shadowdom
因为涉及到许多其他部门,为了避免样式加载混乱,基座对子应用节点包裹了一层影子DOM
developer.mozilla.org/zh-CN/docs/…
shadow_DOM 需要大量的适配工作,不过,目前看shadow_DOM规范 正在正确的道路上前进,极有可能在将来成为标准,所以这部分工作还是值得的。
React升级到17以上
shadow_DOM对document做了代理,17版本以下的React合成事件会不生效
最简单的方式是升级到17以上
css适配
这个需要检查一下加载子应用后整体样式的变化
原生方法报错
document.querySelector或document.getElementById等方法会失效
解决方案:
1.主流框架中其实不推荐操作Dom,请用框架的方式修改
2.在mount的时候会把container这个子应用容器拿到,我们可以放在window上,所有用的地方做兼容
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
// 把container挂载到自己的window上(此处不会污染基座的window)
window.MicroAppContainer = props.container
....
}
复制代码
然后全局搜索document.,找到querySelector或getElementById等方法替换成
(window.MicroAppContainer || document).
第三方库运行时共享
基座如果加载过react/vue等库,我们可以在子应用中直接使用它,这是微前端最直接的优化手段
但不得不说,这个共享运行时不太好,可能会带来一些问题
具体操作是基座加载CDN资源,并配置webpack的externals,这样window上就有了相应的库
子应用配置相同的externals就行
(axios等运行时单例的库一定不要带上…)
拓展
1.内存监控
多应用共存仍然会拖累内存,我们需要监听浏览器内存,卸载最不常用的子应用(LRU算法)
2.跨域
子应用请求的后端接口可能会跨域,因为host是基座的host,这块可能需要后端配合或搭建中间层转发
3.部署调试
如果是iframe,部署方式不变。其他部门调试子应用可能需要这边提供一套完整的脚手架
4.微应用和原地址共存
没有特殊情况是支持共存的,特殊需求可以通过webpack配置多个html来做
一些其他注意事项
1.start这个qankun方法在工程中不用加,里面主要做了一些Single-Spa对路由的处理,这里涉及不到
2.后期如果qiankun支持module federation的话,可以让组件级别复用更加容易(期待)
3.遇到错误可以先到官网查看 qiankun.umijs.org/zh/faq
总结
总结一下上面的改造过程,有三个点需要注意
1.多页签的话需要手动控制子应用加载和卸载(loadMicroApp),不基于路由
2.子应用工程里面有路由的话需要改造成虚拟路由
3.shadowdom做样式硬隔离目前成本较高,根据场景来
截至本文写完,已经小范围试用,如果有任何问题或者建议,欢迎评论区拍砖!如果感觉吃力的话,下一篇我会对qiankun的原理进行剖析,着重讲解几个我关注的点