monorepo与微前端工程化改造

969 阅读8分钟

前置概念

monorepo主要将不同工程、依赖包管理在一个代码仓库。与此相对应的名次是polyrepo,是将不同工程和依赖包管理在多个代码仓库。

monorepo

monorepo相比于polyrepo来说,最大的有点就是解决了共享代码问题。

共享代码

假如日常工作中,有两个工程app、docs共用一个依赖包shared-utils。

polyrepo方式下升级shared-utils流程如下:

  1. 升级shared-utils代码,并进行git commit
  2. 发布shared-utils到私有或者公共仓库
  3. 升级app的shared-utils依赖版本,并进行git commit
  4. 升级docs的shared-utils依赖版本,并进行git commit
  5. 准备app和docs工程的打包与发布

当工程和依赖非常多时,polyrepo方式下整个更新流程会非常繁琐、冗长且易出错。

monorepo方式下升级shared-utils流程如下:

  1. 升级shared-utils代码,并进行git commit
  2. 准备app和docs工程的打包与发布

不需要shared-utils的version升级、发布,也不需要更改app、docs依赖版本的更新,工程app、docs直接获取同一代码仓库下的shared-utils最新代码,app、docs、shared-utils整个代码仓库只需要提交一次git commit就可完成本次更新。

统一任务操作

将所有代码放到一个仓库了,可以尽可能优化并统一相关脚本任务。

workspaces

monorepo的主要组成基础是工作空间(workspace)。

  1. workspaces:每个工程、依赖都有各自的工作workspace、package.json,各个workspace可以相互依赖(意思是可以相互引用,例如app和docs工程都可以使用shared-utils包)
  2. root workspace:整个monorepo在根目录下,还有一个根workspace、package.json。根workspace主要用来解决以下三个问题:
    1. 安装整个monorepo的依赖项目,比如:turbo
    2. 添加整个monorepo的任务脚本,不仅仅是单个workspace的脚本
    3. 存放整个工程的readme.md,说明整个monorepo怎么运行

monorepo是一种概念,具体需要通过包管理器(npm、pnpm、yarn 1、yarn 2等)来实现。本文主使用pnpm,通过pnpm来实现workspaces管理与依赖安装。

pnpm vs npm vs yarn:

不要问,问就是pnpm快,节约时间,节约生命。

  1. pnpm.io/7.x/pnpm-vs…
  2. pnpm.io/7.x/feature…

workspace管理

pnpm-workspace.yaml定义根workspace以及各个工程与依赖的workspace,并使您能够从工作区中包含/排除目录。默认情况下,包括所有子目录的所有包。

yiban

packages:
  # all packages in direct subdirs of packages/
  - 'packages/*'
  # all packages in subdirs of components/
  - 'components/**'
  # exclude packages that are inside test directories
  - '!**/test/**'

package安装

pnpm依赖: 各个workspaces之间依赖可相互引用,以docs工程引用shared-utils依赖为例,依赖管理最终展示如package.json所示:

# pnpm monorepo依赖引用管理
{
  "dependencies": {
    "shared-utils": "workspace:*"
  }
}

# 对比 npm & yarn monorepo依赖引用管理
{
  "dependencies": {
    "shared-utils": "*"
  }
}

pnpm命令: 各个workspaces之间依赖的相互引用,主要通过pnpm相关命令来实现,管理依赖的基本命令如下所示:

npm命令-安装pnpm:

npm install -g pnpm

pnpm命令-整个仓库安装依赖:

使用场景:克隆完代码或者创建完monorepo后在根目录执行pnpm install,根workspace及各个workspace都会创建node_modules

pnpm installl

pnpm命令-某个workspace安装依赖:

pnpm add <package> --filter <workspace>

# 示例
pnpm add react --filter web

pnpm命令-某个workspace移除依赖:

pnpm uninstall <package> --filter <workspace>

# 示例
pnpm uninstall react --filter web

pnpm命令-某个workspace升级依赖:

pnpm update <package> --filter <workspace>

