多包项目
什么是 Monorepo
Monorepo 项目简称多包项目,一个包含多个子项目的仓库。
那为什么要放多个项目在一个仓库下呢?
是因为这些项目互相引用,相互依赖,放在一个仓库下方便管理及依赖。
所以管理一个多包项目的关键,需要实现以下 2 点:
- 能够很方便的管理包与包之间的依赖关系
- 能够在发布其中一个包时,自动更新依赖了该包的其他包并发布
什么是 lerna
使用最广泛成熟的 Monorepe 项目管理方案就是 lerna + yarn。
lerna 是一个优化使用 git 和 npm 管理多包存储库工作流的工具。
它具有以下功能:
- 自动解决 packages 之间的依赖关系;
- 通过 git 检测文件改动,自动发布;
- 根据 git 提交记录,自动生成 CHANGELOG。
更多详细的 lerna 介绍可以见我的另外一篇博客:最详细的 lerna 中文手册。
既然 lerna 这么好用也这么熟悉了,那为什么还要切换到 pnpm 呢?
有以下几个原因:
- pnpm 内置了管理 monorepo 功能,使用起来比 lerna 简单
- pnpm 安装比 yarn 高效,也节省电脑内存
什么是 pnpm
pnpm 介绍可以查看 pnpm 官网。
pnpm 是新一代的包管理工具,相较于 npm 和 yarn,有以下 2 个优点:
节约磁盘空间并提升安装速度
节约磁盘空间: pnpm 安装依赖时,依赖会被存储在硬盘中,不同项目的同一依赖都会硬链接到硬盘位置,不会额外占用磁盘空间。 同一依赖包的不同版本,也只会将不同版本中有差异的文件添加到仓库中,不会下载整个包占用磁盘空间。
提升安装速度: 安装依赖时,会先去硬盘位置寻找包,如果能找到,则建立硬链接,比起重新下载包或者从缓存中拷贝移动包,速度快了很多 创建非扁平化的 node_modules 文件夹 npm、yarn 为了解决同一依赖被安装多次的问题,将所有包都被提升到模块目录的根目录。 但是当依赖包有多个版本的时候,只会提升一个,其余版本的包依然会被安装多次。 另外扁平化 node_modules 时,项目可以访问到未被添加进当前项目的依赖,这样是有隐患的, 因为没有显式依赖,万一有一天别的包不依赖这个包了,代码就不能跑了,因为你依赖这个包,但是现在不会被安装了。
pnpm 采用磁盘硬链接连接依赖,已经解决了依赖会被安装多次的问题。
为了避免幽灵依赖,pnpm 选择创建非扁平化的 node_modules,项目无法访问到未被添加进当前项目的依赖。
为什么使用 pnpm? pnpm 的特性和优势?
- 快速高效、节省磁盘空间 pnpm 是同类工具速度的将近 2 倍 node_modules 中的所有文件均克隆或硬链接自单一存储位置 如果使用 npm 或 yarn,如果 100 个项目同时用到同一个依赖包时,就需要在硬盘上拷贝 100 份。而使用 pnpm 时,所有依赖包都保存在硬盘的统一位置(.pnpm-store),不同项目之间共享依赖包。
如果这些项目使用的同一依赖包是不同的版本,那么只有不同的文件会被再次保存起来。 直接依赖通过符号链接的方式添加在 node_modules 根目录下。实际指向的地址是:.pnpm/<name>@<version> /node_modules/<name>@<version>
该地址会硬链接到 .pnpm-store 直接依赖中的间接依赖通过软链接到 node_modules/.pnpm目录下,具体应该是 node_modules/.pnpm/<name>@<version> /node_modules/name@<version>
,然后硬链接到 .pnpm-store
支持 monorepo pnpm 跟 npm 和 Yarn 一样,内置了对 单一存储库monorepo 的支持,只需要在项目根目录下创建 pnpm-workspace.yaml 文件,定义 workspace 的根目录。
权限严格、更加安全 使用 pnpm 创建的 node_modules 默认是非扁平结构,可以防止幽灵依赖。
pnpm 常用命令
pnpm list
别名 ls。作用:该命令将以树形结构输出所有已安装软件包的版本及其依赖包。
--global, -g:列出全局安装目录中的软件包,而不是当前项目中的软件包。
pnpm list --global # 查看全局安装的软件包
pnpm link
别名 ln。作用:让当前目录下的软件包在系统范围内或其它位置都可访问。
pnpm link <dir> # 直接连接对应包的目录
pnpm link --global # 待发布的软件包中执行
pnpm link --global <pkg> # 将待发布的软件包软连接到项目中
断开连接
pnpm unlink 与 pnpm link --global 对应。
pnpm link --global <package> 与 pnpm uninstall --global <package> 对应。
迁移到 pnpm
安装 pnpm 需要 node 版本大于等于 16.14,执行 npm i -g pnpm
# 删除依赖包
rm -rf node_modules
# 在根目录下新建 .npmrc 文件
auto-install-peers=true # 自动安装任何缺少同级依赖关系
shamefully-hoist=false # [不推荐] 将所有依赖提升至根目录,即一个扁平的目录结构类似 npm 和 yarn
# 从另一个包管理器的 lock 文件生成 pnpm-lock.yaml
pnpm import
# 加上 --frozen-lockfile 参数:安装依赖时不更新 pnpm-lock.yaml
pnpm install --frozen-lockfile
# 启动项目,可能需要解决下幽灵依赖带来的问题
# 修改 ci 文件配置
FAQ 常见问题
- .pnpm-store 存储资源的位置在哪? Mac/linux 中默认会设置到
{home dir}>/.pnpm-store/v3
Windows 下会设置到当前盘的根目录下,比如 D 盘:D:.pnpm-store\v3
# 执行以下命令查看 store 存储目录的路径
pnpm store path
- .pnpm-store 越来越大怎么办? 可以执行 pnpm store prune,从存储中删除未引用的包。
未引用的包是系统上的任何项目中都未使用的包。 运行 pnpm store prune 是无害的,对您的项目没有副作用。 如果以后的安装需要已经被删除的包,pnpm 将重新下载他们。
最好的做法是 pnpm store prune 来清理存储,但不要太频繁。 有时,未引用的包会再次被需要。 这可能在切换分支和安装旧的依赖项时发生,在这种情况下,pnpm 需要重新下载所有删除的包,会暂时减慢安装过程。
请注意,当 存储服务器 正在运行时,这个命令是被禁止的。
创建 Monorepo 项目
创建目录结构:
mkdir my-project
cd ./my-project
npm init -y
mkdir packages
cd ./packages
mkdir my-project-a
cd ./my-project-a
npm init -y
mkdir my-project-b
cd ./my-project-b
npm init -y
启动 pnpm 的 workspace 功能,根目录新增 pnpm-workspace.yaml,指定工作空间的目录:
packages:
- "packages/**"
当我们配置了指定工作空间的目录后,packages 里的包互相引用时,会自动依赖本地编译的路径,方便实时调试。
至此我们就解决了 Monorepre 项目的管理包与包之间的依赖关系的问题。
安装项目内依赖
限制仅允许 pnpm 安装依赖,更新 package.json:
{
"scripts": {
"preinstall": "npx only-allow pnpm"
}
}
安装 eslint 等全局依赖:
pnpm i eslint -w -D
安装子项目内独立的依赖:
cd ./packages/my-project-a
pnpm i rollup -D
发布流程
pnpm 没有提供内置的发布流程解决方案,官方推荐了两个开源的版本控制工具:
- changesets
- rush
changesets 的入手学习成本更低,于是乎选择了 changesets 来管理发布流程。
安装 changesets
pnpm add -Dw @changesets/cli
初始化后生成的 .changeset/config.json:
{
"$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json",
"changelog": "@changesets/cli/changelog", // changelog 生成方式
"commit": false, // 不要让 changeset 在 publish 的时候帮我们做 git add
"fixed": [],
"linked": [], // 配置哪些包要共享版本
"access": "restricted", // 公私有安全设定,内网建议 restricted ,开源使用 public
"baseBranch": "master", // 项目主分支
"updateInternalDependencies": "patch", // 确保某包依赖的包发生 upgrade,该包也要发生 version upgrade 的衡量单位(量级)
"ignore": [] // 不需要变动 version 的包
}
管理 changelog
如果是开源库可以安装 @changesets/changelog-github 来管理 changelog。
安装:
pnpm add -Dw @changesets/changelog-github
更新 .changeset/config.json:
{
"changelog": [
"@changesets/changelog-github",
{
"repo": "worktile/slate-angular" // 改为你的 github 仓储
}
]
}
如果不是开源库,则保持 "changelog": "@changesets/cli/changelog"。
生成 changesets
npx changeset
选择要发布的包:
选择发布的类型:
填写发布备注:
确认发布:
生成临时文件:
更新版本
更新版本前可以先把开发区的改动提交上去。
git add .
git commit -m 'feat: msg'
git push
更新版本:
npx changeset version
自动生成 CHANGELOG.md 并更新 package.json 中的版本,同时如果子项目间有相互依赖,也会更新依赖版本。
发布版本
发布至 npm:
npx changeset publish
至此我们就解决了 Monorepro 项目的在发布其中一个包时,自动更新依赖了该包的其他包并发布的问题。