qiankun 是一个基于 single-spa 的微前端实现库
,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
什么是微前端
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
微前端架构具备以下几个核心价值:
-
技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权 -
独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新 -
增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略 -
独立运行时
每个微应用之间状态隔离,运行时状态不共享
主应用搭建
1. 安装 qiankun
$ yarn add qiankun # 或者 npm i qiankun -S
2. 在主应用中注册微应用
新建 main文件 以react为主应用
<div id="subapp-viewport" />
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);
}
- 独立安装zone.js
npm i zone.js -S
import 'zone.js'; // for angular subapp
import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from 'qiankun';
import './index.less';
/**
* 主应用 **可以使用任意技术栈**
* 以下分别是 React 和 Vue 的示例,可切换尝试
*/
import render from './render/ReactRender';
// import render from './render/VueRender';
/**
* Step1 初始化应用(可选)
*/
render({ loading: true });
const loader = loading => render({ loading });
/**
* Step2 注册子应用
*/
registerMicroApps(
[
{
name: 'react16', // 应用名称
entry: '//localhost:7100', // 应用地址
container: '#subapp-viewport', // 嵌入主应用位置
loader, //状态发生变化时会调用的方法
activeRule: '/react16', // 匹配规则
props: {
aa: 11,
}, // 主应用需要传递给微应用的数据
},
{
name: 'vue3',
entry: '//localhost:7105',
container: '#subapp-viewport',
loader,
activeRule: '/vue3',
props: {
aa: 12,
},
},
],
{
beforeLoad: [
app => {
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',
},
});
/**
* Step3 设置默认进入的子应用
*/
setDefaultMountApp('/react16');
/**
* Step4 启动应用
*/
start();
runAfterFirstMounted(() => {
console.log('[MainApp] first app mounted');
});
当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式:
import { loadMicroApp } from 'qiankun';
loadMicroApp({
name: 'app',
entry: '//localhost:7100',
container: '#yourContainer',
});
子应用搭建
微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。
1. 导出相应的生命周期钩子
微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出
bootstrap、mount、unmount
三个生命周期钩子,以供主应用在适当的时机调用。
react 子项目
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
function render(props) {
const { container } = props;
ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
}
function storeTest(props) {
props.onGlobalStateChange((value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev), true);
props.setGlobalState({
ignore: props.name,
user: {
name: props.name,
},
});
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap(props) {
console.log('[react16] react app bootstraped',props);
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log('[react16] props from main framework', props);
storeTest(props);
render(props);
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}
vue 子项目
import './public-path';
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import routes from './router';
import store from './store';
let router = null;
let instance = null;
let history = null;
function render(props = {}) {
const { container } = props;
history = createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/vue3' : '/');
router = createRouter({
history,
routes,
});
instance = createApp(App);
instance.use(router);
instance.use(store);
instance.mount(container ? container.querySelector('#app') : '#app');
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap(props) {
console.log('%c%s', 'color: green;', 'vue3.0 app bootstraped',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 mount(props) {
storeTest(props);
render(props);
instance.config.globalProperties.$onGlobalStateChange = props.onGlobalStateChange;
instance.config.globalProperties.$setGlobalState = props.setGlobalState;
}
export async function unmount() {
instance.unmount();
instance._container.innerHTML = '';
instance = null;
router = null;
history.destroy();
}
3. 添加public-path.js文件,并引入子应用index.js/main.js
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
qiankun 基于 single-spa,所以你可以在这里找到更多关于微应用生命周期相关的文档说明。
2. 配置微应用的打包工具
除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:
webpack:
const packageName = require('./package.json').name;
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
}
},
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
};
效果图
通信
浏览器 api
由于qiankunshi采用HTML Entry,localStrage、cookie可共享。
qiankun api
props
主应用通过
props
,传递给子应用
{
name: 'react16', // 应用名称
entry: '//localhost:7100', // 应用地址
container: '#subapp-viewport', // 嵌入主应用位置
loader,
activeRule: '/react16', // 匹配规则
props: {
aa: 11,
},
},
- 子应用在生命周期中获取
export async function bootstrap(props) {
console.log(props)
}
export async function mount(props) {
console.log(props)
}
export async function unmount(props) {
console.log(props)
}
initGlobalState
主应用通过
initGlobalState
传递,并监听
// 初始化 state
const actions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log("main-onGlobalStateChange", state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();
- 子应用在生命周期中接收
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
props.setGlobalState(state);
}
注意坑点:
- 子应用必须支持跨域
- react 需要设置public-path的文件,保证子应用的正常加载
- angluar子应用注意zone.js与乾坤框架的兼容问题
- anluar子应用安装single-spa-angular后,需要独立安装zone.js依赖,并引入
- 子应用嵌套、低版本chrome浏览器需要注意library配置修改为packageNmae
- 主、子应用根元素id保持唯一,建议以各自项目名称设置