主要研究对象qiankun,探究内部原理
乾坤qiankun简介
1. 安装
$ yarn add qiankun # 或者 npm i qiankun -S
2. 主应用注册
registerMicroApps([
{
name: 'reactApp',
entry: '//localhost:3000',
container: '#container',
activeRule: '/views/app-react',
},
{
name: 'vueApp',
entry: '//localhost:8080',
container: '#container',
activeRule: '/views/app-vue',
},
{
name: 'reactApp2',
entry: 'http://localhost:9100/react-app2/template/index.html',
container: '#container',
activeRule: '/views/app-react2',
}
]);
// 启动 qiankun
start();
当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。如果子应用还存在子路由,则需要配置子应用路由的basename
3. 子应用的改造
导出生命周期钩子
微应用需要在自己的入口 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配置
- 新增
public-path.js文件,并在入口文件最顶部引用,用于修改运行时的 publicPath - 允许开发环境跨域和 umd 打包,修改 webpack 打包:
const packageName = require('./package.json').name;
module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
};
关于
public-path的描述可以参见此文档:webpack.docschina.org/guides/publ…关于
library的描述可以参见此文档:webpack.docschina.org/guides/auth…
single-spa的使用
single-spa主要功能是实现了路由劫持与应用加载,但未实现js、css隔离
乾坤qiankun的原理
在研究qiankun的原理之前,我们应该试着提出一些问题,然后在阅读源码的过程中去寻找答案。对于qiankun,我们比较关心的问题有:
qiankun与single-spa是什么关系?qiankun如何实现js沙箱?qiankun如何实现css隔离?
registerMicroApps
配置微应用的第一步就是在主应用中注册子应用,配置好子应用的名称、入口、dom容器以及唤醒条件,之后qiankun会按照唤醒条件将子应用挂载到dom容器上。
- 用法:registerMicroApps(apps, lifeCycles?)
- 参数:
- apps -
Array<RegistrableApp>- 必选,微应用的一些注册信息 - lifeCycles -
LifeCycles- 可选,全局的微应用生命周期钩子
- apps -
- 详解
- RegistrableApp
- name -
string- 必选,微应用的名称,微应用之间必须确保唯一 - entry -
string | { scripts?: string[]; styles?: string[]; html?: string }- 必选,微应用的入口 - container -
string | HTMLElement- 必选,微应用的容器节点的选择器或者Element实例。如container: '#root'或container: document.querySelector('#root') - activeRule -
string | (location: Location) => boolean | Array<string | (location: Location) => boolean>- 必选,微应用的激活规则 - loader -
(loading: boolean) => void- 可选,loading状态发生变化时会调用的方法 - props -
object- 可选,主应用需要传递给微应用的数据
- name -
- LifeCycles
- beforeLoad -
Lifecycle | Array<Lifecycle>- 可选 - beforeMount -
Lifecycle | Array<Lifecycle>- 可选 - afterMount -
Lifecycle | Array<Lifecycle>- 可选 - beforeUnmount -
Lifecycle | Array<Lifecycle>- 可选 - afterUnmount -
Lifecycle | Array<Lifecycle>- 可选
- beforeLoad -
- RegistrableApp
- 参数:
- 示例
import { registerMicroApps } from 'qiankun';
registerMicroApps([{
name: 'app1',
entry: '//localhost:8080',
container: '#container',
activeRule: '/react',
props: {
name: 'kuitos'
}
}], {
beforeLoad: app => console.log('before load', app.name),
beforeMount: [
app => console.log('before mount', app.name)
]
});
- 实现原理:背后调用
single-spa的registerApplication方法,此处主要工作在registerApplication的app参数中处理:
registerApplication({
name,
app: async () => {
loader(true);
await frameworkStartedDefer.promise;
const { mount, ...otherMicroAppConfigs } = (
await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
)();
return {
mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
...otherMicroAppConfigs,
};
},
activeWhen: activeRule,
customProps: props,
});
其中loadApp是核心,重新包装了single-spa所需applicat的mount、unmount方法,在这两个生命周期中分别加入了一些额外的操作。如在mount的时候,会执行beforeMount、afterMount回调,以及mountSandbox开启沙箱。在unmount的时候,会执行beforeUnmount、afterUnmount回调,以及unmountSandbox释放沙箱。
-
qiankun是如何实现沙箱的?
通过调用
createSandboxContainer生成一个沙箱,如果当前浏览器环境支持Proxy则采用Proxy,反之则采用快照方式。对于Proxy模式来说,首先调用createFakeWindow(rawWindow)生成一个原始window的复制版fakeWindow
loadApp函数原理
执行用户调用registerApplication时传入的app方法,加载应用
- 调用
import-html-entry模块的importEntry函数,获取到对应子应用的html文件、可执行脚本文件以及publicpath - 调用
getDefaultTplWrapper将子应用的html内容用div标签包裹起来 - 调用
createElement函数生成剔除html、body、head标签后的子应用html内容(通过innerHTML达到过滤效果) - 调用
getRender函数得到render函数 - 调用第4步得到的render,将container内部清空,并将子应用的dom元素渲染到指定的contanter元素上
- 调用
getAppWrapperGetter函数,生成一个可以获取处理过的子应用dom元素的函数initialAppWrapperGetter,以备后续使用子应用dom元素 - 如果
sandbox为true,则调用createSandboxContainer函数 - 执行
execScripts函数,执行子应用脚本 - 执行
getMicroAppStateActions函数,获取onGlobalStateChange、setGlobalState、offGlobalStateChange,用于主子应用传递信息 - 执行
parcelConfigGetter函数,包装mount和unmount
createSandboxContainer函数原理
registerApplication函数原理
- 规则化参数,registerApplication参数有两种形式,首先将不同的参数形式归结为同一种格式
{name,loadApp,activeWhen,customProps} - 检测注册的
appName是否重名,重名则抛出异常 - 将
application配置推入到全局数组apps中保存 - 如果是浏览器环境(以是否存在window变量作为依据),则:
- 确保在引入了
jQuery的前提前下window.jQuery能够使用 - 执行
reroute函数,并进行下列操作:- 遍历
apps数组,获取到NOT_MOUNTED、MOUNTED、LOADING_SOURCE_CODE的application - 检测是否执行了
start方法,如果已经执行了(例如在页面上切换路由),则执行performAppChanges函数,反之执行loadApps函数(首次刷新页面时)
- 遍历
- 确保在引入了
performAppChanges函数原理
利用Promise.resolve().then()将函数内的全部代码异步执行
- 利用
window.dispatchEvent和CustomEvent触发single-spa:before-no-app-change或single-spa:before-app-change以及single-spa:before-routing-event事件(这两个接口IE不支持) - 执行队列中
app的mount和unmount操作(具体操作有待进一步研究)
start函数原理
启动 qiankun,可以传入一些资源获取方式以及是否采用沙箱等配置
- 用法参见:qiankun.umijs.org/zh/api#star…
- 实现原理:背后调用
single-spa的start方法,执行reroute或setUrlRerouteOnly函数