2022 该怎么选择 monorepo 的管理工具(下)

5,648 阅读8分钟

前言

在上一篇文章中:2022 该怎么选择 monorepo 的管理工具(上),我详细介绍了 Nx 和 Turborepo 两个适用 monorepo 工程管理方式可以称之为 build system 的工具,并分析了它们的一些优势以及大概的原理,也简单介绍了 build system 是什么。

每个优秀的工具都有其优势和不足的地方,选择工具的时候没有绝对的好坏之分,只有适不适合。综合权衡工具的学习成本、优点和缺点,选出当下或者未来一段时间能满足团队需要的工具才是真理。

话不多说,本文会继续介绍 lerna@5 和 Pnpm + changesets,下面开始正文。

Lerna@5

当 lerna@4 之前的 maintainer 宣布因为要 “退休” 不再维护 lerna 的时候,社区还是引起了一阵小波动,因为 lerna 作为近几年比较受欢迎的 monorepo 管理工具,还是有不错的用户数量的,前端社区比较出名的一些开源项目:Jest、Vue-cli、React、Webpack-cli 等就是基于 lerna 管理的。

不过最后 Nx 背后的团队 nrwl 接手了,并在 lerna@4 的基础上,开发了 lerna@5。

两年前,我开始使用 lerna 的时候,我的第一感觉是,开箱即用,学习成本低:

  • lerna init,初始化一个基于 lerna 管理的 monorepo 项目,当然你也可以使用 --independent指定项目下的包版本管理类型;
  • lerna bootstrap,链接所有的 package 并且安装每个 package 中的 dependencies;
  • lerna run build,运行所有 package 的 build script;
  • lerna version --conventional-commits,根据 Conventional Commit 规范自动升级 package 的版本、打 git tag,并且生成 changelog;
  • lerna publish from-git,消费 lerna version 生成的 git tag,然后将相关改动的 package 发布到 NPM 上

简单的几个命令,就能完成一个 monorepo 的工作流,而且无论是本地使用还是与 CI 整合,使用的方式几乎没有很大的差别。所以对于中小型的 monorepo 项目,如果对于运行任务的性能没有太大的要求还是非常不错的。

那么 lerna@5 有什么不一样了?

nrwl 团队接手后,为了更好的推广 Nx,直接将 lerna 与 Nx 整合,所以有了 lerna@5

当然最大的优点还是提升了 lerna 运行任务的速度,来看一张 benchmark 图感受下:

在一个大型的 monorepo 中,运行 build 任务的时间,Lerna + Nx 比 Turborepo 快 5倍左右

在 v5.5.0 版本,也支持了结合 Pnpmp 包管理器一起使用。


所以,综合来看,lerna 是一个非常轻量、运行速度快、学习成本低的 monorepo 管理工具。

Pnpm + Changesets

Pnpm

如果使用过 Pnpm 的小伙伴,它是作为 Yarn 和 NPM 的替代品角色出现的。我们知道 Yarn 和 NPM 有个很大的痛点是在当前项目下的安装的所有依赖都会提升在项目的根目录下维护,而且不同项目的 node_modules 下的依赖是没法共享的,这就造成两个严重的问题:

1.随着个人电脑上的项目越多,依赖越复杂,node_modules 占用的磁盘空间就越大;

2.随着依赖越多,安装速度也会越来越慢。


Pnpm 通过 Symlinked 架构的方式,通过一个 store 中心化管理所有项目的依赖:

从而避免重复安装相同版本的依赖问题,所以提升了安装速度的时候,也节省了磁盘空间。


感兴趣的小伙伴可以通过访问它的官网,了解更多:Pnpm

我们回到主题,Pnpm 作为一个包管理工具,也是支持 workspace 的,也就是说,它也支持用来管理 monorepo 项目。

怎么使用 Pnpm 来搭建一个 monorepo 了?

首先你需要在目录下创建一个 pnpm-workspace.yaml :

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

当然,别忘了,同时也要有创建一个 .npmrc文件。

假如在 monorepo 中有一个 package 命名为 foo,它依赖了本地的 package bar,则声明依赖的时候需要这样指定:

{
    "dependencies": {
        "bar": "workspace:^1.5.0",
    }
}

使用基于 workspace 管理各 package 的任务:

  • pnm run build -- parallel,运行各 package 下的 build script;
  • pnpm exec jest -r,在各 package 下执行 jest ;
  • pnpm test/start,运行各 package 下的 test/start script。

你会发现,如果想要做一个需要发布的工作流的项目貌似少了点什么。没错,少了 bump version 、生成 changelog、发布到 NPM 等配套的发布工作流。