# 示例
pnpm update react --filter web

turbo命令-某个workspace执行任务:

turbo run dev --filter docs

一句话总结:monorepo实现将多个相关工程、依赖包放到由pnpm管理的一个workspaces仓库内,满足不同环境的开发、测试、发布等任务管理需要。

安装完turbo,应该使用turbo替换原来包管理器,输入turbo run xxx执行package.json中的脚本命令,

创建monorepo

第一步:mkdir my-monorepo && cd my-monorepo

第二步:pnpm init

第三步:mkdir apps packages docs sdk

第四步:touch pnpm-workspace.yaml

packages:
  - "docs"
  - "apps/*"
  - "packages/*"

第五步:cd apps,创建main、sub-app-1、sub-app-2、sub-app-3

main项目创建(umi4):

mkdir main && cd main

$ npx create-umi@latest
? Pick Umi App Template › - Use arrow-keys. Return to submit.
    Simple App
❯   Ant Design Pro
    Vue Simple App
touch .env 

/* .env */
PORT=7001

sub-app-1项目创建(umi3):

mkdir sub-app-1 && cd sub-app-1

yarn create @umijs/umi-app
touch .env 

/* .env */
PORT=7002

sub-app-2项目创建(cra+@craco/craco):

npx create-react-app sub-app-2
cd sub-app-2
rm -rf node_modules

安装@craco/craco

npm i -D @craco/craco

创建craco.config.js文

touch craco.config.js

my-app
  ├── node_modules
+ ├── craco.config.js
  └── package.json

/* craco.config.js */
module.exports = {
  // ...
};

修改scripts

"scripts": {
-  "start": "react-scripts start"
+  "start": "craco start"
-  "build": "react-scripts build"
+  "build": "craco build"
-  "test": "react-scripts test"
+  "test": "craco test"
}

安装antd

npm install antd

修改 src/App.js,引入 antd 的按钮组件。

import React from 'react';
import { Button } from 'antd';
import 'antd/dist/reset.css';
import './App.css';

const App = () => (
  <div className="App">
    <Button type="primary">Button</Button>
  </div>
);

export default App;
touch .env 

/* .env */
PORT=7003

sub-app-3项目创建(umi4):

mkdir sub-app-3 && cd sub-app-3

$ npx create-umi@latest
? Pick Umi App Template › - Use arrow-keys. Return to submit.
    Simple App
❯   Ant Design Pro
    Vue Simple App
touch .env 

/* .env */
PORT=7004

第六步:根workspace的package.json添加命令,先执行pnpm install,后执行pnpm run start启动四个项目:

