微前端umi + qiankun从搭建到部署的实践
近期在项目中需要使用qiankun实现微前端的微服务化,分享记录一下搭建的过程以及遇到的问题项目框架整体为umi搭建的主应用,两个子应用均为create-react-app搭建,进入系统,菜单main为主应用页面,点击菜单sub-react切换第一个子应用,菜单sec-sub切换第二个子应用,项目根目录下是主应用,child文件夹下为子应用sub-react,sec-sub,搭建后的初始目录结构如
近期在项目中需要使用qiankun实现微前端的微服务化,分享记录一下搭建的过程以及遇到的问题
项目框架
整体为umi搭建的主应用,两个子应用均为create-react-app搭建,进入系统,菜单main为主应用页面,点击菜单sub-react切换第一个子应用,菜单sec-sub切换第二个子应用,
项目根目录下是主应用,child文件夹下为子应用sub-react,sec-sub,搭建后的初始目录结构如下:
主应用配置
非umi搭建主应用
主应用负责导航的渲染和登录态的下发,为子应用提供一个挂载的容器div,同时主应用也可以添加自己的业务逻辑
主应用不限技术栈,只需要提供一个容器 DOM,然后注册微应用并 start 即可。
按照官网的主应用注册流程
先安装 qiankun :$ yarn add qiankun # 或者 npm i qiankun -S
注册微应用一般写在入口文件内。
注册微应用并启动:
import { registerMicroApps, start } from 'qiankun';
import { SUB_REACT, SUB_REACT_SECOND } from "@/utils/proxy"; //此为定义的全局变量
registerMicroApps([
{
name: "reactApp", // 子应用名称
// entry: SUB_REACT, // 子应用入口路径 此为定义的全局变量
entry: 'http://localhost:3000/', // 子应用入口路径
container: "#subapp", // 子应用挂载div的id
activeRule: "/sub-react",
props: { // 向子应用传值
msg: {
data: {
mt: "you are one",
},
},
historyMain: (value) => {
history.push(value);
},
},
},
{
name: "reactAppSecond",
entry: SUB_REACT_SECOND,
container: "#subapp", // 子应用挂载div的id
activeRule: "/sec_sub",
props: {
msg: {
data: {
mt: "you are one",
},
},
},
},
]);
// 启动 qiankun
start();
layout.jsx 子应用会挂载在layout页面 id为subapp的div上
<Content style={{ margin: "3px 3px" }}>
<div
id="subapp"
style={{ padding: 24, background: "#fff", minHeight: 360 }}
>
{this.props.children}
</div>
</Content>
umi搭建主应用
因为项目主应用是umi搭建的,一开始并没有在意,后面在主应用注册子应用之后,出现菜单切换子应用挂载后容器不存在问题,具体表现就是子应用一闪而过,变为空白,
具体还有另一种原因导致: 是因为主项目和子项目都是用id为app,造成子项目直接挂在在app上,导致主项目里面承载子项目的容器丢失,此时应当改子项目的id ,详情可参考文章
下面是umi搭建的主应用配置
官网参考
启用方式
yarn add @umijs/plugin-qiankun -D
配置 qiankun 开启。
.umirc.ts 文件
defineConfig({
...... ,
qiankun: {
master: {
// 注册子应用信息
apps: [
{
name: 'app1', // 唯一 id
entry: '//localhost:7001', // html entry
},
{
name: 'app2', // 唯一 id
entry: '//localhost:7002', // html entry
},
],
},
},
});
app.js 文件配置
以下详细配置可写在app.ts 文件中作为在.umirc.ts 文件中注册之后的补充
在app.ts中补充的原因:.umirc.ts 文件中注册时不能使用props传递参数
import { SUB_REACT, SUB_REACT_SECOND } from "@/utils/proxy";
// 子应用传递参数使用
export const qiankun = {
master: {
// 注册子应用信息
apps: [
{
entry: SUB_REACT, // html entry
name: "reactApp", // 子应用名称
container: "#subapp", // 子应用挂载的 div
activeRule: "/sub-react",
props: {
// 子应用传值
msg: {
data: {
mt: "you are one",
},
},
historyMain: (value:any) => {
history.push(value);
},
},
},
{
entry: SUB_REACT_SECOND, // html entry
name: "reactAppSecond",
container: "#subapp", // 子应用挂载的div
activeRule: "/sec_sub",
props: {
// 子应用传值
msg: {
data: {
mt: "you are one",
},
},
historyMain: (value:any) => {
history.push(value);
},
},
},
],
},
}
router.js 文件配置
{
title: "sub-react",
path: "/sub-react",
component: "../layout/index.js",
routes: [
{
title: "sub-react",
path: "/sub-react",
microApp: "reactApp",
microAppProps: {
autoSetLoading: true, // 开启子应用loading
// className: "reactAppSecond", // 子应用包裹元素类名
// wrapperClassName: "myWrapper",
},
},
],
},
{
title: "sec_sub",
path: "/sec_sub",
component: "../layout/index.js",
routes: [
{
title: "sec_sub",
path: "/sec_sub",
microApp: "reactAppSecond",
microAppProps: {
autoSetLoading: true, // 开启子应用loading
// className: "reactAppSecond",
// wrapperClassName: "myWrapper",
},
},
],
},
子应用配置
参考官网的子应用配置
新增.env文件添加PORT变量,端口号与父应用配置的保持一致。
SKIP_PREFLIGHT_CHECK=true
PORT=3000
PUBLIC_URL=/
在 src 目录新增 public-path.js:
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
设置 history 模式路由的 base:
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>
入口文件 index.js 修改,为了避免根 id #root 与其他的 DOM 冲突,需要限制查找范围。
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import Main from "./Main";
import Home from "./Home";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter, Switch, Route, Redirect } from "react-router-dom";
import "./public-path.js";
function render(props) {
const { container } = props;
console.log(props);
ReactDOM.render(
<BrowserRouter
basename={
window.__POWERED_BY_QIANKUN__ ? "/sub-react" : "/child/sub-react/"
}
>
<Switch>
<Route
path="/"
exact
render={(propsAPP) => <App {...propsAPP} propsMainAPP={props} />}
></Route>
<Route
path="/main"
exact
render={(propsAPP) => <Main {...propsAPP} propsMainAPP={props} />}
></Route>
<Route path="/home" exact component={Home}></Route>
{/* 子应用一定不能写,否则会出现路由跳转bug */}
{/* <Redirect from="*" to="/"></Redirect> */}
</Switch>
</BrowserRouter>,
container
? container.querySelector("#root-sub-app")
: document.querySelector("#root-sub-app")
);
}
// 表示子应用处于非qiankun内的环境,即独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
console.log("独立运行时");
render({});
}
export async function bootstrap() {
console.log("[react16] react app bootstraped");
}
export async function mount(props) {
// props.onGlobalStateChange((state, prev) => {
// // state: 变更后的状态; prev 变更前的状态
// console.log(state, prev);
// });
render(props);
}
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(
container
? container.querySelector("#root-sub-app")
: document.querySelector("#root-sub-app")
);
}
reportWebVitals();
修改 webpack 配置
安装插件 @rescripts/cli,当然也可以选择其他的插件,例如 react-app-rewired。
npm i -D @rescripts/cli
根目录新增 .rescriptsrc.js:
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;
},
};
修改 package.json:
- "start": "react-scripts start",
+ "start": "rescripts start",
- "build": "react-scripts build",
+ "build": "rescripts build",
- "test": "react-scripts test",
+ "test": "rescripts test",
- "eject": "react-scripts eject"
umi为主应用时加载子应用的loading
参考上面umi主应用的router.js文件配置,开启子应用loading
子应用加载之后可通过 主应用传递的props中的 setLoading(false)
来手动关闭loading
<button
onClick={() => {
propsMainAPP.setLoading(false);
}}
>
关闭子应用loading
</button>
父子应用通信
initGlobalState(state)
用法
定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法。
返回
MicroAppStateActions
onGlobalStateChange: (callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void, 在当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback
setGlobalState: (state: Record<string, any>) => boolean, 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
offGlobalStateChange: () => boolean,移除当前应用的状态监听,微应用 umount 时会默认调用
示例
主应用:
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();
微应用:
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
props.setGlobalState(state);
}
通过注册时的props传递 以及 实现应用之间相互跳转
{
name: "reactApp", // 子应用名称
// entry: SUB_REACT, // 子应用入口路径 此为定义的全局变量
entry: 'http://localhost:3000/', // 子应用入口路径
container: "#subapp", // 子应用挂载div的id
activeRule: "/sub-react",
props: { // 向子应用传值
msg: {
data: {
mt: "you are one",
},
},
historyMain: (value) => { // 向子应用传递 路由跳转的方法,以此实现在子应用跳转至主应用页面
history.push(value);
},
},
},
子应用独立仓库–聚合管理
单纯地将所有子仓库放到聚合目录下并.gitignore掉。
我这边的处理是把主应用作为聚合库,子应用clone在主应用根目录下,并将子应用文件 .gitignore掉
聚合库要能够做到一键install和一键启动整个项目,我们参考qiankun的examples,使用npm-run-all
聚合库安装npmi npm-run-all -D
。
聚合库的package.json增加clone、start、install、pull、以及build命令:
命令视自己情况而定
npm-run-all的--serial表示有顺序地一个个执行,--parallel表示同时并行地运行。
"clone": "npm-run-all --serial clone:*",
"clone:md": "md child ",
"clone:sub-react": "cd child && git clone http://xxx/sub-react.git",
"clone:sub-react-second": "cd child && git clone http://xxx/sub-react-second.git",
"installAll": "npm-run-all --parallel installAll:*",
"installAll:sub-react": "cd child && cd sub-react && npm i",
"installAll:sub-react-second": "cd child && cd sub-react-second && npm i",
"checkout": "npm-run-all --serial checkout:*",
"checkout:sub-react": "cd child && cd sub-react && git checkout develop ",
"checkout:sub-react-second": "cd child && cd sub-react-second && git checkout develop",
"start": "npm-run-all --parallel start:*",
"start:sub-react": "cd child && cd sub-react && npm start",
"start:sub-react-second": "cd child && cd sub-react-second && npm start",
"start:main": " umi dev",
"build": "npm-run-all --serial build:*",
"build:main": "cross-env UMI_ENV=dev umi build",
"build:sub-react": "cd child && cd sub-react && npm run build",
"build:sub-react-second": "cd child && cd sub-react-second && npm run build",
"pull": "npm-run-all --parallel pull:*",
"pull:main": "git pull",
"pull:sub-react": "cd child && cd sub-react && git pull",
"pull:sub-react-second": "cd child && cd sub-react-second && git pull"
部署
我的方法是将主应用以及子应用都打包至根目录的dist文件,在做聚合库的时候,我修改了子应用的打包路径,直接打包至主应用打包好的dist文件下
在设置打包命令时 使用 npm-run-all的--serial
表示有顺序地一个个执行
我们将子应用都放在xx.com/child/这个二级目录下,根路径/留给主应用。
部署之后的演示
遇到的问题
路由切换问题
主应用切换至子应用,然后子应用之前相互切换时会出现切换不过去的问题,原因是子应用的路由重定向导致,
{/* 子应用一定不能写,否则会出现路由跳转bug */}
{/* <Redirect from="*" to="/"></Redirect> */}
umi主应用配置问题
umi主应用需要使用umi内置的qiankun,否则会出现切换子应用后挂载容器丢失问题
结语
以上是对qiankun这个框架的实现,希望能给大家一些参考
github源码 https://github.com/Mine-7/Qiankun-T
参考文章
更多推荐
所有评论(0)