新聞中心
前言
相信很多關(guān)注 Monorepo 生態(tài)的同學(xué),應(yīng)該大都看過(guò)這篇文章 monorepo.tools [1] ,其中列舉了現(xiàn)存的幾個(gè)主流的 Monorepo 相關(guān)的工具:

- Bazel (by Google) [2]
- Lage (by Microsoft) [3]
- Lerna [4]
- Nx (by Nrwl) [5]
- Rush (by Microsoft) [6]
- Turborepo (by Vercel) [7]
相應(yīng)地,在這篇文章中也對(duì)各類(lèi)工具進(jìn)行了一一介紹。并且,我相信每個(gè)看過(guò)這篇文章的同學(xué),都會(huì)留下這么個(gè)疑問(wèn): 這么多 Monorepo Tool,我要如何進(jìn)行選型?
這里,我給出的答案是 PNPM + Turborepo + Changesets。那么,又為什么是這 3 者呢?下面,我將會(huì)分別圍繞這 3 個(gè)技術(shù)展開(kāi),來(lái)一一解答這個(gè)選型的原因以及怎么做。
PNPM
PNPM 的動(dòng)機(jī)( Motivation [8] ),如它在官方文檔介紹的所說(shuō): “Saving disk space and boosting installation speed”,節(jié)省磁盤(pán)空間和提高安裝速度 。除開(kāi)這個(gè)動(dòng)機(jī)描述的顯著優(yōu)點(diǎn)外, PNPM 內(nèi)置了對(duì) Monorepo 的支持 [9] ,并解決了很多令人詬病的問(wèn)題。
其中,比較經(jīng)典的就是 Phantom dependencies(幻影依賴(lài))。由于,默認(rèn)情況下 yarn 、 npm 安裝的依賴(lài)都是會(huì)被提升。所以,有時(shí)候你可能會(huì)遇到 Monorepo 項(xiàng)目中的某個(gè)包中的 package.json 沒(méi)有安裝這個(gè)依賴(lài),結(jié)果實(shí)際代碼中卻使用了這個(gè)依賴(lài)...
雖說(shuō),PNPM 可以解決這個(gè)問(wèn)題,但是, 默認(rèn)情況 下 PNPM 安裝的依賴(lài)也是會(huì)被提升的。如果,需要 PNPM 禁止依賴(lài)提升,我們可以通過(guò)在 Monorepo 項(xiàng)目工作區(qū)下的 .npmrc 文件中 配置 [10] ,例如只提升 lodash :
hoist-pattern[]=*lodash*
當(dāng)然,還有一些其他的問(wèn)題,有興趣的同學(xué)可以看 ELab 團(tuán)隊(duì)寫(xiě)的這篇文章 《Monorepo 的這些坑,我們幫你踩過(guò)了!》 [11] 。
那么,在簡(jiǎn)單解答了為什么用 PNPM 后,下面我們來(lái)看一下要怎么用?
Workspace 配置
要使用 PNPM 的 Monorepo 很簡(jiǎn)單,只需要在 Monorepo 項(xiàng)目的工作區(qū)下新建 pnpm-workspace.yaml 文件并配置:
packages:
- 'packages/**'
接下來(lái),則是記憶常用依賴(lài)和多包任務(wù)執(zhí)行相關(guān)的命令。由于,我們的技術(shù)選型中有 Turborepo,它會(huì)負(fù)責(zé)多包任務(wù)的執(zhí)行。所以,這里只需要記憶 常用依賴(lài)相關(guān)的命令 。
常用依賴(lài)相關(guān)命令
pnpm i
在 PNPM 中,安裝依賴(lài)可以用 pnpm i 來(lái)完成。在 Monorepo 的場(chǎng)景下,默認(rèn)情況下 pnpm i 會(huì)安裝所有的依賴(lài)(包括 packages/* )。此外, pnpm i 還需要用到 3 個(gè)選項(xiàng)(Option):
- --filter
,安裝依賴(lài)到指定的 package,不聲明要安裝的依賴(lài)包則默認(rèn)安裝 package.json 中的所有依賴(lài) - --prod, P,安裝依賴(lài)到 dependencies
- --dev, D,安裝依賴(lài)到 devDependencies
pnpm remove
在 PNPM 中,刪除在 package.json 中的某個(gè)依賴(lài),可以用 pnpm remove 完成。它的選項(xiàng)(Option)使用和 pnpm i 大同小異。其中,不同地是當(dāng)我們?cè)诠ぷ鲄^(qū)想要?jiǎng)h除 packages 中所有包的 package.json 中的某個(gè)依賴(lài)的時(shí)候,需要使用 -r ,例如移除所有包中的 lodash :
pnpm remove lodash -r
當(dāng)然,可能還有同學(xué)有一些其他的訴求,有興趣的同學(xué)可以移步文檔了解,這里不做展開(kāi)。
Changesets
經(jīng)常維護(hù)開(kāi)源項(xiàng)目的同學(xué)都知道的一點(diǎn),每次包(Package)的發(fā)布,需要修改 package.json 的 version 字段,以及同步更新一下本次發(fā)布修改的 CHANGELOG.md。
這么一來(lái),就會(huì)凸顯一個(gè)問(wèn)題,每次發(fā)布都需要手動(dòng)地去更新 version 、更新 CHANGELOG.md,未免 有點(diǎn)繁瑣 。并且,用過(guò) Lerna 的同學(xué),應(yīng)該都知道 Lerna 內(nèi)置了對(duì)這塊的支持。
但是,無(wú)論是 PNPM 又或者是下面要說(shuō)的 Turborepo 都不支持這塊,所以 2 者的官方文檔都給大家推薦了用于支持這塊能力的工具,例如 Changesets [12] 、 Beachball [13] 、 Auto [14] 等。
那么,這里我們要介紹的就是 Changesets。下面,我們來(lái)看一下在前面建好的 PNPM 的 Monorepo 項(xiàng)目中如何使用 Changesets。首先,需要執(zhí)行在 Monorepo 項(xiàng)目的工作區(qū)下,執(zhí)行如下 2 個(gè)命令:
pnpm i -DW @changesets/cli
pnpm changeset init
前者是安裝 Changesets 的 CLI,后者是初始化 .changeset 文件夾以及對(duì)應(yīng)的文件:
.changeset
|-- config.json
|__ README.md
這里,我們來(lái)看一下 config.json [15] 文件:
{
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"linked": [],
"access": "restricted",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": []
}除開(kāi) $schema 這個(gè)不需要修改的字段, config.json 文件中列了 7 個(gè)字段,各個(gè)字段分別代表的作用為:
- changelog 設(shè)置 CHANGELOG.md 生成方式,可以設(shè)置 false 不生成,也可以設(shè)置為定義生成行為的文件地址或依賴(lài)名稱(chēng),例如 Changsets 提供的 `changelog-git` [16] 。其中,定義生成行為的文件固定代碼模版為:
async function getReleaseLine() {}
async function getDependencyReleaseLine() {}
export default {
getReleaseLine,
getDependencyReleaseLine
}- commit 設(shè)置是否把執(zhí)行 changeset add 或 changeset publish 操作時(shí)對(duì)修改用 Git 提交
- linked 設(shè)置共享版本的包,而不是獨(dú)立版本的包,例如一個(gè)組件庫(kù)中主題和單獨(dú)的組件的關(guān)系,也就是修改 Version 的時(shí)候,共享的包需要同步一起更新版本
- access 設(shè)置執(zhí)行 npm publish 的 --access 選項(xiàng),通常情況下我們是公共的包,所以設(shè)置 public 即可(注意,它會(huì)被 package.json 中的 access 字段重寫(xiě))
- baseBranch 設(shè)置默認(rèn)的 Git 分支,例如現(xiàn)在 GitHub 的默認(rèn)分支應(yīng)該是 main
- updateInternalDependencies 設(shè)置互相依賴(lài)的包版本更新機(jī)制,它是一個(gè)枚舉( major|minor|patch ),例如設(shè)置為 minor 時(shí),只有當(dāng)依賴(lài)的包新了 minor 版本或者才會(huì)對(duì)應(yīng)地更新 package.json 的 dependencies 或 devDependencies 中對(duì)應(yīng)依賴(lài)的版本
- ignore 設(shè)置不需要發(fā)布的包,這些會(huì)被 Changesets 忽略
在初始化 .changeset 文件夾后,就可以正常使用 changeset 相關(guān)的命令,主要是這 3 個(gè)命令:
- pnpm chageset 用于生成本次修改的要添加到 CHANGELOG.md 中的描述
- pnpm changeset version 用于生成本次修改后的包的版本
- pnpm changeset publish 用于發(fā)布包
此外,如果是在業(yè)務(wù)場(chǎng)景下,我們通常需要把包發(fā)到公司 私有的 NPM Registry ,而這有很多種配置方式。但是, 需要注意 的是 Changesets 只支持在每個(gè)包中聲明 publicConfig.registry 或者配置 process.env.npm_config_registry ,對(duì)應(yīng)的代碼會(huì)是這樣:
// https://github.com/changesets/changesets/blob/main/packages/cli/src/commands/publish/npm-utils.ts
function getCorrectRegistry(packageJson?: PackageJSON): string {
const registry =
packageJson?.publishConfig?.registry ?? process.env.npm_config_registry;
return !registry || registry === "https://registry.yarnpkg.com"
? "https://registry.npmjs.org"
: registry;
}
可以看到,如果在前面說(shuō)的這 2 種情況下獲取不到 registry 的話(huà),Changesets 都是按公共的 Registry 去查找或者發(fā)布包的。
Turborepo
說(shuō)起 Turborepo,可能大家會(huì)有點(diǎn)陌生。但是,對(duì)于 Vercel [17] 我想大家都知道(畢竟 Rich Harris [18] 、Sebastian Markb?ge 等都加入了),Turbrepo 則是 Vercel 旗下的一個(gè)開(kāi)源項(xiàng)目。Turborepo 是用于為 JavaScript/TypeScript 的 Monorepo 提供一個(gè)極快的構(gòu)建系統(tǒng),簡(jiǎn)單地理解就是用 Turborepo 來(lái)執(zhí)行 Monorepo 項(xiàng)目的中構(gòu)建(或者其他)任務(wù)會(huì) 非???!
關(guān)于 Turborepo 其他優(yōu)勢(shì),其 官方文檔 [19] 寫(xiě)的很詳盡,有興趣的同學(xué)可以自行了解~
所以,你可以理解成 快 是選擇 Turborepo 負(fù)責(zé) Monorepo 項(xiàng)目多包任務(wù)執(zhí)行的原因。而在 Turborepo 中執(zhí)行多包任務(wù)是通過(guò) turbo run


咨詢(xún)
建站咨詢(xún)