作者简介
谢宇航:Self-introduction is not defined !
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的小块,而在用户看来仍然是内聚的单个产品。
微前端具备以下几个特点
提到微前端的解决方案,为什么不用 iframe
?这几乎是所有微前端方案第一个会被提到的问题。但是大部分微前端方案又不约而同放弃了 iframe
方案,自然是有原因的...
虽然iframe
无疑是最简单的方式,还天然支持样式隔离以及全局变量隔离,但是iframe
也有让人诟病的几个问题:
iframe url
状态会丢失、后退前进按钮无法在iframe
里使用。但是产品又要求你子应用之间跳转返回到上一个子应用里某个页面的url
的时候,你是不是心里会凉了一截。iframe
里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize
时自动居中,这时候你是不是已经感到无比的崩溃。iframe
内外系统的通信、数据同步等需求,主应用的 cookie
,token
要透传到根域名的不同的子应用中实现免登效果。在之前需求中,子应用接口数据还需要到主应用中解密才可以,导致需要利用post message
+ 监听做父子之间。虽然可以解决,但是你是不是感到十分的恶心?iframe
还存在同源策略,极大地增加了子应用之间通信的难度。我们开始切入主题,既然说了iframe
这么多缺点,那么一定是有很好的方案去解决iframe
的痛点。下面我们用一张表格来对比一下qiankun
和iframe
的区别:
qiankun | Iframe | |
---|---|---|
数据共享 | window | hash |
事件机制 | 私有通信机制 | PostMessage |
访问历史 | 应用之间统一 | 应用之间独立 |
全局作用域 | 共享 | 完全隔离 |
CSS 作用域 | 可共享可独立 | 完全独立 |
资源加载 | 可预加载 快 | 慢 |
qiankun
是通过**共享window
**来实现,而iframe
则很局限,只能在通过hash
、query
等方式在url
上添加数据。qiankun
封装了一套私有通信机制,主应用和子应用可以通过提供封装好的 API 来进行双向的数据交互,而iframe
子应用到主应用通信则需要postMessage
方式进行。qiankun 是基于 single-spa,具备 js 沙箱、样式隔离、HTML Loader、预加载 等微前端系统所需的能力。qiankun 可以用于任意 js 框架,微应用接入像嵌入一个 iframe 系统一样简单。
他的实现原理主要分为以下几个方面,这里通过梳理原理顺便讲一下对应需要的配置项。这样在应用期间可以很清晰的明白为什么这样配置,并且也可以帮助快速定位问题。
例如下面的链路图,当点击导航中的某个子应用链接,这时候主应用通过劫持 url change 事件,匹配子路由并加载命中的子应用资源,待加载完之后,子路由接管 url change 事件。子应用的路由表需要设置 base url 来保证访问某一子应用期间是一直可以命中当前子应用的。
主应用与子应用的集成方式为运行时组合。子应用自己构建打包,主应用运行时动态加载子应用资源。这样的方式也更加灵活,并没有采用构建时的组合方式。这样子应用每次发布更新不依赖父应用重新打包发布。达到父子应用解耦的效果。
刚才讲到路由系统时说道匹配完子路由并加载子应用,那么 qiankun 加载过程实际上是主框架通过 fetch html
的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中(如下图所示)。由于子节点是实实在在插入到主应用的,这样实际上实现了共享 window 的效果。
qiankun 一大特点就是将 html 做为入口文件,规避了 JavaScript 为了支持缓存而根据文件内容动态生成文件名,造成入口文件无法锁定的问题。qiankun 将 html 做为入口,所依赖的 import-html-entry
库。
原理:import-html-entry
模块
需要注意的几点:
使用 fetch 涉及跨域,需要在子应用中设置 CORS 跨域。
需要配置public path
解决微应用动态载入的 脚本、样式、图片 等地址不正确的问题。
qiankun 的 JS 沙箱是基于 Proxy 实现代理了 window 上常用的常量和方法以及不支持 Proxy 时降级通过快照实现备份还原。
原理:
由于微前端场景下,不同技术栈的子应用会被集成到同一个运行时中,所以我们必须在框架层确保各个子应用之间不会出现样式互相干扰的问题。解决方案有以下几种:
基于 Web Components 的 Shadow DOM 能力,子应用的样式作用域仅在 shadow 元素下,我们可以将每个子应用包裹到一个 Shadow DOM 中,保证其运行时的样式的绝对隔离。
但是 shadow DOM 的缺点也很明显。比如 sub-app 里调用了 antd modal 组件,由于 modal 是动态挂载到 document.body 的,而由于 Shadow DOM 的特性 antd 的样式只会在 shadow 这个作用域下生效,结果就是弹出框无法应用到 antd 的样式。解决的办法是把 antd 样式上浮一层,丢到主文档里,但这么做意味着子应用的样式直接泄露到主文档了。
通过约定 css 前缀的方式来避免样式冲突,即各个子应用使用特定的前缀来命名 class,或者直接基于 css module 方案写样式。
通常可以给主应用样式都加前缀,因为主应用内容相对较少,加起来很方便。这也适用于全新的项目。但是对于子应用中使用了三方的组件库,三方库在写入了大量的全局样式的同时又不支持定制化前缀的时候又很难去解决。
配合 HTML Entry,只需要在应用切出/卸载后,同时卸载掉其样式表即可,原理是浏览器会对所有的样式表的插入、移除做整个 CSSOM 的重构,从而达到 插入、卸载 样式的目的。这样就可以保证,在一个时间点里,只有一个应用的样式表是生效的。这也是 qiankun 默认选择的 css 隔离方式。
# 或者 npm i qiankun -S
$ yarn add qiankun
import { registerMicroApps, loadMicroApp, start } from 'qiankun';
registerMicroApps([
{
name: 'react app', // app name registered
entry: '//localhost:7100',
container: '#yourContainer',
activeRule: '/yourActiveRule',
},
{
name: 'vue app',
entry: { scripts: ['//localhost:7100/main.js'] },
container: '#yourContainer2',
activeRule: '/yourActiveRule2',
},
]);
// 如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式:
loadMicroApp({
name: 'app',
entry: '//localhost:7100',
container: '#yourContainer',
});
start();
微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrap
、mount
、unmount
三个生命周期钩子,以供主应用在适当的时机调用。
/**
bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('react app bootstraped');
}
/**
应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root'),
);
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}
webpack:
const packageName = require('./package.json').name;
module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
};
// 父应用
import { initGlobalState } from 'qiankun';
// 定义全局状态,并返回通信方法
const { onGlobalStateChange, setGlobalState } = initGlobalState({
user: 'qiankun',
});
// 监听全局状态
onGlobalStateChange((value, prev) =>
// // value: 变更后的状态; prev 变更前的状态
console.log('[onGlobalStateChange - master]:', value, prev
));
// 修改全局状态(触发监听事件)
setGlobalState({
ignore: 'master',
user: {
name: 'master',
},
});
// 子应用
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
props.setGlobalState(state);
}
demo 如下视频
需要对大型工程进行拆分解耦
较低成本的应用改造
在老项目中使用新的前端技术栈
对产品体验有高要求
开发无感知
LBG开源项目推广:
还在手写 HTML 和 CSS 吗?
还在写布局吗?
快用 Picasso(毕加索) 吧!
一键生成高可用的前端代码,让你有更多的时间去沉淀和成长
开源项目地址:https://github.com/wuba/Picasso (欢迎Star)
官网地址:https://picassoui.58.com