参考文档
一、介绍
qiankun
是基于 single-spa
做了二次封装的微前端框架,通过解决了 single-spa
的一些弊端和不足,来帮助大家实现更简单、无痛的构建一个生产可用的微前端架构系统。
1.1 qiankun的优点/特点
1.HTML Entry
qiankun
通过 HTML Entry
的方式来解决 JS Entry
带来的问题,让你接入微应用像使用iframe 一样简单。
2.样式隔离
qiankun 实现了两种样式隔离
严格的样式隔离模式,为每个微应用的容器包裹上一个 shadow dom
节点,从而确保微应用的样式不会对全局造成影响
实验性的方式,通过动态改写 css 选择器来实现,可以理解为 css scoped
的方式
3.运行时沙箱
qiankun
的运行时沙箱分为 JS 沙箱
和 样式沙箱
JS 沙箱为每个微应用生成单独的 window proxy 对象,配合 HTML Entry 提供的 JS 脚本执行器 (execScripts) 来实现 JS 隔离;
样式沙箱 通过重写 DOM 操作方法,来劫持动态样式和 JS 脚本的添加,让样式和脚本添加到正确的地方,即主应用的插入到主应用模版内,微应用的插入到微应用模版,并且为劫持的动态样式做了 scoped css 的处理,为劫持的脚本做了 JS 隔离的处理,更加具体的内容可继续往下阅读或者直接阅读 qiankun 2.x 运行时沙箱 源码分析
4.资源预加载
qiankun
实现预加载的思路有两种,一种是当主应用执行 start
方法启动 qiankun
以后立即去预加载微应用的静态资源,另一种是在第一个微应用挂载以后预加载其它微应用的静态资源,这个是利用 single-spa
提供的 single-spa:first-mount
事件来实现的
5.应用间通信
qiankun
通过发布订阅模式来实现应用间通信,状态由框架来统一维护,每个应用在初始化时由框架生成一套通信方法,应用通过这些方法来更改全局状态和注册回调函数,全局状态发生改变时触发各个应用注册的回调函数执行,将新旧状态传递到所有应用
二、源码解读
2.1 框架目录结构
从 github 克隆项目以后,执行一下命令:
- 安装 qiankun 框架所需的包
yarn install
- 安装示例项目的包
yarn examples:install
以上命令执行结束以后:
2.2 有料的 package.json
-
npm-run-all
一个 CLI 工具,用于并行或顺序执行多个 npm 脚本
-
father-build
基于 rollup 的库构建工具,father 更加强大
- 多项目的目录组织以及 scripts 部分的编写
- main 和 module 字段
标识组件库的入口,当两者同时存在时,module 字段的优先级高于 main
2.3 示例项目中的主应用
这里需要更改一下示例项目中主应用的 webpack
配置
{
...
devServer: {
// 从 package.json 中可以看出,启动示例项目时,主应用执行了两条命令,其实就是启动了两个主应用,但是却只配置了一个端口,浏览器打开 localhost:7099 和你预想的有一些出入,这时显示的是 loadMicroApp(手动加载微应用) 方式的主应用,基于路由配置的主应用没起来,因为端口被占用了
// port: '7099'
// 这样配置,手动加载微应用的主应用在 7099 端口,基于路由配置的主应用在 7088 端口
port: process.env.MODE === 'multiple' ? '7099' : '7088'
}
...
}
2.4 启动示例项目
yarn examples:start
命令执行结束以后,访问 localhost:7099
和 localhost:7088
两个地址,可以看到如下内容:
到这一步,就证明项目正式跑起来了,所有准备工作就绪
三、示例项目
官方为我们准备了两种主应用的实现方式,五种微应用的接入示例,覆盖面可以说是比较广了,足以满足大家的普遍需要了
3.1 主应用
主应用在 examples/main
目录下,提供了两种实现方式,基于路由配置的 registerMicroApps
和 手动加载微应用的 loadMicroApp
。主应用很简单,就是一个从 0 通过 webpack 配置的一个同时支持 react 和 vue 的项目,至于为什么同时支持 react 和 vue,继续往下看
3.1.1 webpack.config.js
就是一个普通的 webpack
配置,配置了一个开发服务器 devServer
、两个 loader
(babel-loader、css loader)、一个插件 HtmlWebpackPlugin
(告诉 webpack html 模版文件是哪个)
通过 webpack
配置文件的 entry
字段得知入口文件分别为 index.js
和 multiple.js
3.1.2 基于路由配置
通用将微应用关联到一些 url
规则的方式,实现当浏览器 url
发生变化时,自动加载相应的微应用的功能
3.1.2.1 index.js
// qiankun api 引入
import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from '../../es';
// 全局样式
import './index.less';
// 专门针对 angular 微应用引入的一个库
import 'zone.js';
/**
* 主应用可以使用任何技术栈,这里提供了 react 和 vue 两种,可以随意切换
* 最终都导出了一个 render 函数,负责渲染主应用
*/
// import render from './render/ReactRender';
import render from './render/VueRender';
// 初始化主应用,其实就是渲染主应用
render({ loading: true });
// 定义 loader 函数,切换微应用时由 qiankun 框架负责调用显示一个 loading 状态
const loader = loading => render({ loading });
// 注册微应用
registerMicroApps(
// 微应用配置列表
[
{
// 应用名称
name: 'react16',
// 应用的入口地址
entry: '//localhost:7100',
// 应用的挂载点,这个挂载点在上面渲染函数中的模版里面提供的
container: '#subapp-viewport',
// 微应用切换时调用的方法,显示一个 loading 状态
loader,
// 当路由前缀为 /react16 时激活当前应用
activeRule: '/react16',
},
{
name: 'react15',
entry: '//localhost:7102',
container: '#subapp-viewport',
loader,
activeRule: '/react15',
},
{
name: 'vue',
entry: '//localhost:7101',
container: '#subapp-viewport',
loader,
activeRule: '/vue',
},
{
name: 'angular9',
entry: '//localhost:7103',
container: '#subapp-viewport',
loader,
activeRule: '/angular9',
},
{
name: 'purehtml',
entry: '//localhost:7104',
container: '#subapp-viewport',
loader,
activeRule: '/purehtml',
},
],
// 全局生命周期钩子,切换微应用时框架负责调用
{
beforeLoad: [
app => {
// 这个打印日志的方法可以学习一下,第三个参数会替换掉第一个参数中的 %c%s,并且第三个参数的颜色由第二个参数决定
console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
},
],
beforeMount: [
app => {
console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
},
],
afterUnmount: [
app => {
console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
},
],
},
);
// 定义全局状态,并返回两个通信方法
const { onGlobalStateChange, setGlobalState } = initGlobalState({
user: 'qiankun',
});
// 监听全局状态的更改,当状态发生改变时执行回调函数
onGlobalStateChange((value, prev) => console.log('[onGlobalStateChange - master]:', value, prev));
// 设置新的全局状态,只能设置一级属性,微应用只能修改已存在的一级属性
setGlobalState({
ignore: 'master',
user: {
name: 'master',
},
});
// 设置默认进入的子应用,当主应用启动以后默认进入指定微应用
setDefaultMountApp('/react16');
// 启动应用
start();
// 当第一个微应用挂载以后,执行回调函数,在这里可以做一些特殊的事情,比如开启一监控或者买点脚本
runAfterFirstMounted(() => {
console.log('[MainApp] first app mounted');
});
3.1.2.2 VueRender.js
/**
* 导出一个由 vue 实现的渲染函数,渲染了一个模版,模版里面包含一个 loading 状态节点和微应用容器节点
*/
import Vue from 'vue/dist/vue.esm';
// 返回一个 vue 实例
function vueRender({ loading }) {
return new Vue({
template: `
<div id="subapp-container">
<h4 v-if="loading" class="subapp-loading">Loading...</h4>
<div id="subapp-viewport"></div>
</div>
`,
el: '#subapp-container',
data() {
return {
loading,
};
},
});
}
// vue 实例
let app = null;
// 渲染函数
export default function render({ loading }) {
// 单例,如果 vue 实例不存在则实例化主应用,存在则说明主应用已经渲染,需要更新主营应用的 loading 状态
if (!app) {
app = vueRender({ loading });
} else {
app.loading = loading;
}
}
3.1.2.3 ReactRender.js
/**
* 同 vue 实现的渲染函数,这里通过 react 实现了一个一样的渲染函数
*/
import React from 'react';
import ReactDOM from 'react-dom';
// 渲染主应用
function Render(props) {
const { loading } = props;
return (
<>
{loading && <h4 className="subapp-loading">Loading...</h4>}
<div id="subapp-viewport" />
</>
);
}
// 将主应用渲染到指定节点下
export default function render({ loading }) {
const container = document.getElementById('subapp-container');
ReactDOM.render(<Render loading={loading} />, container);
}
3.1.3 手动加载微应用
3.1.3.1 multiple.js
/**
* 调用 loadMicroApp 方法注册了两个微应用
*/
import { loadMicroApp } from '../../es';
const app1 = loadMicroApp(
// 应用配置,名称、入口地址、容器节点
{ name: 'react15', entry: '//localhost:7102', container: '#react15' },
// 可以添加一些其它的配置,比如:沙箱、样式隔离等
{
sandbox: {
// strictStyleIsolation: true,
},
},
);
const app2 = loadMicroApp(
{ name: 'vue', entry: '//localhost:7101', container: '#vue' },
{
sandbox: {
// strictStyleIsolation: true,
},
},
);
3.2 vue
vue 微应用在 examples/vue
目录下,就是一个通过 vue-cli 创建的 vue demo 应用,然后对 vue.config.js
和 main.js
做了一些更改
3.2.1 vue.config.js
一个普通的 webpack
配置,需要注意的地方就三点
{
...
// publicPath 没在这里设置,是通过 webpack 提供的全局变量 __webpack_public_path__ 来即时设置的,webpackjs.com/guides/public-path/
devServer: {
...
// 设置跨域,因为主应用需要通过 fetch 去获取微应用引入的静态资源的,所以必须要求这些静态资源支持跨域
headers: {
'Access-Control-Allow-Origin': '*',
},
},
output: {
// 把子应用打包成 umd 库格式
library: `${name}-[name]`, // 库名称,唯一
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
}
...
}
3.2.2 main.js
// 动态设置 __webpack_public_path__
import './public-path';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
// 路由配置
import routes from './router';
import store from './store';
Vue.config.productionTip = false;
Vue.use(ElementUI);
let router = null;
let instance = null;
// 应用渲染函数
function render(props = {}) {
const { container } = props;
// 实例化 router,根据应用运行环境设置路由前缀
router = new VueRouter({
// 作为微应用运行,则设置 /vue 为前缀,否则设置 /
base: window.__POWERED_BY_QIANKUN__ ? '/vue' : '/',
mode: 'history',
routes,
});
// 实例化 vue 实例
instance = new Vue({
router,
store,
render: h => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
// 支持应用独立运行
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
/**
* 从 props 中获取通信方法,监听全局状态的更改和设置全局状态,只能操作一级属性
* @param {*} props
*/
function storeTest(props) {
props.onGlobalStateChange &&
props.onGlobalStateChange(
(value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
true,
);
props.setGlobalState &&
props.setGlobalState({
ignore: props.name,
user: {
name: props.name,
},
});
}
/**
* 导出的三个生命周期函数
*/
// 初始化
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
// 挂载微应用
export async function mount(props) {
console.log('[vue] props from main framework', props);
storeTest(props);
render(props);
}
// 卸载、销毁微应用
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
}
3.2.3 public-path.js
/**
* 在入口文件中使用 ES6 模块导入,则在导入后对 __webpack_public_path__ 进行赋值。
* 在这种情况下,必须将公共路径(public path)赋值移至专属模块,然后将其在最前面导入
*/
// qiankun 设置的全局变量,表示应用作为微应用在运行
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
3.3 jQuery
这是一个使用了 jQuery 的项目,在 examples/purehtml
目录下,展示了如何接入使用 jQuery 开发的应用
3.3.1 package.json
为了达到演示效果,使用 http-server
在起了一个本地服务器,并且支持跨域
{
...
"scripts": {
"start": "cross-env PORT=7104 http-server . --cors",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
}
3.3.2 entry.js
// 渲染函数
const render = $ => {
$('#purehtml-container').html('Hello, render with jQuery');
return Promise.resolve();
};
// 在全局对象上导出三个生命周期函数
(global => {
global['purehtml'] = {
bootstrap: () => {
console.log('purehtml bootstrap');
return Promise.resolve();
},
mount: () => {
console.log('purehtml mount');
// 调用渲染函数
return render($);
},
unmount: () => {
console.log('purehtml unmount');
return Promise.resolve();
},
};
})(window);
3.3.3 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Purehtml Example</title>
<script src="//cdn.bootcss.com/jquery/3.4.1/jquery.min.js">
</script>
</head>
<body>
<div style="display: flex; justify-content: center; align-items: center; height: 200px;">
Purehtml Example
</div>
<div id="purehtml-container" style="text-align:center"></div>
<!-- 引入 entry.js,相当于 vue 项目的 publicPath 配置 -->
<script src="//localhost:7104/entry.js" entry></script>
</body>
</html>
3.4 angular 9、react 15、react 16
这三个实例项目就不一一分析了,和 vue 项目类似,都是配置打包工具将微应用打包成一个 umd 格式,然后配置应用入口文件 和 路由前缀
3.5 UmiJS
UmiJS
有自己独立支持qiankun
的插件@umijs/plugin-qiankun
yarn add @umijs/plugin-qiankun -D
- 配置
qiankun
开启。
3.5.1 主应用配置
第一步:注册子应用:
子应用注册有两种方式,二选一即可
a. 插件构建期配置子应用
export default {
qiankun: {
master: {
// 注册子应用信息
apps: [
{
name: 'app1', // 唯一 id
entry: '//localhost:7001', // html entry
},
{
name: 'app2', // 唯一 id
entry: '//localhost:7002', // html entry
},
],
},
},
};
b. 运行时动态配置子应用(src/app.ts 里开启)
// 从接口中获取子应用配置,export 出的 qiankun 变量是一个 promise
export const qiankun = fetch('/config').then(({ apps }) => ({
// 注册子应用信息
apps,
// 完整生命周期钩子请看 https://qiankun.umijs.org/zh/api/#registermicroapps-apps-lifecycles
lifeCycles: {
afterMount: (props) => {
console.log(props);
},
},
// 支持更多的其他配置,详细看这里 https://qiankun.umijs.org/zh/api/#start-opts
}));
完整的主应用配置项看这里 masterOptions 配置列表
第二步:装载子应用
子应用的装载有两种方式,二选一即可:
a. 使用路由绑定的方式(建议使用这种方式来引入自带路由的子应用。)
在 /app1/project 和 /app2 这两个路径时分别加载微应用 app1 和 app2,只需要增加这样一些配置即可:
export default {
routes: [
{
path: '/',
component: '../layouts/index.js',
routes: [
{
path: '/app1',
component: './app1/index.js',
routes: [
{
path: '/app1/user',
component: './app1/user/index.js',
},
+ // 配置微应用 app1 关联的路由
+ {
+ path: '/app1/project',
+ microApp: 'app1',
+ },
],
},
+ // 配置 app2 关联的路由
+ {
+ path: '/app2',
+ microApp: 'app2'
+ },
{
path: '/',
component: './index.js',
},
],
},
],
}
微应用路由也可以配置在运行时,通过 src/app.ts
添加:
export const qiankun = fetch('/config').then(({ apps }) => {
return {
apps,
routes: [
{
path: '/app1',
microApp: 'app1',
},
],
};
});
运行时注册的路由会自动关联到你配置的根路由下面:
export default {
routes: [
{
path: '/',
component: '../layouts/index.js',
routes: [
+ {
+ path: '/app1',
+ microApp: 'app1',
+ },
{
path: '/test',
component: './test.js',
},
],
},
]
}
b. 使用 <MicroApp />
组件的方式(建议使用这种方式来引入不带路由的子应用。)
我们可以直接使用 React 标签的方式加载我们已注册过的子应用:
import { MicroApp } from 'umi';
export function MyPage() {
return (
<div>
<div>
+ <MicroApp name="app1" />
</div>
</div>
)
}
可以通过配置 autoSetLoading
的方式,开启微应用的 loading 动画。
import { MicroApp } from 'umi';
export function MyPage() {
return (
<div>
<div>
<MicroApp name="app1" autoSetLoading />
</div>
</div>
);
}
3.5.2 子应用配置
第一步:插件注册(config.js)
export default {
qiankun: {
slave: {},
},
};
第二步:配置运行时生命周期钩子(可选)
插件会自动为你创建好 qiankun
子应用需要的生命周期钩子,但是如果你想在生命周期期间加一些自定义逻辑,可以在子应用的 src/app.ts
里导出 qiankun
对象,并实现每一个生命周期钩子,其中钩子函数的入参 props
由主应用自动注入。
export const qiankun = {
// 应用加载之前
async bootstrap(props) {
console.log('app1 bootstrap', props);
},
// 应用 render 之前触发
async mount(props) {
console.log('app1 mount', props);
},
// 应用卸载之后触发
async unmount(props) {
console.log('app1 unmount', props);
},
};
环境变量配置
建议您提前在子应用中指定应用启动的具体端口号,如通过.env
指定
PORT=8081
3.5.3 父子应用通讯
3.5.3.1 配合 useModel 使用(推荐)
需确保已安装 @umijs/plugin-model
或 @umijs/preset-react
主应用使用下面任一方式透传数据:
-
如果你用的 MicroApp
组件模式消费微应用,那么数据传递的方式就跟普通的 react
组件通信是一样的,直接通过 props
传递即可:
function MyPage() {
const [name, setName] = useState(null);
return (
<MicroApp name={name} onNameChange={(newName) => setName(newName)} />
);
}
-
如果你用的 路由绑定式
消费微应用,那么你需要在 src/app.ts
里导出一个 useQiankunStateForSlave
函数,函数的返回值将作为 props
传递给微应用,如:
// src/app.ts
export function useQiankunStateForSlave() {
const [masterState, setMasterState] = useState({});
return {
masterState,
setMasterState,
};
}
微应用中会自动生成一个全局 model
,可以在任意组件中获取主应用透传的 props
的值。
import { useModel } from 'umi';
function MyPage() {
const masterProps = useModel('@@qiankunStateFromMaster');
return <div>{JSON.stringify(masterProps)}</div>;
}
或者可以通过高阶组件 connectMaster
来获取主应用透传的 props
import { connectMaster } from 'umi';
function MyPage(props) {
return <div>{JSON.stringify(props)}</div>;
}
export default connectMaster(MyPage);
和 <MicroApp />
的方式一同使用时,会额外向子应用传递一个 setLoading
的属性,在子应用中合适的时机执行 masterProps.setLoading(false)
,可以标记微模块的整体 loading
为完成状态。
3.5.3.2 基于 props 传递
主应用中配置 apps 时以 props 将数据传递下去(参考主应用运行时配置一节)
// src/app.js
export const qiankun = fetch('/config').then((config) => {
return {
apps: [
{
name: 'app1',
entry: '//localhost:2222',
props: {
onClick: (event) => console.log(event),
name: 'xx',
age: 1,
},
},
],
};
});
子应用在生命周期钩子中获取 props 消费数据(参考子应用运行时配置一节)
3.5.4 嵌套子应用
除了导航应用之外,App1 与 App2 均依赖浏览器 url
,为了让 App1 嵌套 App2,两个应用同时存在,我们需要在运行时将 App2 的路由改为 memory
类型。
- 在 App1 中加入 master 配置
export default {
qiankun: {
master: {
// 注册子应用信息
apps: [
{
name: 'app2', // 唯一 id
entry: '//localhost:7002', // html entry
},
],
},
},
};
- 通过
<MicroAppWithMemoHistory />
引入 App2
import { MicroAppWithMemoHistory } from 'umi';
export function MyPage() {
return (
<div>
<div>
+ <MicroAppWithMemoHistory name="app2" url="/user" />
</div>
</div>
)
}