前置概念
monorepo主要将不同工程、依赖包管理在一个代码仓库。与此相对应的名次是polyrepo,是将不同工程和依赖包管理在多个代码仓库。
monorepo
monorepo相比于polyrepo来说,最大的有点就是解决了共享代码问题。
共享代码
假如日常工作中,有两个工程app、docs共用一个依赖包shared-utils。
在polyrepo方式下升级shared-utils流程如下:
- 升级shared-utils代码,并进行git commit
- 发布shared-utils到私有或者公共仓库
- 升级app的shared-utils依赖版本,并进行git commit
- 升级docs的shared-utils依赖版本,并进行git commit
- 准备app和docs工程的打包与发布
当工程和依赖非常多时,polyrepo方式下整个更新流程会非常繁琐、冗长且易出错。
在monorepo方式下升级shared-utils流程如下:
- 升级shared-utils代码,并进行git commit
- 准备app和docs工程的打包与发布
不需要shared-utils的version升级、发布,也不需要更改app、docs依赖版本的更新,工程app、docs直接获取同一代码仓库下的shared-utils最新代码,app、docs、shared-utils整个代码仓库只需要提交一次git commit就可完成本次更新。
统一任务操作
将所有代码放到一个仓库了,可以尽可能优化并统一相关脚本任务。
workspaces
monorepo的主要组成基础是工作空间(workspace)。
- workspaces:每个工程、依赖都有各自的工作workspace、package.json,各个workspace可以相互依赖(意思是可以相互引用,例如app和docs工程都可以使用shared-utils包)
- root workspace:整个monorepo在根目录下,还有一个根workspace、package.json。根workspace主要用来解决以下三个问题:
-
- 安装整个monorepo的依赖项目,比如:turbo
- 添加整个monorepo的任务脚本,不仅仅是单个workspace的脚本
- 存放整个工程的readme.md,说明整个monorepo怎么运行
monorepo是一种概念,具体需要通过包管理器(npm、pnpm、yarn 1、yarn 2等)来实现。本文主使用pnpm,通过pnpm来实现workspaces管理与依赖安装。
pnpm vs npm vs yarn:
不要问,问就是pnpm快,节约时间,节约生命。
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 的更多功能:
参考
开发
设置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的库来解决此问题。
- Install dotenv-cli in your root workspace:
# Installs dotenv-cli in the root workspace
pnpm add dotenv-cli --ignore-workspace-root-check
- 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
- 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.
- 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/auto和microsoft/beachball
初始化changesets
在根workspace安装changsets依赖:
pnpm add -Dw @changesets/cli
然后初始化changeset:
pnpm changeset init
更新版本&发布
- 在根workspace运行
pnpm changeset,在根目录下会产生.changeset目录包含changeset的变更目录 - 运行
pnpm changeset version更新package的版本,并且该包的changelog以及所有依赖该包的changelog文件都会进行更新。 - 包的版本已经升级,因此需要运行
pnpm install,将重新升级pnpm的lockfile并更新依赖。 - 执行
git add .git commit -m'xx: xx'提交代码. - 执行
pnpm publish -r,该命令将会发布所有已更新且允许发布的包。
后续每次发版更新,只需要重新执行上述的1、2、3、4、5即可。
参考
任务管理
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"
}
名字主要用来:
- 当前当前依赖包的名字
- 在其他workspace引入该包
- 发布包到私有/共有仓库
同时,可以使用组织或者用户范围去避免包命名冲突。
"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命令时,会进行以下变动:
- 检查workspaces中安装过的依赖
- 软链workspaces相关的依赖包到node_modules,保证能正常导入包的内容
- 网络下载其它依赖包
所以无论何时添加/删除工作区,或更改它们在文件系统上的位置,都需要从根目录重新运行安装命令,以重新设置工作区。
注:每次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)需要做的事情有:
- 新增 public-path.js 文件,用于修改运行时的 publicPath。什么是运行时的 publicPath ?。
注意:运行时的 publicPath 和构建时的 publicPath 是不同的,两者不能等价替代。
- 微应用建议使用 history 模式的路由,需要设置路由 base,值和它的 activeRule 是一样的。
- 在入口文件最顶部引入 public-path.js,修改并导出三个生命周期函数。
- 修改 webpack 打包,允许开发环境跨域和 umd 打包。
主要的修改就是以上四个,可能会根据项目的不同情况而改变。例如,你的项目是 index.html 和其他的所有文件分开部署的,说明你们已经将构建时的 publicPath 设置为了完整路径,则不用修改运行时的 publicPath (第一步操作可省)。
无 webpack 构建的微应用直接将 lifecycles 挂载到 window 上即可。
根据以上说明,将cra工程改造为微应用:
微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。
- 在 src 目录新增 public-path.js:
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
- 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;
- 入口文件 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);
}
- 修改 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);
},
},
};