近期在项目中需要使用qiankun实现微前端的微服务化,分享记录一下搭建的过程以及遇到的问题

项目框架

整体为umi搭建的主应用,两个子应用均为create-react-app搭建,进入系统,菜单main为主应用页面,点击菜单sub-react切换第一个子应用,菜单sec-sub切换第二个子应用,

image.png

image.png

项目根目录下是主应用,child文件夹下为子应用sub-react,sec-sub,搭建后的初始目录结构如下:

image.png

主应用配置

非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掉

image.png

聚合库要能够做到一键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/这个二级目录下,根路径/留给主应用。

image.png

部署之后的演示

test.gif

遇到的问题

路由切换问题

主应用切换至子应用,然后子应用之前相互切换时会出现切换不过去的问题,原因是子应用的路由重定向导致,

{/* 子应用一定不能写,否则会出现路由跳转bug */}
{/* <Redirect from="*" to="/"></Redirect> */}

umi主应用配置问题

umi主应用需要使用umi内置的qiankun,否则会出现切换子应用后挂载容器丢失问题

结语

以上是对qiankun这个框架的实现,希望能给大家一些参考

github源码 https://github.com/Mine-7/Qiankun-T

参考文章

微前端在小米 CRM 系统的实践

微前端qiankun从搭建到部署的实践

搭建umi + qiankun + antd的微前端平台

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