从零搭建一个qiankun微前端demo

了解微前端的起因是因为我公司的大多数页面都是手机h5,分散且基本毫无关联,每次新页面都开一个二级域名,很难管理,所以研究了微前端,虽然很久以前就听过,拖延让我直到有需要才去自己学习

本文初探qiankun,并且搭建一个可以跑的基础demo仓库地址

前言

微前端是什么呢?按照qiankun文档中的一段摘录

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

我的理解是,微前端可以将多个关联性不强,不同项目的子应用合体在一个项目里,并且与技术栈无关,在同一个页面可以同时显示ReactVuejQuery的项目

那么qiankun是什么呢?

qiankun 是一个基于 single-spa微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统

由于是国内开源的项目,文档也是中文,自然学习qiankun也是最友好的

qiankun使用的两种方式

  • 随便什么项目安装qiankun后使用
  • 基于umijsplugin-qiankun

第一种方式,对主应用和子应用都没有要求,只要安装了qiankun并且照着文档配置好,就能跑通,但是需要配置的东西要多一点

第二种方式,主应用需要为umijs的项目,子应用如果也为umijs的项目,则配置非常简单,并且有额外的功能,比如跨应用的React hook来共享数据

所以,这两种方式,都探索一番

在普通项目中使用qiankun

普通项目并不需要是框架项目,仅仅一个js,一个html都可以的

主应用

主应用安装qiankun

yarn add qiankun

在主应用的html里增加一个id为root的div

<div id="root"></div>

主应用的js文件中写上qiankun的配置

import { registerMicroApps } from "qiankun";

// 仓库demo中有2个子项目,这里仅举例create-react-app的项目
registerMicroApps([
  {
    // 子应用唯一名称
    name: "app2",
    // 子应用入口
    entry: "//localhost:8002",
    // 子应用挂载的元素
    container: "#root",
    // 子应用匹配路径
    activeRule: "/app2",
  },
]);

start(); // 微前端 —— 启动

子应用

这里的子应用使用create-react-app的项目

修改webpack配置

由于要修改webpack的配置,在不eject的情况下需要安装react-app-rewired来修改配置

yarn add react-app-rewired --dev

修改pageage.json中的scripts

"scripts": {
-	"start": "react-scripts start",
+	"start": "react-app-rewired start",

增加react-app-rewired配置文件

根目录增加react-app-rewired的配置文件config-overrides.js

const { name } = require("./package");

module.exports = {
  webpack: function override(config, env) {
    // 根据qiankun文档配置
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = "umd";
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    return config;
  },
  devServer: function (configFunction) {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      // 微前端项目中子项目必须支持跨域
      config.headers = {
        "Access-Control-Allow-Origin": "*",
      };
      return config;
    };
  },
};

修改挂载元素id

修改页面挂载元素id,因为主应用占用了root这个id

public/index.html

- <div id="root"></div>
+ <div id="root-cra"></div>

修改子应用入口文件

src/index.jsx

增加render函数

-ReactDOM.render(
-  <App />,
-  document.getElementById('root')
-);

+ const render = () => {
+   ReactDOM.render(<App />, document.getElementById("root-cra")); // 修改id
+ };

添加qiankun生命周期钩子

// 在不是qiankun的情况下独立运行
// qiankun会注入__POWERED_BY_QIANKUN__变量
// 如果没有这个变量,表示并不是子应用,直接渲染页面节点
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log("app2 create-react-app bootstraped");
}
/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log("app2 create-react-app mount", props);
  // 调用render,渲染子应用
  render();
}
/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  console.log("app2 create-react-app unmount");
  ReactDOM.unmountComponentAtNode(document.getElementById("root-cra")!);
}

访问主应用地址 localhost:8080,是主应用,访问localhost:8080/app2create-react-app的子应用,如果子应用有路由也可以直接访问,如localhost:8000/app2/page1

在umijs中使用qiankun

umijs中,只需要主应用添加对应的插件plugin-qiankun

主应用

安装 plugin-qiankun

yarn add @umijs/plugin-qiankun@next --dev

新增 document.ejs

新建 src/pages/document.ejs,umi 约定如果这个文件存在,会作为默认模板

<!doctype html>
<html>
<head>
    <meta charSet="utf-8"/>
    <title>micro frontend</title>
</head>
<body>
<div id="root-subapp"></div>
</body>
</html>

这一步主要是需要增加一个额外的div元素来放置子应用,在plugin-qiankun中默认子应用挂载在root-subapp,如果没有这个元素会报错

修改配置 .umirc.ts

import { defineConfig } from 'umi';

export default defineConfig({
  qiankun: {
    master: {
      // 注册子应用信息
      apps: [
        {
          name: 'app1', // 唯一 id
          entry: '//localhost:8001', // html entry
          base: '/app1', // app1 的路由前缀,通过这个前缀判断是否要启动该应用,通常跟子应用的 base 保持一致
          history: 'browser', // 子应用的 history 配置,默认为当前主应用 history 配置
          // 子应用通过钩子函数的参数props可以拿到这里传入的值
          props: {},
        },
      ],
      jsSandbox: true, // 是否启用 js 沙箱,默认为 false
      prefetch: true, // 是否启用 prefetch 特性,默认为 true
    },
  },
});

子应用

umijs子应用就非常简单了,只需要修改.umirc.ts就行了

import { defineConfig } from 'umi';

export default defineConfig({
  base: `/app1`, // 子应用的 base,默认为 package.json 中的 name 字段
  qiankun: { slave: {} },
});

全局共享数据

在普通qiankunumijs中又不相同了

普通qiankun项目

普通qiankun可以通过initGlobalState方法来定义

主应用

import { initGlobalState } from 'qiankun';
// 初始化 state
const actions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
actions.setGlobalState(state); // 修改state
actions.offGlobalStateChange(); // 移除当前应用的状态监听,微应用 umount 时会默认调用

子应用

umijs的子应用钩子函数需要定义在src/app.js

export const qiankun = {
    // 从生命周期钩子函数 mount 中获取通信方法,使用方式和 master 一致
    async mount(props) {
      props.onGlobalStateChange((state, prev) => {
        // state: 变更后的状态; prev 变更前的状态
        console.log(state, prev);
      });
      props.setGlobalState(state); // 设置
    }
}

由于方法和事件只在钩子函数里有,我觉得可以在mount的时候注册一个像event bus这样的方法来供全局调用修改函数

umijs的qiankun项目

plugin-umi提供了一个比较方便的React hook来全局调用

主应用

约定主应用中在 src/rootExports.jsexport 内容

let data = '';
let eventList = [];

export function getData() {
  return data;
}

export function bindOnChange(fn) {
  if (typeof fn === 'function') {
    eventList.push(fn);
  }
  return function unBind() {
    eventList = eventList.filter(v => v !== fn);
  };
}

export function setData(newData) {
  data = newData;
  eventList.forEach(cb => cb(data));
}

子应用

// ...
const { bindOnChange, setData } = useRootExports();
useEffect(() => {
    const unBind = bindOnChange((data) => {
        console.log('root exports data change:', data);
    });
    return () => unBind();
}, []);

需要注意的是,如果这里子应用单独运行或者是主应用的qiankun不基于umijs,这个钩子会报错的,需要自己做好判断

直接通过配置传递

apps: [
    {
        name: 'app1', // 唯一 id
        // ...
        // 传递给子应用
        props: {
            username: 'zhangyu'
        },
    },
],

子应用在生命周期钩子函数中的参数可以拿到props的内容

qiankun中这个配置是可以动态加载的,本文只探索了固定配置


本文完整demo[仓库地址](