前言
天地混沌,洪荒初开。应业务之需,初探乾坤(qiankun)。
我要做什么
公司项目采用react、angular框架,功能复杂繁多,导致项目结构复杂、代码量巨大。为了进一步验证qiankun方案是否可行,确定一下调研目标:
- 兼容react、angular不同技术栈项目
- 子应用嵌套
- 子应用并行
- localStrage、cookie是否共享
- qiankun的props、initGlobalState通信机制可用性
项目搭建
react项目:
主基座搭建sub-main,子项目搭建react-sub、react-sub2,使用官网create-react-app进行,结合@rescripts/cli快速配置脚手架,进行相关配置。
主项目sub-main:
* 子项目react-sub:
子项目react-sub2:
angular项目:
使用@angular/cli脚手架构建子项目angular-sub
qiankun框架使用
主项目安装依赖
yarn add qiankun # 或者 npm i qiankun -S
主应用中注册微应用
import { registerMicroApps, start } from "qiankun";
registerMicroApps([
{
name: "react-sub", // 应用名称
entry: "//localhost:3000", // 应用地址
container: "#subContainer", // 嵌入主应用位置
activeRule: "/sub1", // 匹配规则
},
{
name: "react-sub2",
entry: "//localhost:3002",
container: "#subContainer",
activeRule: "/sub2",
},
{
name: "angular-sub",
entry: "//localhost:4200",
container: "#subContainer",
activeRule: "/sub3",
},
]);
start();
子应用配置
子应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出bootstrap、mount、unmount三个生命周期钩子,以供主应用在适当的时机调用。
react项目:
// 项目挂载
function render(props) {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("sub2root")
);
console.log(props);
}
// 子应用独立运行判断
if (!window.__POWERED_BY_QIANKUN_PARENT__) {
render();
}
// 只会在子应用初始化的时候调用一次
export async function bootstrap() {
console.log("react-sub2 bootstraped");
}
// 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
export async function mount(props) {
console.log("react-sub2 props from main framework", props);
render(props);
}
// 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(
container
? container.querySelector("#sub2root")
: document.querySelector("#sub2root")
);
}
添加public-path.js文件,并引入子应用index.js
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
angular项目:
Angular 与 qiankun 目前的兼容问题,需要特殊处理。
使用 single-spa-angular 生成一套配置
ng add single-spa-angular
在生成 single-spa 配置后,我们需要进行一些 qiankun 的接入配置。我们在 Angular 微应用的入口文件 main.single-spa.ts 中,导出 qiankun 主应用所需要的三个生命周期钩子函数,代码实现如下:
import "zone.js/dist/zone"; // 需额外引入,保证子项目独立运行
import { enableProdMode, NgZone } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { Router } from '@angular/router';
import { singleSpaAngular, getSingleSpaExtraProviders } from 'single-spa-angular';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { singleSpaPropsSubject } from './single-spa/single-spa-props';
if (environment.production) {
enableProdMode();
}
// 微应用单独启动时运行
if (!(window as any).__POWERED_BY_QIANKUN__) {
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
}
const lifecycles = singleSpaAngular({
bootstrapFunction: singleSpaProps => {
singleSpaPropsSubject.next(singleSpaProps);
return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule);
},
template: '<app-root />',
Router,
NgZone,
});
export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;
需在独立安装zone.js, 并引入上述文件
npm i zone.js -S
处理angular项目兼容问题,安装zone.js,并在主应用中的index.js文件引入
npm i zone.js -S
注:兼容原因
- qiankun在加载子应用前,会使用es6的Proxy方法,在window上创建一份proxy(也就是所谓的jsSandbox),让子应用实际运行在proxy中,以此来监听子应用做出的对window修改并能在卸载应用时还原。简单来说,就是当前的proxy的与Angular所依赖的zone.js不兼容,导致在加载Angular子应用时直接报错。
- 当前qiankun进行子应用的模块导入时,会查找最后一个添加到window上的全局变量作为umd导出的变量。而Angular 6.x以上版本会在全局创建ngDevMode等全局变量,导致qiankun寻找到的的export变量为ngDevMode。最后导致qiankun进行模块查找时出现错误。
打包命令修改,支持指定路径应用
ng serve --disable-host-check --port 2000 --base-href /sub3 --live-reload false
构建工具配置
react应用webpack:
const { name } = require('./package');
module.exports = {
webpack: config => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
config.output.globalObject = 'window';
return config;
},
devServer: _ => {
const config = _;
config.headers = {
'Access-Control-Allow-Origin': '*',
};
config.historyApiFallback = true;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;
return config;
},
};
ng 配置文件 extra-webpack.config.js
const singleSpaAngularWebpack = require("single-spa-angular/lib/webpack")
.default;
const webpackMerge = require("webpack-merge");
const { name } = require("./package");
module.exports = (angularWebpackConfig, options) => {
const singleSpaWebpackConfig = singleSpaAngularWebpack(
angularWebpackConfig,
options
);
const singleSpaConfig = {
output: {
library: `${name}-[name]`,
libraryTarget: "umd",
},
externals: {
"zone.js": "Zone",
},
};
const mergedConfig = webpackMerge.smart(
singleSpaWebpackConfig,
singleSpaConfig
);
return mergedConfig;
};
注:主应用加载子应用时,子应用必须支持跨域加载
启用各应用独立运行
由实际效果可得,qiankun可兼容react、angular子应用,angular项目存在一些兼容问题。
加载子应用react-sub
加载子应用react-sub2
加载子应用angular-sub
子应用嵌套
子项目自己运行一个 qiankun
存在的问题:
1、子项目无法根据已有信息判断是独立运行还是被集成
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
由于子项目本身也是一个qiankun 项目,所以独立运行时 window.POWERED_BY_QIANKUN 为 true,被集成时,还是 true。
解决办法:在主项目的入口文件另外定义一个全局变量window.POWERED_BY_QIANKUN_PARENT = true;,用这个变量来区分是被集成还是独立运行
2、子项目入口文件的修改
主要有以下几点注意的地方: 切换子项目时,避免重复注册孙子项目, 由于子项目会被注入一个前缀,那么孙子项目的路由也要加上这个前缀 注意容器的冲突,子项目和孙子项目使用不同的容器
3、打包配置的修改
以上操作完成后,在主项目中可以把这个 qiankun 子项目加载出来,但是点击其孙子项目,报错,生命周期找不到。
修改一下孙子项目的打包配置:
library: `${name}`,
原因:qiankun 取子项目的生命周期,优先取子项目运行时最后一个挂载到 window 上的变量,如果这个不是生命周期函数,再根据 appName 取。让 webpack 的 library 值对应 appName 即可。
运行结果:
子应用并行
start开启多实例,路由匹配规则activeRule配置上多个应用,则会同事激活,注意子应用并行需要挂在主应用不同id位置。
registerMicroApps([
{
name: "react-sub", // 应用名称
entry: "//localhost:3000", // 应用地址
container: "#subContainer", // 嵌入主应用位置
activeRule: "/sub1", // 匹配规则
},
{
name: "react-sub2",
entry: "//localhost:3002",
container: "#subContainer2",
activeRule: "/sub1",
},
]);
start({singular: false});
通信
浏览器 api
由于qiankunshi采用HTML Entry,localStrage、cookie可共享。
qiankun api
props
主应用通过props,传递给子应用
{
name: "react-sub", // 应用名称
entry: "//localhost:3000", // 应用地址
container: "#subContainer", // 嵌入主应用位置
activeRule: "/sub1", // 匹配规则
props: {
aa: 11,
},
},
子应用在生命周期中获取
export async function bootstrap(props) {
}
export async function mount(props) {
}
export async function unmount(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保持唯一,建议以各自项目名称设置