WJCHumble / Blog

分享编程和生活(Sharing programming and life)
40 stars 1 forks source link

现代 Monorepo 工程技术选型,聊聊我的思考 #35

Open WJCHumble opened 2 years ago

WJCHumble commented 2 years ago

前言

相信很多关注 Monorepo 生态的同学,应该大都看过这篇文章 monorepo.tools,其中列举了现存的几个主流的 Monorepo 相关的工具:

相应地,在这篇文章中也对各类工具进行了一一介绍。并且,我相信每个看过这篇文章的同学,都会留下这么个疑问:这么多 Monorepo Tool,我要如何进行选型?

这里,我给出的答案是 PNPM + Turborepo + Changesets。那么,又为什么是这 3 者呢?下面,我将会分别围绕这 3 个技术展开,来一一解答这个选型的原因以及怎么做。

PNPM

PNPM 的动机(Motivation),如它在官方文档介绍的所说:“Saving disk space and boosting installation speed”,节省磁盘空间和提高安装速度。除开这个动机描述的显著优点外,PNPM 内置了对 Monorepo 的支持,并解决了很多令人诟病的问题。

其中,比较经典的就是 Phantom dependencies(幻影依赖)。由于,默认情况下 yarnnpm 安装的依赖都是会被提升。所以,有时候你可能会遇到 Monorepo 项目中的某个包中的 package.json 没有安装这个依赖,结果实际代码中却使用了这个依赖...

虽说,PNPM 可以解决这个问题,但是,默认情况下 PNPM 安装的依赖也是会被提升的。如果,需要 PNPM 禁止依赖提升,我们可以通过在 Monorepo 项目工作区下的 .npmrc 文件中 配置,例如只提升 lodash

hoist-pattern[]=*lodash*

当然,还有一些其他的问题,有兴趣的同学可以看 ELab 团队写的这篇文章《Monorepo 的这些坑,我们帮你踩过了!》

那么,在简单解答了为什么用 PNPM 后,下面我们来看一下要怎么用?

Workspace 配置

要使用 PNPM 的 Monorepo 很简单,只需要在 Monorepo 项目的工作区下新建 pnpm-workspace.yaml 文件并配置:

packages:
  - 'packages/**'

接下来,则是记忆常用依赖和多包任务执行相关的命令。由于,我们的技术选型中有 Turborepo,它会负责多包任务的执行。所以,这里只需要记忆常用依赖相关的命令

常用依赖相关命令

pnpm i

