该小节主要知识点:
- 什么是
Monorepo? - ⭐️⭐️⭐️⭐ 搭建 mini-umi 项目并
实践Monorepo组织管理 - Monorepo 中的
构建顺序问题 与循环依赖 Turborepo高性能构建系统 接入与开源项目实践
本小节所有代码已放入 mini-umi 仓库
/juejin/mini-umi目录 下
什么是 Monorepo ?
Monorepo 是一种组织管理代码的方式,指在一个 git 仓库中管理多个项目
项目结构一般为:
.
├── package.json
└── packages
├── packageA
├── packageB
└── packageC
这样管理仓库有什么 好处 呢?
- Monorepo 可以通过
workspace快速软链接link,方便 npm包 的本地开发 - 可以使用
统一的工程化配置如 tsconfig eslint prettier 等 - 抽取
公共依赖减少重复安装
搭建 mini-umi 项目并实践 Monorepo 组织管理
1.根目录搭建
新建项目目录
mkdir mini-umi
code mini-umi
搭建项目骨架
pnpm init // 新建 package.json
pnpm i typescript -d // 安装 typescript
tsc --init // 新建 tsconfig 文件
修改tsconfig.json
{
"compilerOptions": {
"strict": true,
"declaration": true,
"skipLibCheck": true,
"baseUrl": "./",
"moduleDetection": "auto",
"esModuleInterop": true
}
}
使用 father 代替 tsc
什么是 father ?
father 是 蚂蚁体验技术部推出的一款 NPM 包研发工具,能够帮助开发者更高效、高质量地研发 NPM 包、生成构建产物、再完成发布。它主要具备以下特性:
- ⚔️ 双模式构建: 支持 Bundless 及 Bundle 两种构建模式,ESModule 及 CommonJS 产物使用 Bundless 模式,UMD 产物使用 Bundle 模式
- 🎛 多构建核心: Bundle 模式使用 Webpack 作为构建核心,Bundless 模式支持 esbuild、Babel 及 SWC 三种构建核心,可通过配置自由切换
- 🔖 类型生成: 无论是源码构建还是依赖预打包,都支持为 TypeScript 模块生成 .d.ts 类型定义
- 🚀 持久缓存: 所有产物类型均支持持久缓存,二次构建或增量构建只需『嗖』的一下
- 🩺 项目体检: 对 NPM 包研发常见误区做检查,让每一次发布都更加稳健
- 🏗 微生成器: 为项目追加生成常见的工程化能力,例如使用 jest 编写测试
- 📦 依赖预打包: 开箱即用的依赖预打包能力,帮助 Node.js 框架/库提升稳定性、不受上游依赖更新影响(实验性) 以上摘自 father-#README.md
为什么要使用father?
因为他内置了 Bundless 和 Bundle 两种构建模式,而且内置有很方便的脚手架
tips:father 也是基于Umi微内核架构开发的
这里我们不使用 father 内置脚手架
npm i father
新建 .fatherrc.ts 配置文件
import { defineConfig } from "father";
export default defineConfig({
cjs: {
output: 'dist',
sourcemap: true
}
})
新建 src/index.ts
export const yang = 'yang'
console.log(yang);
修改 package.json
{
"name": "mini-umi",
"version": "1.0.0",
"description": "a simple model for Umi",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"dev": "father dev" // 与 tsc --watch 类似
},
"keywords": [],
"author": "洋",
"license": "ISC",
"dependencies": {
"father": "^4.1.0"
}
}
运行 dev 脚本
npm run dev
效果为下图即算成功
2.创建 workspace 工作区
在工作区内的 package 可以很方便的使用软链接引用
# pnpm-workspace.yaml(根目录)
packages:
- 'packages/*'
3.子包搭建
删除src,新建packages目录 在 package目录下 创建如下结构:
.
├── package.json
└── packages
├── packageA
├── packageB
└── packageC
这里与根目录搭建基本一致 大家自己试着创建一下
最终效果如下:
问题1:根目录中的 .fatherrc.ts 为什么要改名为 .fatherrc.base.ts (内容不变)
因为我们这边子目录中的 .fatherrc.ts 配置文件要继承根目录中的配置
import { defineConfig } from "father";
export default defineConfig({
extends: '../../.fatherrc.base.ts'
})
问题2:为什么 tsconfig 不需要在每个子项目里面再写一份呢?
还记得 Monorepo 有个优点之一 可以使用 统一的工程化配置, 像tsconfig我们只需要在根目录写一份即可在所有包里生效
4.使用workspace
1.在所有子项目 package.json 中加入构建脚本
"scripts": {
"dev": "father dev",
+ "build": "father build"
},
2.在根目录 package.json 中加入
"scripts": {
"dev": "father dev",
+ "build:all": "pnpm run -r build"
},
3.在根目录运行脚本
npm run build:all
效果如下,所有子包全部打包产物 dist 目录成功,说明 workspace 设置成功
原理:pnpm run -r xxx
-r 指的是递归 所以这里会递归执行所有workspace里的 xxx 命令,上述示例中即为 build 命令
⭐️⭐️⭐️⭐ 核心:4.使用软链接
1.修改 A包 配置
//package/packageA/package.json
"dependencies": {
+ "package-b": "workspace:*"
}
这里修改完毕依赖之后一定要重新安装依赖 pnpm i
出现链接标记即算成功 点开观察发现 package-b 就是我们旁边的 B包
2.修改 A包 内容
// package/packageA/src/index.ts
import { yang } from 'package-b'
console.log(yang);
3.修改 B包 内容
export const yang = 'yang from b'
4.也别忘了修改 B包 导出的入口文件
// package/packageB/package.json
- "main": "index.js",
+ "main": "./dist/index.js",
5.在根目录打包所有子包产物
npm run build:all
6.在A包中
node dist/index.js
到这里,我们 mini-umi 的 Monorepo 仓库就算是基本搭建成功了,是不是很简单(〃'▽'〃)
先接着往下看
Monorepo 中的 构建顺序问题 与 循环依赖
上述步骤中的 2-4-5 其实是很方便的
5.在根目录打包
所有子包产物
npm run build:all
问题一:构建顺序问题:
前面提到了 npm run build:all 时 其实执行了 pnpm run -r build
它会去自动分析依赖关系,得到递归的顺序
你会发现 它先执行了 B包的build 再去执行 A包的Build
这明显符合我们的预期
因为我们的 A包 依赖于 B包的产物,所以正确顺序应该是 先build出 B包的产物,再build A包
但是,要是不使用 pnpm run -r build,该怎么解决构建顺序的问题呢?
如何解决构建顺序问题呢?
1. 手动 先build A包 再build B包
优势:可控
劣势:管理的子包一旦庞大,可能要手动 build 几十次
2. 写不同的脚本组合
这个方案就比较多了,你可以写各种各样的脚本组合在一起去保证他的构建顺序
优势:方案较多
劣势:要写脚本 我懒
3. 使用可分析的构建--Turborepo
当我们使用 Turborepo 后他会去自动分析包的引用关系(这与pnpm run -r build是一致的),从而得到正确的构建顺序
下一小节将为我们的项目引入 Turborepo
问题二:循环依赖问题
循环依赖的例子很简单:
A包依赖了B包 B包依赖了A包
这个问题乍一看很不正常 傻子才会这么用吧 但是确实有可能出现 比方说在引用 ts 类型 的时候
其实 pnpm workspace 是可以支持循环依赖的
问题在于 Trubo 不支持循环依赖
所以我们在 Monorepo 仓库中应尽量避免出现 循环依赖 的问题
Turborepo - 高性能构建系统 接入与实践
什么是 Turborepo ?
Turborepo is a
high-performancebuild system for JavaScript and TypeScript codebases. Turborepo 是一个用于 JavaScript 和 TypeScript 代码库的“高性能”构建系统。
通俗一点讲,他是我们在 Monorepo 仓库中的一种优化构建的方案
这一点一定要区分于 Mutirepo 和 Monorepo 的代码组织方案
使用Turborepo有什么好处呢?
官方的表述是:
Turborepo leverages advanced build system techniques to speed up development, both on your local machine and your CI/CD.
1.Never do the same work twice
Turborepo remembers the output of any task you run - and can skip work that's already been done.
翻译过来就是:
1.构建缓存 -- 而且可以远程缓存,这在团队开发十分有用
2.智能调度构建加速,减少空闲CPU
3.通过 Pipeline 定义任务之间的关系,加快构建速度
4.方便快捷的配置文件
当然,和Monorepo一样,当你去执行 npm run build:all 这种构建脚本时,Turborepo会为你自动根据它们的依赖关系,达到最优的构建顺序
如何为一个项目接入 Turborepo?
其实这部分还是比较简单的,大致分为以下几步:
1.确定你的 Monorepo 项目中的 workspace 正常
因为接下来 Turborepo 构建是要基于我们的 workspace
2.安装Turbo
pnpm add turbo -Dw
3.配置 Turbo 的配置文件
//turbo.json 这里是一套完整的官方示例
{
"$schema": "https://turbo.build/schema.json",
"pipeline": { // 这是上面提到的任务管道
"build": {
// A package's `build` script depends on that package's
// dependencies and devDependencies
// `build` tasks being completed first
// (the `^` symbol signifies `upstream`).
"dependsOn": [
"^build"
],
// note: output globs are relative to each package's `package.json`
// (and not the monorepo root)
"outputs": [
".next/**"
]
},
"test": {
// A package's `test` script depends on that package's
// own `build` script being completed first.
"dependsOn": [
"build"
],
"outputs": [],
// A package's `test` script should only be rerun when
// either a `.tsx` or `.ts` file has changed in `src` or `test` folders.
"inputs": [
"src/**/*.tsx",
"src/**/*.ts",
"test/**/*.ts",
"test/**/*.tsx"
]
},
"lint": {
// A package's `lint` script has no dependencies and
// can be run whenever. It also has no filesystem outputs.
"outputs": []
},
"deploy": {
// A package's `deploy` script depends on the `build`,
// `test`, and `lint` scripts of the same package
// being completed. It also has no filesystem outputs.
"dependsOn": [
"build",
"test",
"lint"
],
"outputs": []
}
}
}
4.添加.gitignore
将它的缓存文件 ignore 掉,防止上传到 Github 仓库
// gitignore
+ .turbo
5.替换构建脚本
- ”build“: "xxxx"
+ "build": "turbo run build"
6.(可选)远程构建缓存
npx turbo login // 登录远程缓存账号
npx turbo link // 将当前构建仓库与远端 link
远程构建缓存 在团队协同开发的时候能提升不少的速度
到这里,我们的 Turborepo 接入基本上就完成了,只需要执行 npm run build 即可看到效果
下面是 开源项目 实战
Turborepo 实战案例
这边我用蚂蚁AntV团队新开源的 GraphInsight 为例接入 Turborepo
这是他现在的 package.json
"scripts": {
"preinstall": "npx only-allow pnpm",
"postinstall": "npm run build:all:es",
"build:all:es": "pnpm run -r build:es",
"start": "cd packages/gi-site && npm run start",
"gi-common-component": "cd packages/gi-common-components && npm run build:es",
"gi-sdk": "cd packages/gi-sdk && npm run build:es",
"gi-assets-basic": "cd packages/gi-assets-basic && npm run build:es",
"gi-assets-advance": "cd packages/gi-assets-advance && npm run build:es",
"gi-assets-algorithm": "cd packages/gi-assets-algorithm && npm run build:es",
"gi-assets-scene": "cd packages/gi-assets-scene && npm run build:es",
"gi-assets-graphscope": "cd packages/gi-assets-graphscope && npm run build:es",
"gi-assets-neo4j": "cd packages/gi-assets-neo4j && npm run build:es",
"gi-assets-tugraph": "cd packages/gi-assets-tugraph && npm run build:es",
"gi-theme-antd": "cd packages/gi-theme-antd && npm run build:es",
"build": "npm run build:site && npm run move:dist",
"build:assets": "cd packages/gi-assets-basic && NODE_OPTIONS=--max_old_space_size=2048 npm run build",
"build:basicAssets": "cd packages/gi-assets-basic && NODE_OPTIONS=--max_old_space_size=2048 npm run build",
"build:core": "cd packages/gi && NODE_OPTIONS=--max_old_space_size=2048 npm run build",
"build:site": "cd packages/gi-site && pnpm install && NODE_OPTIONS=--max_old_space_size=2048 npm run build",
"build:testing": "cd packages/gi-assets-testing && NODE_OPTIONS=--max_old_space_size=2048 npm run build",
"clean:all": "pnpm run -r clean",
"core": "cd packages/gi && npm run start",
"move:dist": "node ./scripts/deploy.js",
"site": "cd packages/gi-site && NODE_OPTIONS=--max_old_space_size=2048 npm run start"
},
之前是
这里的 build:all:es 为了保障执行顺序,属实是 '辛苦' 它了,所以我给他提了pr,将他用 pnpm run -r build:es 替换掉了
现在我们使用 Turborepo 替换掉它
1.安装 Turbo
pnpm add turbo -Dw
2.加上配置文件 Pipeline
// turbo.json
+{
+ "$schema": "https://turbo.build/schema.json",
+ "pipeline": {
+ "build:es": {
+ "dependsOn": [
+ "^build:es"
+ ],
+ "outputs": [
+ "es/**",
+ "lib/**"
+ ]
+ }
+ },
+ "globalDependencies": [
+ ".prettierrc.js"
+ ]
+}
3.替换构建脚本
// package.json
- "build:all:es": "pnpm run -r build:es",
+ "build:all:es": "turbo run build:es",
4.缓存文件加入gitignore
// gitignore
+ .turbo
执行 "npm run build:all:es" 即可看到效果:
⭐️ 替换之前的 "pnpm run -r build:es"
共计用时:5.1+9.5+10.2+28.2+20.4+26 s
⭐️ 替换之后的 "turo run build:es" 第一次构建+缓存
共计用时 44 s
⭐️ 缓存之后的 "turo run build:es" 构建
平均时间: 800+ms
从 40s+ 优化到了 1s 以下,只能说非常香了
直接给 AntV 提交 Pr
小结:
本小节我们
1.首先为大家介绍了什么是 Monorepo。它其实是一种代码仓库的组织管理方式,通过Monorepo的管理,我们可以很方便的在 workspace 中开发本地 npm库,统一规范配置等
2.通过代码带着大家搭建好了一个实用的 Monorepo仓库,防止大家踩坑,并学会了一些Monorepo开发中的小技巧,也引出了我们下文中的 pnpm构建顺序问题 以及 Turborepo
3.为大家介绍了 pnpm 使用 Monorepo 管理仓库出现的两个问题:构建顺序以及循环依赖问题以及它们的解决方案
4.引入了现代高性能构建方案-Turborepo,简述了如何在一个 Monorepo 项目中接入Turbo,并以蚂蚁体验技术部开源产品 GraphInsight 为了进行实战接入演示效果
下一小节
小节思考:
1.Monorepo 管理仓库 究竟有哪些好处呢?它与传统 Mutirepo仓库 有哪些优劣你能说的上来吗
2.你能在我们的新建的 mini-umi 项目中接入 Turborepo 吗,试一下吧
3.你知道新建 .fatherrc.ts 配置文件的时候 为什么 export default 后面要使用一个defineConfig 函数吗
2.1 已知信息一:defineConfig函数 参数是 config 返回值也是config
2.2 已知信息二:vite 配置文件中也是同样的用法
import { defineConfig } from "father";
export default defineConfig({
cjs: {
output: 'dist',
sourcemap: true
}
})