所以,你需要 changesets。

Changesets

一个 changeset 就是我们在开发完一个功能或者完成一个修复后,可以作为 semver bump 时消费的包含变更内容简要描述的抽象,一般是以一个 md 文件形式存储在当前项目的 .changeset 目录下。

让我们用一个图来了解下它大概的使用流程:

看中间虚线圈起来的发布工作流,主要是分三步完成:

  1. 在完成一个功能开发后,在 release 分支执行 npx changeset,然后就会在当前目录下生成一个随机名称的 UNIQUE_ID.md 文件,这个文件就是 changeset 的实体,里面的内容大概是这样:
---
"@myproject/core": minor
---

add a feature
  1. 执行 npx changeset version,changesets 会消费 .changeset 目录下的 md 文件,用来 bump version 并生成 changelog;
  2. 执行 npx changeset publish,将上一步变更的 packages 发布到 NPM 上。

这个工作流相比 lerna 的会多一步添加 changeset 的过程,但是可以更加灵活地定制你的发布工作流。

目前它与 Pnpm 结合地比较好,可以这样使用:

  1. pnpm changeset init,先进行 changeset 初始化;
  2. pnpm changeset,生成一个 changeset;
  3. pnpm version,消费 changeset,bump version 和生成 changelog;
  4. pnm publish -r,发布 workspace 下的所有变更的 packages。

你也可以结合社区的 github action,在 CI 中集成这个工作流,做到完全自动化的发布:

name: Bump version and publish
on:
  push:
    branches:
      - main
env:
  CI: true
  PNPM_CACHE_FOLDER: .pnpm-store
jobs:
  version:
    timeout-minutes: 15
    runs-on: ubuntu-latest
    steps:
      - name: checkout code repository
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: setup node.js
        uses: actions/setup-node@v2
        with:
          node-version: 14
      - name: install pnpm
        run: npm i pnpm@latest -g
      - name: Setup npmrc
        run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
      - name: setup pnpm config
        run: pnpm config set store-dir $PNPM_CACHE_FOLDER
      - name: install dependencies
        run: pnpm install
      - name: create and publish versions
        uses: changesets/action@v1
        with:
          version: pnpm ci:version
          commit: "chore: update versions"
          title: "chore: update versions"
          publish: pnpm ci:publish
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

总体来说,这一套 monorepo 管理方式,还是比较灵活和完善的,比较适合中小型项目,对于大型的工程,运行任务的性能可能还是个痛点,毕竟 Pnmp 解决的是安装依赖的性能。当然也有一定的学习成本,不过相对于 Nx 来说,我个人觉得上手成本中规中矩。

该怎么选

我们先来对比下两者:

  • lerna@5 学习成本低,开箱即用,配套设置完善,集成了 Nx 后,也可以应对大型的 monorepo 场景;
  • Pnpm + changesets,学习成本也还算中规中矩,Pnpm 有其独特的优势,安装依赖速度和节省你的电脑磁盘空间,发布工作流可以比较灵活地设计,也可以低成本地集成 CI 做自动化发布。

光从各自的优势来看,我觉得对于我们的场景,可以说是伯仲之间,但是最后我选择了 lerna@5。理由如下:

  • lerna@5 完全符合我的需求,学习成本低,开箱即用
  • 我比较熟悉 lerna 的工作流,有一定的使用经验,踩过一些坑
  • 背后有不错的团队在维护,issue 的响应速度和版本迭代都能得到保证

果然不负我所望,在 v5.5.0 版本,lerna 也能完全结合 Pnpm 使用(我选型的时候是 v5.2.0 版本,NPM 包管理工具使用了 Yarn)。

我个人觉得选型另外一个重要因素就是团队内有一个非常熟悉、甚至精通这项技术的人,遇到任何问题,基本能找到解决方案。当时我对 changeset 这样的工具只停留在在 demo 中使用过的阶段,所以考虑时间成本问题,我还是没有冒险。

总结

虽然只是一次小型的 monorepo 管理工具的选型过程,但是我还是花了一些时间去调研社区内的方案,然后综合对比,最后选择了 lerna@5。我还是非常愿意接受新的技术,如果它能更好地解决我遇到的问题,我依然觉得 Nx 、Turborepo 和 Pnpm + Changesets 是非常优秀的工具。

但是在这个选型的过程中,除了满足当下的需求之外,我们需要考虑以下因素:

  • 学习成本,不只是你个人,还有团队其它成员;
  • 团队内是否有熟悉、精通该选型的人,否则遇到问题,容易入坑;
  • 项目维护的情况,issue 是否有响应,迭代是否在正常进行。