一、介绍
1、什么是qiankun
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。(官方说的)
正所谓乾坤无偏奉一体,万象森罗尽在其中。在我看来qiankun就是一个乾坤百宝袋(主应用),可以把很多应用(微应用)都装进来,每个应用都可以独立开发,独立部署,技术栈自主。 有了qiankun企业的业务就可以实现真正的增量升级,将一个业务量庞大的系统拆分成多个自主独立的模块,每个微应用独立运行,解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用后,随之而来的应用不可维护的问题。(再也不用担心程序猿们在屎山修改业务)
2、qiankun的特性
技术栈无关:微应用可以使用任何技术栈,只需和主应用连接皆可实现开箱即用。
独立开发,独立部署:统一系统的不同应用可由不同团队独立开发,大大节省项目开发周期。
增量升级:在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略。
独立运行:每个微应用之间状态隔离,运行时状态不共享。
资源预加载:在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
3、qiankun vs iframe
为什么不使用iframe这种原生的更为简便的方案而要另辟蹊径的使用qiankun呢?
原因在于iframe最大的的问题在于他的隔离性太强了,很难对其进行突破,导致应用上下文很难被共享,这带来了很多开发体验和产品体验的问题。这也是为什么不用iframe而去选择使用qiankun的原因。
二、qiankun搭建
1、安装qiankun
主应用和微应用都要进行安装哟~
$ yarn add qiankun # 或者 npm i qiankun -S
2、通用配置
在所有应用中安装插件 react-app-rewired
npm install react-app-rewired --save-dev 或 yarn add react-app-rewired --dev
在package.json文件中修改启动脚本
json
"scripts": {
"start": "react-app-rewired start",
},
在根目录新增文件config-overrides.js
javascript
const { name } = require('./package');
module.exports = {
webpack: (config) => {
//将微应用打包成 umd 模块时,设置输出的全局变量名。
config.output.library = `${name}-[name]`;
//设置打包输出的模块格式为 umd。
config.output.libraryTarget = 'umd';
// webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
// config.output.jsonpFunction = `webpackJsonp_${name}`;
// 指定在浏览器环境中使用全局对象
config.output.globalObject = 'window';
return config;
},
devServer: (_) => {
const config = _;
//为开发服务器的响应头添加 Access-Control-Allow-Origin,允许跨域请求(注意仅适用于开发环境)。
config.headers = {
'Access-Control-Allow-Origin': '*',
};
// 当路由请求404时,返回根目录index.html,常用于单页应用的路由配置。
config.historyApiFallback = true;
//禁用热模块替换(HMR),即禁止在开发服务器中热更新模块。
config.hot = false;
//禁用开发服务器的内容监视功能,这样开发服务器不会监听文件的变化。
config.watchContentBase = false;
//禁用开发服务器的实时重新加载功能。
config.liveReload = false;
return config;
},
};
qiankun 在微应用中使用 react-app-rewired
是为了能够定制 Create React App 的 webpack 配置,以适应微应用的需求。这样可以确保微应用在 qiankun 的环境中能够正常运行并集成到宿主应用中。
3、在主应用中注册微应用
js
/*主应用的index.tsx(不同项目架构有不同的注册方法,可以在任何地方注册子应用。
一般来说,我们会在主应用的入口文件或根组件中注册子应用。)*/
import { registerMicroApps, start } from 'qiankun';
const micors = [
{
name: 'react app', // 微应用的名字,自拟最好能一眼看出这个应用的功能
entry: '//localhost:7100', // 微应用的入口地址
container: '#container', // 微应用要挂载的容器id
activeRule: '/app1',// 微应用对应的路由匹配规则
},
{
name: 'vue app',
entry: { scripts: ['//localhost:7100/main.js'] },
container: '#container2',
activeRule: '/app2',
},
];
registerMicroApps(micors,{
beforeLoad: (app) => {
return Promise.resolve();
},
beforeMount: (app) => {
return Promise.resolve();
},
});
start();// 最后别忘了启动哟
当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式如下:
js
import { loadMicroApp } from 'qiankun';
loadMicroApp({
name: 'app',
entry: '//localhost:7100',
container: '#yourContainer',
});
配置微应用挂载点(这个挂载点名字要和上面的container对应)
js
// App.tsx
import { Route, BrowserRouter, Routes, Link } from "react-router-dom";
import "./App.css";
function App() {
return (
<BrowserRouter>
<div className="App">
<Link to="/app1">app1</Link>
<Link to="/app2">app2</Link>
<div id="container"></div>
<Routes>
<Route path="/app1">app1</Route>
<Route path="/app2">app2</Route>
</Routes>
</div>
</BrowserRouter>
);
}
export default App;
以上是我主应用实现路由跳转的一些方式。
4、配置微应用
在所有微应用src
目录新增 public-path.js
文件
js
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
这个文件是用于根据qiankun注入的全局变量来动态配置webpack的publicPath
属性,以确保子应用的静态资源能够正确加载。(指定静态资源路径的,不然图片那些静态资源就加载不出来。)
修改微应用入口文件index.tsx
js
// index.tsx
import "./public-path.js";//导入上一步配置的文件,用于正确加载静态资源文件
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";
export async function bootstrap() {
console.log("[react16] react app bootstraped");
}
export async function mount(props: any) {
console.log("[react16] props from main framework", props);
ReactDOM.render(
<BrowserRouter
basename={window.__POWERED_BY_QIANKUN__ ? "app1" : undefined}
>
<App />
</BrowserRouter>,
props.container
? props.container.querySelector("#root")
: document.getElementById("root")
);
}
export async function unmount(props: any) {
const { container } = props;
ReactDOM.unmountComponentAtNode(
container
? container.querySelector("#root")
: document.querySelector("#root")
);
}
// @ts-ignore
if (!window.__POWERED_BY_QIANKUN__) {
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
}
reportWebVitals();
三、主应用与子应用间的通信
在完成以上步骤之后你就能得到一个由微前端搭建起来的应用啦,当然如果仅仅只是这样这还远远不够,我们还需要实现主应用和子应用的通信。
1、qiankun自带的全局状态API
首先在主应用的入口文件index.tsx写入以下代码
js
import { initGlobalState, MicroAppStateActions } from "qiankun";
const state = {
nickname: "crazy",
};
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log("base", state, prev);
});
在注册微应用时将props注册进微应用
js
registerMicroApps([
{
name: "app1",
entry: "//localhost:3011",
container: "#container",
activeRule: "/app1",
props: { state },
},
{
name: "app2",
entry: "//localhost:3012",
container: "#container",
activeRule: "/app2",
props: { state },
},
]);
在主应用的入口文件中可以使用actions.setGlobalState()对全局变量进行修改
js
actions.setGlobalState({ ...state, nickname: "123", age: 20 });
在微应用中可以使用props.setGlobalState()对已有的state对象中的属性进行修改,但是没有权限向state对象中进行属性的添加,如果你使用props.setGlobalState({ ...state, nickname: "123" ,age:19});那么nickname中的数据会被更改,但是age这一项会被忽略无法添加到全局变量中,而主应用则可以正常添加。
js
props.setGlobalState({ ...state, nickname: "123" });
1.1局限性
如上所说qiankun自带的通信方式,子应用对全局状态中的属性只有修改的权限,但是无法向全局状态中新增或者删除属性。
2、localStorage通信
使用localStorage进行主应用与微应用间的通信是目前我觉得最直接的通信方式,不管是主应用还是微应用,使用以下两行代码都可以很快的进行数据的传递,但是不能滥用localStorage,我们将各应用公用的数据放到localStorage就足够了,否则会出现影响性能,增加跨站点脚本攻击风险等一系列的问题。
js
localStorage.setItem("username", "crazy");
localStorage.getItem("username");
四、API
1、registerMicroApps(apps, lifeCycles?)
用于注册子应用
-
参数
- apps -
Array<RegistrableApp>
- 必选,微应用的一些注册信息 - lifeCycles -
LifeCycles
- 可选,全局的微应用生命周期钩子
- apps -
-
类型
-
RegistrableApp
-
name -
string
- 必选,微应用的名称,微应用之间必须确保唯一。 -
entry -
string | { scripts?: string[]; styles?: string[]; html?: string }
- 必选,微应用的入口。- 配置为字符串时,表示微应用的访问地址,例如
https://qiankun.umijs.org/guide/
。 - 配置为对象时,
html
的值是微应用的 html 内容字符串,而不是微应用的访问地址。微应用的publicPath
将会被设置为/
。
- 配置为字符串时,表示微应用的访问地址,例如
-
container -
string | HTMLElement
- 必选,微应用的容器节点的选择器或者 Element 实例。如container: '#root'
或container: document.querySelector('#root')
。 -
activeRule -
string | (location: Location) => boolean | Array<string | (location: Location) => boolean>
- 必选,微应用的激活规则。- 支持直接配置字符串或字符串数组,如
activeRule: '/app1'
或activeRule: ['/app1', '/app2']
,当配置为字符串时会直接跟 url 中的路径部分做前缀匹配,匹配成功表明当前应用会被激活。 - 支持配置一个 active function 函数或一组 active function。函数会传入当前 location 作为参数,函数返回 true 时表明当前微应用会被激活。如
location => location.pathname.startsWith('/app1')
。
- 支持直接配置字符串或字符串数组,如
-
loader -
(loading: boolean) => void
- 可选,loading 状态发生变化时会调用的方法。 -
props -
object
- 可选,主应用需要传递给微应用的数据。
-
-
LifeCycles
type Lifecycle = (app: RegistrableApp) => Promise;
- beforeLoad -
Lifecycle | Array<Lifecycle>
- 可选 - beforeMount -
Lifecycle | Array<Lifecycle>
- 可选 - afterMount -
Lifecycle | Array<Lifecycle>
- 可选 - beforeUnmount -
Lifecycle | Array<Lifecycle>
- 可选 - afterUnmount -
Lifecycle | Array<Lifecycle>
- 可选
- beforeLoad -
-
js
registerMicroApps([
{
name: 'react app', // 微应用的名字,自拟最好能一眼看出这个应用的功能
entry: '//localhost:7100', // 微应用的入口地址
container: '#container', // 微应用要挂载的容器id
activeRule: '/app1',// 微应用对应的路由匹配规则
loader:(loading)=>{},// loading 状态发生变化时会调用的方法。
props: {},// 传递给子应用的props
},
],
{
// 各生命周期调用的方法
beforeLoad: async (app) => {},
beforeMount: async (app) => {},
afterMount: async (app) => {},
afterUnmount: async (app) => {},
beforeUnmount: async (app) => {},
});
2、start(opts?)
用于启动qiankun
-
prefetch -
boolean | 'all' | string[] | (( apps: RegistrableApp[] ) => { criticalAppNames: string[]; minorAppsName: string[] })
- 可选,是否开启预加载,默认为true
。-
配置为
true
则会在第一个微应用 mount 完成后开始预加载其他微应用的静态资源配置为
'all'
则主应用start
后即开始预加载所有微应用静态资源配置为
string[]
则会在第一个微应用 mounted 后开始加载数组内的微应用资源配置为
function
则可完全自定义应用的资源加载时机 (首屏应用及次屏应用) -
sandbox -
boolean
|{ strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean }
- 可选,是否开启沙箱,默认为true
。 -
singular -
boolean | ((app: RegistrableApp<any>) => Promise<boolean>);
- 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为true
。 -
fetch -
Function
- 可选,自定义的 fetch 方法。 -
getPublicPath -
(entry: Entry) => string
- 可选,参数是微应用的 entry 值。 -
getTemplate -
(tpl: string) => string
- 可选。 -
excludeAssetFilter -
(assetUrl: string) => boolean
- 可选,指定部分特殊的动态加载的微应用资源(css/js) 不被 qiankun 劫持处理。
jsstart();
-
3、setDefaultMountApp(appLink)
用于设置主应用启动后默认进入的微应用。
-
参数
- appLink -
string
- 必选
- appLink -
js
import { setDefaultMountApp } from 'qiankun';
setDefaultMountApp('/app1');
4、runAfterFirstMounted(effect)
用于第一个微应用 mount 后需要调用的方法,比如开启一些监控或者埋点脚本。
-
参数
- effect -
() => void
- 必选
- effect -
js
import { runAfterFirstMounted } from 'qiankun';
runAfterFirstMounted(() => startMonitor());
5、loadMicroApp(app, configuration?)
用于手动加载微应用
-
参数
-
app -
LoadableApp
- 必选,微应用的基础信息- name -
string
- 必选,微应用的名称,微应用之间必须确保唯一。 - entry -
string | { scripts?: string[]; styles?: string[]; html?: string }
- 必选,微应用的入口(详细说明同上)。 - container -
string | HTMLElement
- 必选,微应用的容器节点的选择器或者 Element 实例。如container: '#root'
或container: document.querySelector('#root')
。 - props -
object
- 可选,初始化时需要传递给微应用的数据。
- name -
-
configuration -
Configuration
- 可选,微应用的配置信息- sandbox -
boolean
|{ strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean }
- 可选,是否开启沙箱,默认为true
。 - singular -
boolean | ((app: RegistrableApp<any>) => Promise<boolean>);
- 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为false
。 - fetch -
Function
- 可选,自定义的 fetch 方法。 - getPublicPath -
(entry: Entry) => string
- 可选,参数是微应用的 entry 值。 - getTemplate -
(tpl: string) => string
- 可选 - excludeAssetFilter -
(assetUrl: string) => boolean
- 可选,指定部分特殊的动态加载的微应用资源(css/js) 不被 qiankun 劫持处理
- sandbox -
-
-
返回值 -
MicroApp
- 微应用实例- mount(): Promise;
- unmount(): Promise;
- update(customProps: object): Promise;
- getStatus(): | "NOT_LOADED" | "LOADING_SOURCE_CODE" | "NOT_BOOTSTRAPPED" | "BOOTSTRAPPING" | "NOT_MOUNTED" | "MOUNTING" | "MOUNTED" | "UPDATING" | "UNMOUNTING" | "UNLOADING" | "SKIP_BECAUSE_BROKEN" | "LOAD_ERROR";
- loadPromise: Promise;
- bootstrapPromise: Promise;
- mountPromise: Promise;
- unmountPromise: Promise;
js
import { loadMicroApp } from 'qiankun';
import React from 'react';
class App extends React.Component {
containerRef = React.createRef();
microApp = null;
componentDidMount() {
this.microApp = loadMicroApp({
name: 'app1',
entry: '//localhost:1234',
container: this.containerRef.current,
props: { brand: 'qiankun' },
});
}
componentWillUnmount() {
this.microApp.unmount();
}
componentDidUpdate() {
this.microApp.update({ name: 'kuitos' });
}
render() {
return <div ref={this.containerRef}></div>;
}
}
6、prefetchApps(apps, importEntryOpts?)
用于手动预加载指定的微应用静态资源。
-
参数
- apps -
AppMetadata[]
- 必选 - 预加载的应用列表 - importEntryOpts - 可选 - 加载配置
- apps -
-
类型
-
AppMetadata
- name -
string
- 必选 - 应用名 - entry -
string | { scripts?: string[]; styles?: string[]; html?: string }
- 必选,微应用的 entry 地址
- name -
-
js
import { prefetchApps } from 'qiankun';
prefetchApps([
{ name: 'app1', entry: '//localhost:7001' },
{ name: 'app2', entry: '//localhost:7002' },
]);
7、addGlobalUncaughtErrorHandler(handler)
用于添加全局的未捕获异常处理器。
- handler -
(...args: any[]) => void
- 必选
js
import { addGlobalUncaughtErrorHandler } from 'qiankun';
addGlobalUncaughtErrorHandler((event) => console.log(event));
8、removeGlobalUncaughtErrorHandler(handler)
用于移除全局的未捕获异常处理器。
-
参数
- handler -
(...args: any[]) => void
- 必选
- handler -
js
import { removeGlobalUncaughtErrorHandler } from 'qiankun';
removeGlobalUncaughtErrorHandler(handler);
9、initGlobalState(state)
用于定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法。
-
参数
- state -
Record<string, any>
- 必选
- state -
-
返回
-
MicroAppStateActions
- onGlobalStateChange:
(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void
, 在当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback - setGlobalState:
(state: Record<string, any>) => boolean
, 按一级属性设置全局状态,微应用中只能修改已存在的一级属性 - offGlobalStateChange:
() => boolean
,移除当前应用的状态监听,微应用 umount 时会默认调用
- onGlobalStateChange:
-
主应用
js
import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();
微应用
js
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
props.setGlobalState(state);
}