对比lerna和pnpm+changesets实现monorepo项目代码组织管理

1,721 阅读5分钟

写在前面

在前面的文章中对包管理器npm、yarn和pnpm进行分析,也对multirepo和monorepo项目组织方式进行分析,以下是对monorepo的优点和缺点一些结论。

优点:

  • 一个代码库内维护多个模块/包,方便版本管理和依赖管理,便于发包和工程化管理
  • 通过workspace便于开发阶段的模块的引用和调试

缺点:

  • 代码库的体积变大,导致进行包开发、项目启动会变慢
  • 主线破坏会影响到每个单独开发的模块
  • 代码提交影响范围会更大

对于工具库开发更适合monorepo单一代码库的组织方式,对于项目开发更适合multirepo多代码库组织方式。

monorepo实践

monorepo是将多个包放在同一个库进行集中管理和发包的方式,是当前比较流行的项目组织方式。

比如vue3中的包包含:@vue/compiler-core、@vue/compiler-dom、@vue/compiler-sfc、@vue/reactivity等,这样将vue3源码进行拆分发包,可以方便在项目开发进行按需安装依赖。

使用lerna+npm link实现monorepo

过去我们依赖正在开发中的包进行调试,那么就需要不断地进行npm link、版本管理、依赖发包等。

从lerna的官方文档可以看到,lerna解决了三个问题,分别是:

  • Lerna 链接了 repo 中的不同项目,因此它们可以相互导入而无需向 NPM 发布任何内容。
  • Lerna 针对任意数量的项目运行一个命令,它以最有效的方式、以正确的顺序执行,并有可能将其分发到多台机器上。
  • Lerna 管理您的发布过程,从版本管理到发布到 NPM,它提供了多种选项以确保可以适应任何工作流程。

lerna历史上有自己的依赖管理解决方案:lerna bootstrap,此方案已在lerna v7版本废弃,对此可以最好是采用现代包管理工具的workspace。

在项目根目录下执行命令,便会将lerna添加到根级package.json:

npx lerna@latest init

通过根级的packages.json的workspace可以配置工作区间:

{
  "name": "root",
  "private": true,
  "devDependencies": {
    "lerna": "^6.6.1"
  },
  "workspaces": [
    "packages/*"
  ]
}

在lerna.json文件中开启workspace

{
  "$schema""node_modules/lerna/schemas/lerna-schema.json",
  "useWorkspaces"true,
  "version""0.0.0"
}

在packages/remixapp/package.json文件中,"header": "*""footer": "*" 告诉 Lerna 链接 header 和 footer 的内容,就像它们已发布到注册表一样。

{
  ...
  "dependencies": {
    ...
    "header": "*",
    "footer""*"
  }
}

执行npm install后,便可在工作区中的所有项目都可以通过本地包链接正确地相互引用,避免手动使用npm link进行符号链接node_modules文件夹的包引用。

此外还可以通过执行npx nx graph,便可对工作区项目图进行可视化查看关系:

  • 构建所有项目:npx lerna run build,所有项目将按照依赖关系构建子模块
  • 测试所有项目:npx lerna run test,可以按照拓扑顺序运行test脚本

可以执行npx lerna run dev --scope=remixapp,构建执行指定子项目。

整个项目执行指定命令npm exec -ws -- 指定命令,指定子项目执行指定命令npm exec -w header -w footer -- 指定命令

发布项目

执行npx lerna publish --no-private命令,将会执行:

  • 确定依赖包的当前版本
  • 检测自上次发布以来哪些包发生了变化,然后相应地在 package.json 中更新其版本
  • 创建已更改的 package.json 文件的提交,标记提交并将标记和提交推送到远程
  • 将包发布到 NPM

使用pnpm实现monorepo

初始化monorepo目录

npm i pnpm -g #全局安装pnpm
pnpm init #初始化生成package.json文件

touch pnpm-workspace.yaml # 新建pnpm工作空间管理文件,可以在文件中进行包含或排除目录

我们执行上面命令后生成依赖管理文件package.json和工作空间配置文件pnpm-workspace.yaml,可以通过packages配置需要包含和排除的目录:

# pnpm-workspace.yaml
packages: 
  # 包含所有在 packages/ 子目录下的 package
  - "packages/**"
  # 排除包括在 test 文件夹下的 package
  - "!**/test/**"

与此同时,在packages目录下创建多个模块clicoreshared以及utils模块:

全局安装依赖

比如在packages全局需要使用打包工具vite,那么就需要将其安装在根目录下,实现全局共用vite依赖包:

pnpm add vite

但是,这样执行安装命令是会报错的,想把依赖安装到根目录下,必须在执行命令时添加-w标识。(或 --workspace-root)

Run as if pnpm was started in the root of the workspace instead of the current working directory.

局部安装依赖

要在packages/core下安装vueuse依赖,可以执行下面命令:

# 进入子目录
cd packages/core
pnpm init

# 安装@vueuse/core
pnpm add @vueuse/core

安装模块间依赖

在项目中的子模块进行模块依赖安装,可以通过--filter过滤允许将命令限制为包的特定子集。文档

pnpm --filter core add "@cambrian/shared"

执行完命令如下:

通过pnpm --filter选择当前项目工作空间的子目录,这里的workspace:*匹配的是本地包,会在core的node_modules目录下生成@cambrian/shared的软链接。

使用changeset进行版本管理

可以通过changesets帮助管理包的版本控制和变更日志条目,管理monorepo的子项目的版本更新、changelog文件生成以及包的发布。

执行下面命令安装版本变更集依赖包@changesets/cli:

pnpm install @changesets/cli -w  --save-dev

执行pnpm changeset init初始化生成一个包含config.json.changeset目录:

pnpm changeset init

更多信息查看:github.com/changesets/…

生成变更集:

pnpm changeset
# 或者
pnpm changeset add

参考文章

写在最后

本文主要通过lerna和pnpm+changesets对比实现了monorepo进行项目组织管理,解决了工程化配置重复、版本控制、开发调试麻烦等问题。

pnpm workspace+changesets已经在功能上和lerna大差不差了,lerna主要优势在于做了命令缓存、分布式执行任务等性能优化。

学而知不足,水平有限,还望诸君多多指教。觉得文章不错的读者,不妨点个关注,收藏起来上班摸鱼的时候品尝。

欢迎关注笔者公众号「前端一码平川」,助你技术路上一码平川。