ZHANGYU.dev

October 14, 2023

从零搭建一个qiankun微前端demo

JavaScript6.2 min to read

了解微前端的起因是因为我公司的大多数页面都是手机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并且照着文档配置好,就能跑通,但是需要配置的东西要多一点

第二种方式,主应用需要为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';// 初始化 stateconst actions = initGlobalState(state);actions.onGlobalStateChange((state, prev) => {  // state: 变更后的状态; prev 变更前的状态  console.log(state, prev);});actions.setGlobalState(state); // 修改stateactions.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仓库地址