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

近期在项目中需要使用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的微前端平台

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
umi 是一个基于 react前端开发框架,它提供了一些方便的工具和规范,帮助我们更快地搭建部署移动端 web 项目。而 antd design 是一个优秀的 UI 组件库,它提供了丰富的组件,能够满足我们在移动端 web 项目中的各种需求。 首先,我们可以使用 umi 来初始化一个移动端的项目。umi 提供了脚手架工具,可以帮助我们快速创建一个基本的项目结构。通过命令行工具,我们可以选择使用 umi 内置的模板和插件,来搭建一个符合我们需求的移动端项目。 接着,我们可以使用 antd design 来构建我们的移动端页面。antd design 提供了丰富的 UI 组件,如导航栏、按钮、表单等等,这些组件都经过了精心设计和优化,可以帮助我们快速构建漂亮而且功能丰富的移动端页面。我们可以通过引入 antd design 的组件库,然后按照官方文档的指引使用这些组件,实现我们的页面功能。 在搭建项目时,umi 提供了一些功能和规范,例如路由配置、状态管理等。我们可以使用 umi 提供的路由功能来管理页面之间的跳转和传参,通过 umi 的状态管理功能可以更好地管理组件的状态和数据。同时,umi 还支持按需加载和自动优化等功能,能够提高项目的加载速度和性能。 总的来说,umi antd design react 搭建移动端 web 项目是一个很便捷和高效的选择。umi 提供了一个规范的开发框架,antd design 提供了丰富的 UI 组件,它们的结合可以帮助我们更快速地构建移动端 web 项目。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值