在 PNPM 中,安装依赖可以用 pnpm i 来完成。在 Monorepo 的场景下,默认情况下 pnpm i 会安装所有的依赖(包括 packages/*)。此外,pnpm i 还需要用到 3 个选项(Option):

pnpm remove

在 PNPM 中,删除在 package.json 中的某个依赖,可以用 pnpm remove 完成。它的选项(Option)使用和 pnpm i 大同小异。其中,不同地是当我们在工作区想要删除 packages 中所有包的 package.json 中的某个依赖的时候,需要使用 -r,例如移除所有包中的 lodash

pnpm remove lodash -r

当然,可能还有同学有一些其他的诉求,有兴趣的同学可以移步文档了解,这里不做展开。

Changesets

经常维护开源项目的同学都知道的一点,每次包(Package)的发布,需要修改 package.json 的 version 字段,以及同步更新一下本次发布修改的 CHANGELOG.md。

这么一来,就会凸显一个问题,每次发布都需要手动地去更新 version、更新 CHANGELOG.md,未免有点繁琐。并且,用过 Lerna 的同学,应该都知道 Lerna 内置了对这块的支持。

但是,无论是 PNPM 又或者是下面要说的 Turborepo 都不支持这块,所以 2 者的官方文档都给大家推荐了用于支持这块能力的工具,例如 ChangesetsBeachballAuto 等。

那么,这里我们要介绍的就是 Changesets。下面,我们来看一下在前面建好的 PNPM 的 Monorepo 项目中如何使用 Changesets。首先,需要执行在 Monorepo 项目的工作区下,执行如下 2 个命令:

pnpm i -DW @changesets/cli
pnpm changeset init

前者是安装 Changesets 的 CLI,后者是初始化 .changeset 文件夹以及对应的文件:

.changeset
  |-- config.json
  |__ README.md

这里,我们来看一下 config.json 文件:

{
  "$schema": "https://unpkg.com/@changesets/config@1.6.4/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "linked": [],
  "access": "restricted",
  "baseBranch": "master",
  "updateInternalDependencies": "patch",
  "ignore": []
}

除开 $schema 这个不需要修改的字段, config.json 文件中列了 7 个字段,各个字段分别代表的作用为:

async function getDependencyReleaseLine() {}

export default { getReleaseLine, getDependencyReleaseLine }


- `commit` 设置是否把执行 `changeset add` 或 `changeset publish` 操作时对修改用 Git 提交
- `linked` 设置共享版本的包,而不是独立版本的包,例如一个组件库中主题和单独的组件的关系,也就是修改 Version 的时候,共享的包需要同步一起更新版本
- `access` 设置执行 `npm publish` 的 `--access` 选项,通常情况下我们是公共的包,所以设置 `public` 即可(注意,它会被 package.json 中的 `access` 字段重写)
- `baseBranch` 设置默认的 Git 分支,例如现在 GitHub 的默认分支应该是 `main`
- `updateInternalDependencies` 设置互相依赖的包版本更新机制,它是一个枚举(`major|minor|patch`),例如设置为 `minor` 时,只有当依赖的包新了 `minor` 版本或者才会对应地更新 package.json 的 `dependencies` 或 `devDependencies` 中对应依赖的版本

- `ignore` 设置不需要发布的包,这些会被 Changesets 忽略

在初始化 .changeset 文件夹后,就可以正常使用 `changeset` 相关的命令,主要是这 3 个命令:

- `pnpm chageset` 用于生成本次修改的要添加到 CHANGELOG.md 中的描述
- `pnpm changeset version` 用于生成本次修改后的包的版本
- `pnpm changeset publish` 用于发布包

此外,如果是在业务场景下,我们通常需要把包发到公司**私有的 NPM Registry**,而这有很多种配置方式。但是,**需要注意**的是 Changesets 只支持在每个包中声明 `publicConfig.registry` 或者配置 `process.env.npm_config_registry`,对应的代码会是这样:

```javascript
// 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;
}

可以看到,如果在前面说的这 2 种情况下获取不到 registry 的话,Changesets 都是按公共的 Registry 去查找或者发布包的。

Turborepo

说起 Turborepo,可能大家会有点陌生。但是,对于 Vercel 我想大家都知道(毕竟 Rich HarrisSebastian Markbåge 等都加入了),Turbrepo 则是 Vercel 旗下的一个开源项目。Turborepo 是用于为 JavaScript/TypeScript 的 Monorepo 提供一个极快的构建系统,简单地理解就是用 Turborepo 来执行 Monorepo 项目的中构建(或者其他)任务会非常快

关于 Turborepo 其他优势,其官方文档写的很详尽,有兴趣的同学可以自行了解~

所以,你可以理解成是选择 Turborepo 负责 Monorepo 项目多包任务执行的原因。而在 Turborepo 中执行多包任务是通过 turbo run <script>。不过,turbo runlerna run 直接使用有所不同,它需要配置 turbo.json 文件,注册每个需要执行的 script 命令。

在 Turborepo 中有个 Pipelines 的概念,它是由 turbo.json 文件中的 pipline 字段的配置描述,它会在执行 turbo run 命令的时候,根据对应的配置进行有序的执行缓存输出的文件

举个例子,通常情况下我们一个 Monorepo 项目中的每个包可能会有 devbuildtestclean 等 4 个命令,那么对应的 turbo.json 的配置会是这样:

{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "clean": {
      "dependsOn": ["^clean"]
    },
    "test": {
      "dependsOn": ["build", "lint"]
    },
    "dev": {
      "cache": false
    }
  }
}

可以看到,pipeline 中的每个 key 则对应着每个需要执行的 turbo run 命令的名称,其中 dependsOnoutputscache 等 3 个字段分别作用为:

这样一来,我们就可以使用诸如 turbo run build test 的命令,它则会按 pipeline 的配置依次执行对应的命令。

当然,如果你想每个命令都支持单独执行,可以直接配置为 {} 即可。此外,如果要使用 turbo run 命令,还需要在 package.json 中声明 packageManage 字段为指定的包管理工具及版本,例如 "packageManager": "pnpm@6.30.0"

结语

阅读到此处,我想大家应该理解了 PNPM + Turborepo + Changesets 这个技术选型的原因以及要怎么做。当然,这个选型只是我个人的思考所得出的答案,相信也有同学仍然钟情于 Lerna,又或者喜欢 Rush 一把梭,这些观点并无对错,本质上这也是编程的魅力所在,各个轮子都有其存在的价值

最后,如果文中存在表达不当或错误的地方,欢迎各位同学评论交流~

hooper-hc commented 1 year ago

turborepo 确实好用,我也选的: PNPM + Turborepo + Changesets这个组合。

不过可以看看 nx ,基本能够 cover turbo 的所有功能,现在 lerna5+ 基本都是调用 nx 的基础能力。

其他几个都不好用。

hooper-hc commented 1 year ago

Vercel 团队成员可都太夸张了,大佬云集

Guillermo Rauch(创始人): socket.io / mongoose 的作者 Sebastian Markbage : 原 React 团队 Tech Lead Rich Harries: sveltejs 作者 & rollup 作者 Donny/강동윤:swc作者 Tobias Koppers : webpack 作者 Schlez : 原 Wix 工程师 & fnm 作者 Alexander Akait : webpack 核心贡献者 & prettier 贡献者 Jared Palmer : Turborepo 创始人 Ethan Arrowood : nodejs & fastifyjs 库维护者 Broooooklyn : napi.rs 作者

hooper-hc commented 1 year ago

对了,你为啥用 issue 写博客啊

WJCHumble commented 1 year ago

@hooper-hc Issue 算是一个存档,掘金、思否和知乎都有(搜五柳)