{
  "name": "my-monorepo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "pnpm -r --parallel run start"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

第七步:touch .gitignore,执行git init、git status、git add、git commit提交init代码

# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
node_modules
.pnp
.pnp.js

# testing
coverage

# cra
out
build

# other
dist

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# turbo
.turbo

# umi
.umi
.umi-production
.umi-test
.env.local

目前monorepo项目的目录结构如下所示:

my-monorepo
├─ docs
├─ apps
│  ├─ main
│  ├─ sub-app-1
│  ├─ sub-app-2
│  ├─ sub-app-3
├─ packages
└─ sdk
...

依赖管理

内部package

创建math-helpers内部依赖包供各工程调用。

创建package

在/package内,创建math-helpers文件夹。

mkdir packages/math-helpers

创建package.json:

{
  "name": "math-helpers",
  "dependencies": {
    // Use whatever version of TypeScript you're using
    "typescript": "latest"
  }
}

创建src文件夹,并添加文件packages/math-helpers/src/index.ts.

export const add = (a: number, b: number) => {
  return a + b;
};
 
export const subtract = (a: number, b: number) => {
  return a - b;
};

同时,新增配置packages/math-helpers/tsconfig.json:

{
  "compilerOptions": {
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "moduleResolution": "node",
    "preserveWatchOutput": true,
    "skipLibCheck": true,
    "noEmit": true,
    "strict": true
  },
  "exclude": ["node_modules"]
}

引用package

在main与sub-app-1工程中引用math-helpers:

{
  "dependencies": {
    "math-helpers": "workspace:*"
  }
}

在根目录执行pnpm install命令后,就可以测试math-helpers相关函数:

+ import { add } from "math-helpers";
 
+ add(1, 2);

此时,你会发现以下错误:

Cannot find module 'math-helpers' or its corresponding type declarations.

这是因为你并未在math-helpers/package.json文件中声明math-helpers包的入口文件。

修复入口文件和类型导出

在packages/math-helpers/package.json文件中,增加main和types字段:

{
  "name": "math-helpers",
  "main": "src/index.ts",
  "types": "src/index.ts",
  "dependencies": {
    "typescript": "latest"
  }
}

运行app

现在,在app工程中,执行dev脚本:

turbo dev

此时会出现下列报错:

../../packages/math-helpers/src/index.ts
Module parse failed: Unexpected token (1:21)
You may need an appropriate loader to handle this file type,
currently no loaders are configured to process this file.
See https://webpack.js.org/concepts#loaders
> export const add = (a: number, b: number) => {
|   return a + b;
| };

配置打包选项

在umi3中添加以下配置:

/** @type {import('next').NextConfig} */
const nextConfig = {
  transpilePackages: ['math-helpers'],
};
 
module.exports = nextConfig;

总结

我们直接将需要共享的代码做成internal package,不需要打包,就能在项目中使用。

外部package

创建ui项目

father 本身需要在 Node.js v14 以上的环境中运行,使用前请确保已安装 Node.js v14 及以上版本。

father 产出的 Node.js 产物默认兼容到 Node.js v14,Browser 产物默认兼容到 ES5(IE11),暂不支持修改。

通过 create-father 快速创建一个 father 项目:

$ npx create-father ui

$ pnpm add @types/react react --filter=ui

脚手架中仅包含最基础的配置,更多配置项及作用可以参考 配置项文档

编写src/Button.ts

import * as React from "react";

export const Button = () => {
  return <button type="button">Boop</button>;
};

编写src/index.ts

import * as React from "react";
export * from "./Button";
export default "Hello father 4!";

执行构建:

$ pnpm father build

查看 dist 文件夹,可以看到构建产物已被生成出来。恭喜你,已经完成了第一个 father 项目的构建工作 🎉

在main与sub-app-1中引入ui依赖:

{
  "dependencies": {
    "ui": "workspace:*"
  }
}

接下来,你可以查看其它文档了解 father 的更多功能:

参考

  1. github.com/umijs/fathe…
  2. github.com/umijs/fathe…

开发

设置dev脚本

在turbo.json中,设置dev任务:

{
  "pipeline": {
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

因为dev任务不产生输出,因此输出为空。且dev环境比较独特,所以很少需要缓存,所以需要设置cache为false。我们还将persistent设置为true,因为开发任务是长时间运行的任务,并且我们希望确保它不会阻止任何其他任务的执行。

同时,我们需要为根package.json提供一个dev脚本,这使开发人员能够直接从他们的普通任务运行器中运行任务。

{
  "scripts": {
    "dev": "turbo run dev"
  }
}

或者cache也可以配置在命令行内,但是--no-cache只是禁止了写缓存,如果你想禁止读缓存,需要使用--force:

# Run `dev` npm script in all workspaces in parallel,
# but don't cache the output
turbo run dev --no-cache

设置运行依赖

在某些工作流中,您需要在运行开发任务之前先运行任务。例如,生成代码或运行db:migrate任务。

{
  "pipeline": {
    "dev": {
      "dependsOn": ["codegen", "db:migrate"],
      "cache": false
    },
    "codegen": {
      "outputs": ["./codegen-outputs/**"]
    },
    "db:migrate": {
      "cache": false
    }
  }
}

在特定workspace运行dev脚本

可以进入相应workspace,执行脚本命令,turbo将自动获取您在docs工作区中的信息,并运行dev任务。

cd <root>/apps/docs
turbo run dev

或者,可从仓库中的任何其他位置运行相同的任务,请使用--filter语法。例如:

turbo run dev --filter docs

变量(待编写)

在开发过程中,您经常需要使用环境变量。这些可以让您自定义程序的行为,例如,在开发和生产中指向不同的DATABASE_URL,我们建议使用名为dotenv-cli的库来解决此问题。

  1. Install dotenv-cli in your root workspace:
# Installs dotenv-cli in the root workspace
pnpm add dotenv-cli --ignore-workspace-root-check
  1. Add a .env file to your root workspace:
  ├── apps/
  ├── packages/
+ ├── .env
  ├── package.json
  └── turbo.json

Add any environment variables you need to inject:

DATABASE_URL=my-database-url
  1. Inside your root package.json, add a dev script. Prefix it with dotenv and the -- argument separator:
{
  "scripts": {
    "dev": "dotenv -- turbo run dev"
  }
}

This will extract the environment variables from .env before running turbo run dev.

  1. Now, you can run your dev script:
pnpm dev

And your environment variables will be populated! In Node.js, these are available on process.env.DATABASE_URL.

发布

打包代码

设置build脚本

首先安装打包工具,这里以tsup为例,tsup将打包后的文件输出到dist文件夹下,因此需要将dist目录添加到.gitignore中:

pnpm add tsup --filter math-helpers
dist
node_modules

同时在turbo.json中增加outputs,调整turbo.json(通过outputs增加dist时,turbo会自动cache):

{
  "pipeline": {
    "build": {
      "outputs": ["dist/**"]
    }
  }
}

新增build脚本,并改变main和types的指向:

{
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "scripts": {
    "build": "tsup src/index.ts --format cjs --dts"
  }
}

设置依赖优先build

为保证组件库优先工程代码打包,需要通过dependsOn进行配置。

{
  "pipeline": {
    "build": {
      "dependsOn": [
        // Run builds in workspaces I depend on first
        "^build"
      ]
    }
  }
}

现在,可以使用turbo run build命令,turbo会自动处理build脚本,满足依赖先打包,使用依赖包的工程后打包。

设置dev脚本

添加build脚本,解决了代码打包的问题,但开发环境下仍有问题。因为在开发模式下,我们修改源码后,并不能实时打包最新代码。我们可以添加dev命令,为tsup传入--watch,表示:

{
  "scripts": {
    "build": "tsup src/index.ts --format cjs --dts",
    "dev": "npm run build -- --watch"
  }
}

docker打包(待编写)

更新版本与npm发布

在monorepo中手动对包进行版本控制和发布可能会非常令人厌烦。幸运的是,有一个工具可以让事情变得简单、直观——Changesets CLI。同时也可以使用intuit/automicrosoft/beachball

初始化changesets

在根workspace安装changsets依赖:

pnpm add -Dw @changesets/cli

然后初始化changeset:

pnpm changeset init

更新版本&发布

  1. 在根workspace运行pnpm changeset,在根目录下会产生.changeset目录包含changeset的变更目录
  2. 运行pnpm changeset version更新package的版本,并且该包的changelog以及所有依赖该包的changelog文件都会进行更新。
  3. 包的版本已经升级,因此需要运行pnpm install,将重新升级pnpm的lockfile并更新依赖。
  4. 执行git add . git commit -m'xx: xx'提交代码.
  5. 执行pnpm publish -r,该命令将会发布所有已更新且允许发布的包。

后续每次发版更新,只需要重新执行上述的1、2、3、4、5即可。

参考

  1. pnpm.io/7.x/workspa…
  2. github.com/changesets/…

任务管理

lint

commit

cicd

其他

git项目如何迁移

pnpm install-completion

my-monorepo
├─ docs
├─ apps
│  ├─ main
│  ├─ sub-app-1
│  ├─ sub-app-2
│  ├─ sub-app-3
│  ├─ tsconfig
│  ├─ tsconfig
│  ├─ tsconfig
├─ packages
│  ├─ main
│  ├─ tsconfig
│  ├─ shared-utils
│  ├─ tsconfig
│  ├─ tsconfig
│  ├─ tsconfig
│  ├─ tsconfig
└─ sdk

package.json的name属性

每个工作空间都需要在package.json中指定一个名字。

{
  "name": "shared-utils"
}

名字主要用来:

  1. 当前当前依赖包的名字
  2. 在其他workspace引入该包
  3. 发布包到私有/共有仓库

同时,可以使用组织或者用户范围去避免包命名冲突。

"shared-utils" : "workspace:*"

*保证了workspace引用的一直是最新的版本,避免一直因版本升级需要更新依赖版本的问题。

如何引用package

方法一:手动更改package.json的dependencies,然后在根目录下执行pnpm install

方法二:执行pnpm add shared-utils --filter main

使用以上两种方式引用依赖后,即可在文件内引用该包,同时package.json形式如下所示:

{
  "dependencies": {
    "shared-utils": "workspace:*"
  }
}

pnpm install

在monorepo中,当在根目录执行install命令时,会进行以下变动:

  1. 检查workspaces中安装过的依赖
  2. 软链workspaces相关的依赖包到node_modules,保证能正常导入包的内容
  3. 网络下载其它依赖包

所以无论何时添加/删除工作区,或更改它们在文件系统上的位置,都需要从根目录重新运行安装命令,以重新设置工作区。

注:每次workspace的package源码变动时,并不需要重新执行install,只有当workspace的位置或者配置变动时,需要重新install。

如何执行run时报错,你可能需要删除每个workspace的node_modules文件夹(可以提取为某个命令),并重新执行install重新安装依赖。

Edit .gitignore

添加.turbo到.gitignore,因为CLI将这些文件夹用于日志和某些任务输出。

turbo run

Before we move on, let's try running a task called hello that isn't registered in turbo.json:

turbo hello

You'll see an error in the terminal. Something resembling:

Could not find the following tasks in project: hello

That's worth remembering - in order for turbo to run a task, it must be in turbo.json.


微前端改造


创建微前端工程(umi4、umi3、cra)

创建主子应用项目

main项目创建(umi4)

mkdir main && cd main

$ npx create-umi@latest
? Pick Umi App Template › - Use arrow-keys. Return to submit.
    Simple App
❯   Ant Design Pro
    Vue Simple App
touch .env 

/* .env */
PORT=7001

sub-app-1项目创建(umi3)

mkdir sub-app-1 && cd sub-app-1

yarn create @umijs/umi-app
touch .env 

/* .env */
PORT=7002

sub-app-2项目创建(cra+@craco/craco)

npx create-react-app sub-app-2
cd sub-app-2

安装@craco/craco

npm i -D @craco/craco

创建craco.config.js文

touch craco.config.js

my-app
  ├── node_modules
+ ├── craco.config.js
  └── package.json

/* craco.config.js */
module.exports = {
  // ...
};

修改scripts

"scripts": {
-  "start": "react-scripts start"
+  "start": "craco start"
-  "build": "react-scripts build"
+  "build": "craco build"
-  "test": "react-scripts test"
+  "test": "craco test"
}

安装antd

npm install antd

修改 src/App.js,引入 antd 的按钮组件。

import React from 'react';
import { Button } from 'antd';
import 'antd/dist/reset.css';
import './App.css';

const App = () => (
  <div className="App">
    <Button type="primary">Button</Button>
  </div>
);

export default App;
touch .env 

/* .env */
PORT=7003

sub-app-3项目创建(umi4)

mkdir sub-app-3 && cd sub-app-3

$ npx create-umi@latest
? Pick Umi App Template › - Use arrow-keys. Return to submit.
    Simple App
❯   Ant Design Pro
    Vue Simple App
touch .env 

/* .env */
PORT=7004

微前端改造

(主:main,子:sub-app-1、sub-app-2、sub-app-3)

(主:sub-app-1,子:sub-app-2、sub-app-3)

main(umi4)配置为主应用

配置父应用

首先需要配置父应用,注册子应用的相关信息,这样父应用才能识别子应用并在内部引入。注册子应用的方式主要有两种

  • 插件注册子应用。
  • 运行时注册子应用(推荐)。

主要推荐使用运行时注册子应用,本文以运行时为例:

修改父应用的 Umi 配置文件,添加如下内容:

// .umirc.ts
export default {
  qiankun: {
    master: {},
  },
};

修改父应用的 src/app.ts 文件,导出 qiankun 对象:

// src/app.ts
export const qiankun = {
  apps: [
    {
      name: 'sub-app-1',
      entry: '//localhost:7002',
      activeRule: '/sub-app-1',
      container: '#micro-app-1',
    },
    {
      name: 'sub-app-2',
      entry: '//localhost:7003',
      activeRule: '/sub-app-2',
      container: '#micro-app-2',
    },
    {
      name: 'sub-app-3',
      entry: '//localhost:7004',
      activeRule: '/sub-app-3',
      container: '#micro-app-3',
    },
  ],
};

引入子应用

import { defineConfig } from '@umijs/max';

export default defineConfig({
  routes: [
    {
      path: '/',
      redirect: '/home',
    },
    {
      name: '首页',
      path: '/home',
      component: './Home',
      icon: 'HomeOutlined',
    },
    {
      name: 'theme',
      path: '/theme',
      component: './Theme',
      icon: 'SmileOutlined',
    },
    {
      name: 'sub-app-1(umi3)',
      path: '/sub-app-1',
      microApp: 'sub-app-1',
      icon: 'SmileOutlined',
      routes: [
        {
          name: '应用间通信',
          path: '/sub-app-1/one',
        },
        {
          name: '应用间嵌套',
          path: '/sub-app-1/two',
        },
        {
          name: '应用间通信',
          path: '/sub-app-1/sub-app-3',
          routes: [
            {
              name: '嵌套路由1',
              path: '/sub-app-1/sub-app-3/one',
            },
            {
              name: '嵌套路由2',
              path: '/sub-app-1/sub-app-3/three',
            },
          ],
        },
      ],
    },
    {
      name: 'sub-app-2(cra)',
      path: '/sub-app-2',
      component: './LoadSubApp2',
      icon: 'SmileOutlined',
    },
    {
      name: 'sub-app-3(umi4)',
      path: '/sub-app-3',
      microApp: 'sub-app-3',
      icon: 'SmileOutlined',
      routes: [
        {
          name: '嵌套路由1',
          path: '/sub-app-3/one',
        },
        {
          name: '嵌套路由2',
          path: '/sub-app-3/three',
        },
        {
          path: '/sub-app-3',
          redirect: '/sub-app-3/one',
        },
      ],
    },
    {
      path: '/404',
      component: './Exception404',
    },
  ],
});

配置main生命周期

// src/app.ts
export const qiankun = {
  lifeCycles: {
    async beforeLoad(props: any) {
      console.log('main', 'beforeLoad', props);
    },
    async load(props: any) {
      console.log('main', 'load', props);
    },
    async bootstrap(props: any) {
      console.log('main', 'bootstrap', props);
    },
    async beforeMount(props: any) {
      console.log('main', 'beforeMount', props);
    },
    async mount(props: any) {
      console.log('main', 'mount', props);
    },
    async afterMount(props: any) {
      console.log('main', 'afterMount', props);
    },
    async beforeUnmount(props: any) {
      console.log('main', 'beforeUnmount', props);
    },
    async unmount(props: any) {
      console.log('main', 'unmount', props);
    },
    async afterUnmount(props: any) {
      console.log('main', 'afterUnmount', props);
    },
    async unload(props: any) {
      console.log('main', 'unload', props);
    },
    async update(props: any) {
      console.log('main', 'update', props);
    },
  },
};

传递数据

sub-app-1(umi3)配置为微应用

注册为主/微应用

插件注册(config.js)

export default {
  qiankun: {
    master: {
      apps: [
        {
          name: 'sub-app-2',
          entry: '//localhost:7003',
        },
        {
          name: 'sub-app-3',
          entry: '//localhost:7004',
        },
      ],
    },
    slave: {},
  },
};

引入子应用

export default defineConfig({
  routes: [
    {
      path: '/theme',
      name: 'theme',
      component: '@/pages/theme',
    },
    {
      path: '/',
      component: '@/layouts/index',
      routes: [
        {
          path: '/one',
          name: 'one',
          component: '@/pages/one',
        },
        {
          path: '/two',
          name: 'two',
          component: '@/pages/two',
        },
        {
          name: 'micro-3',
          path: '/sub-app-3',
          microApp: 'sub-app-3',
        },
        {
          path: '/',
          redirect: '/one',
        },
      ],
    },
  ],
});

配置sub-app-1生命周期

export const qiankun: any = {
  async beforeLoad(props: any) {
    console.log('sub-app-1', 'beforeLoad', props);
  },
  async load(props: any) {
    console.log('sub-app-1', 'load', props);
  },
  async bootstrap(props: any) {
    console.log('sub-app-1', 'bootstrap', props);
  },
  async beforeMount(props: any) {
    console.log('sub-app-1', 'beforeMount', props);
  },
  async mount(props: any) {
    console.log('sub-app-1', 'mount', props);
  },
  async afterMount(props: any) {
    console.log('sub-app-1', 'afterMount', props);
  },
  async beforeUnmount(props: any) {
    console.log('sub-app-1', 'beforeUnmount', props);
  },
  async unmount(props: any) {
    console.log('sub-app-1', 'unmount', props);
  },
  async afterUnmount(props: any) {
    console.log('sub-app-1', 'afterUnmount', props);
  },
  async unload(props: any) {
    console.log('sub-app-1', 'unload', props);
  },
  async update(props: any) {
    console.log('sub-app-1', 'update', props);
  },
};

接收/传递数据

sub-app-2(cra)配置为微应用

注册为微应用

微应用分为有 webpack 构建和无 webpack 构建项目,有 webpack 的微应用(主要是指 Vue、React、Angular)需要做的事情有:

  1. 新增 public-path.js 文件,用于修改运行时的 publicPath。什么是运行时的 publicPath ?

注意:运行时的 publicPath 和构建时的 publicPath 是不同的,两者不能等价替代。

  1. 微应用建议使用 history 模式的路由,需要设置路由 base,值和它的 activeRule 是一样的。
  2. 在入口文件最顶部引入 public-path.js,修改并导出三个生命周期函数。
  3. 修改 webpack 打包,允许开发环境跨域和 umd 打包。

主要的修改就是以上四个,可能会根据项目的不同情况而改变。例如,你的项目是 index.html 和其他的所有文件分开部署的,说明你们已经将构建时的 publicPath 设置为了完整路径,则不用修改运行时的 publicPath (第一步操作可省)。

无 webpack 构建的微应用直接将 lifecycles 挂载到 window 上即可。

根据以上说明,将cra工程改造为微应用:

微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。

  1. 在 src 目录新增 public-path.js:
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. App.js设置 history 模式路由的 base:
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "./pages/Home";

function App(props) {
  const { base = "/sub-app-2" } = props;

  return (
    <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? base : "/"}>
      <Routes>
        <Route path="/" element={<Home {...props} />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;
  1. 入口文件 index.js 修改,为了避免根 id #root 与其他的 DOM 冲突,需要限制查找范围。
import "./public-path";
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";

const rootId = "#micro-app-3";
let root = null;

function render(props) {
  const { container } = props;

  const rootContainer = container
    ? container.querySelector(rootId)
    : document.querySelector(rootId);

  root = ReactDOM.createRoot(rootContainer);

  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
}

if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}

export async function bootstrap(props) {
  console.log("[react18] react app bootstraped");
}

export async function mount(props) {
  console.log("[react18] props from main framework", props);
  render(props);
}

export async function unmount(props) {
  const { container } = props;

  const rootContainer = container
    ? container.querySelector(rootId)
    : document.querySelector(rootId);

  root.unmount(rootContainer);
}

export async function update(props) {
  render(props);
}
  1. 修改 webpack 配置craco.config.js。
const { name } = require("./package.json");

module.exports = {
  webpack: {
    configure: (config, { env, paths }) => {
      config.output.library = `${name}-[name]`;
      config.output.libraryTarget = "umd";
      config.output.chunkLoadingGlobal = `webpackJsonp_${name}`;
      config.output.globalObject = "window";
      return config;
    },
  },
  devServer: (devServerConfig, { env, paths, proxy, allowedHost }) => {
    const config = { ...devServerConfig };
    config.historyApiFallback = true;
    config.hot = false;
    config.liveReload = false;
    return config;
  },
};

注意修改public/index.html的挂在div

引入子应用

请按需在/src/App.js进行路由配置即可。

配置sub-app-2生命周期

export async function beforeLoad(props) {
  console.log("sub-app-2", "beforeLoad", props);
}

export async function load(props) {
  console.log("sub-app-2", "load", props);
}

export async function bootstrap(props) {
  console.log("sub-app-2", "bootstrap", props);
}

export async function beforeMount(props) {
  console.log("sub-app-2", "beforeMount", props);
}

export async function mount(props) {
  console.log("sub-app-2", "mount", props);
  render(props);
}

export async function afterMount(props) {
  console.log("sub-app-2", "afterMount", props);
}

export async function beforeUnmount(props) {
  console.log("sub-app-2", "beforeUnmount", props);
}

export async function unmount(props) {
  console.log("sub-app-2", "unmount", props);
  const { container } = props;

  const rootContainer = container
    ? container.querySelector(rootId)
    : document.querySelector(rootId);

  root.unmount(rootContainer);
}

export async function afterUnmount(props) {
  console.log("sub-app-2", "afterUnmount", props);
}

export async function unload(props) {
  console.log("sub-app-2", "unload", props);
}

export async function update(props) {
  console.log("sub-app-2", "update", props);
  render(props);
}

接收数据

sub-app-3(umi4)配置为微应用

注册为微应用

插件注册(.umirc.ts)

export default {
  qiankun: {
    slave: {},
  },
};

引入子应用

import { defineConfig } from '@umijs/max';

export default defineConfig({
  routes: [
    {
      path: '/',
      redirect: '/home',
    },
    {
      name: '首页',
      path: '/home',
      component: './Home',
    },
    {
      name: 'sub-app-3/one',
      path: '/one',
      component: './One',
    },
    {
      name: 'sub-app-3/two',
      path: '/two',
      component: './Two',
    },
    {
      name: 'sub-app-3/three',
      path: '/three',
      component: './Three',
    },
    {
      name: 'sub-app-3/theme',
      path: '/theme',
      component: './Theme',
    },
  ],
});

配置sub-app-3生命周期

export const qiankun = {
  lifeCycles: {
    async beforeLoad(props: any) {
      console.log('sub-app-3', 'beforeLoad', props);
    },
    async load(props: any) {
      console.log('sub-app-3', 'load', props);
    },
    async bootstrap(props: any) {
      console.log('sub-app-3', 'bootstrap', props);
    },
    async beforeMount(props: any) {
      console.log('sub-app-3', 'beforeMount', props);
    },
    async mount(props: any) {
      console.log('sub-app-3', 'mount', props);
    },
    async afterMount(props: any) {
      console.log('sub-app-3', 'afterMount', props);
    },
    async beforeUnmount(props: any) {
      console.log('sub-app-3', 'beforeUnmount', props);
    },
    async unmount(props: any) {
      console.log('sub-app-3', 'unmount', props);
    },
    async afterUnmount(props: any) {
      console.log('sub-app-3', 'afterUnmount', props);
    },
    async unload(props: any) {
      console.log('sub-app-3', 'unload', props);
    },
    async update(props: any) {
      console.log('sub-app-3', 'update', props);
    },
  },
};