参考文档
qiankun
是基于 single-spa
做了二次封装的微前端框架,通过解决了 single-spa
的一些弊端和不足,来帮助大家实现更简单、无痛的构建一个生产可用的微前端架构系统。
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
通过发布订阅模式来实现应用间通信,状态由框架来统一维护,每个应用在初始化时由框架生成一套通信方法,应用通过这些方法来更改全局状态和注册回调函数,全局状态发生改变时触发各个应用注册的回调函数执行,将新旧状态传递到所有应用
从 github 克隆项目以后,执行一下命令:
yarn install
yarn examples:install
这里需要更改一下示例项目中主应用的 webpack
配置
{
...
devServer: {
// 从 package.json 中可以看出,启动示例项目时,主应用执行了两条命令,其实就是启动了两个主应用,但是却只配置了一个端口,浏览器打开 localhost:7099 和你预想的有一些出入,这时显示的是 loadMicroApp(手动加载微应用) 方式的主应用,基于路由配置的主应用没起来,因为端口被占用了
// port: '7099'
// 这样配置,手动加载微应用的主应用在 7099 端口,基于路由配置的主应用在 7088 端口
port: process.env.MODE === 'multiple' ? '7099' : '7088'
}
...
}
yarn examples:start
命令执行结束以后,访问 localhost:7099
和 localhost:7088
两个地址,可以看到如下内容:
到这一步,就证明项目正式跑起来了,所有准备工作就绪
官方为我们准备了两种主应用的实现方式,五种微应用的接入示例,覆盖面可以说是比较广了,足以满足大家的普遍需要了
主应用在 examples/main
目录下,提供了两种实现方式,基于路由配置的 registerMicroApps
和 手动加载微应用的 loadMicroApp
。主应用很简单,就是一个从 0 通过 webpack 配置的一个同时支持 react 和 vue 的项目,至于为什么同时支持 react 和 vue,继续往下看
就是一个普通的 webpack
配置,配置了一个开发服务器 devServer
、两个 loader
(babel-loader、css loader)、一个插件 HtmlWebpackPlugin
(告诉 webpack html 模版文件是哪个)
通过 webpack
配置文件的 entry
字段得知入口文件分别为 index.js
和 multiple.js
通用将微应用关联到一些 url
规则的方式,实现当浏览器 url
发生变化时,自动加载相应的微应用的功能
// 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');
});
/**
* 导出一个由 vue 实现的渲染函数,渲染了一个模版,模版里面包含一个 loading 状态节点和微应用容器节点
*/
import Vue from 'vue/dist/vue.esm';
// 返回一个 vue 实例
function vueRender({ loading }) {
return new Vue({
template: `
Loading...
`,
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;
}
}
/**
* 同 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);
}
/**
* 调用 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,
},
},
);
vue 微应用在 examples/vue
目录下,就是一个通过 vue-cli 创建的 vue demo 应用,然后对 vue.config.js
和 main.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}`,
}
...
}
// 动态设置 __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;
}
/**
* 在入口文件中使用 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__;
}
这是一个使用了 jQuery 的项目,在 examples/purehtml
目录下,展示了如何接入使用 jQuery 开发的应用
为了达到演示效果,使用 http-server
在起了一个本地服务器,并且支持跨域
{
...
"scripts": {
"start": "cross-env PORT=7104 http-server . --cors",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
}
// 渲染函数
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);
<!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>
这三个实例项目就不一一分析了,和 vue 项目类似,都是配置打包工具将微应用打包成一个 umd 格式,然后配置应用入口文件 和 路由前缀
UmiJS
有自己独立支持qiankun
的插件@umijs/plugin-qiankun
yarn add @umijs/plugin-qiankun -D
qiankun
开启。子应用注册有两种方式,二选一即可
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. 使用
组件的方式(建议使用这种方式来引入不带路由的子应用。)
我们可以直接使用 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>
);
}
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
需确保已安装
@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);
和
的方式一同使用时,会额外向子应用传递一个 setLoading
的属性,在子应用中合适的时机执行 masterProps.setLoading(false)
,可以标记微模块的整体 loading
为完成状态。
主应用中配置 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 消费数据(参考子应用运行时配置一节)
除了导航应用之外,App1 与 App2 均依赖浏览器 url
,为了让 App1 嵌套 App2,两个应用同时存在,我们需要在运行时将 App2 的路由改为 memory
类型。
export default {
qiankun: {
master: {
// 注册子应用信息
apps: [
{
name: 'app2', // 唯一 id
entry: '//localhost:7002', // html entry
},
],
},
},
};
引入 App2import { MicroAppWithMemoHistory } from 'umi';
export function MyPage() {
return (
<div>
<div>
+ <MicroAppWithMemoHistory name="app2" url="/user" />
</div>
</div>
)
}