前言
Monorepo这个词你应该不止一次听说了,像Vue3、Vite、ElementPlus等优秀开源项目都是使用Monorepo的方式管理项目,且这里说到的这几个项目都是采用pnpm作为包管理工具。
可见 基于 pnpm 的 Monorepo大势所趋,本文就是讲解如何使用pnpm构建一个简单的Monorepo方式管理的项目。
两个疑问
Q:什么是 Monorepo
A:Monorepo是一种项目管理方式,就是把多个项目放在一个仓库里面,可以参考神三元大佬的一篇文章:现代前端工程为什么越来越离不开 Monorepo?,这篇文章中介绍了Monorepo的概念、收益以及MulitRepo的弊端。
Q:什么是pnpm?
A:pnpm就是一个包管理工具,原生支持Monorepo,比npm和yarn更快一些,其他的可以参考官网和神三元大佬的另一篇文章:为什么现在我更推荐 pnpm 而不是 npm/yarn?
搭建流程
首先需要安装 pnpm,需要你的Node.js版本大于14.xx,安装最新的就行了。
npm i pnpm -g
第一步,开始创建我们的项目,例如我的项目叫 pnpm-monorepo,命令如下:
mkdir pnpm-monorepo
第二步,初始化 package.json ,命令如下:
pnpm init
小小修改一个 package.json配置,让 node 命令运行的时候使用 esm 规范运行
{
"name": "pnpm-monorepo",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"license": "MIT"
}
第三步,创建pnpm-workspace.yaml文件,这个文件定义了工作空间的根目录,内容如下:
packages:
- 'packages/**'
然后,我们在packages中创建多个项目了,目录结构如下:
pnpm-monorepo
├── package.json
├── packages
│ ├── components
│ │ ├── index.js
│ │ └── package.json
│ ├── utils
│ │ ├── index.js
│ │ └── package.json
├── pnpm-lock.yaml
└── pnpm-workspace.yaml
第四步,编写每个项目的package.json,其实主要是编写一下名称,方便以后使用,这里我的如下:
{
"name": "@packages/components",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"type": "module",
"license": "ISC",
"dependencies": {
"lodash": "^4.17.21",
}
}
剩余的@packages/utils copy 一份,改一下 name 字段。
全局安装
一般是用 全局的公共依赖包,比如打包涉及到的 rollup、typescript 等
pnpm 提供了 -w, --workspace-root 参数,可以将依赖包安装到工程的根目录下,作为所有 package 的公共依赖。
例如装一个 axios 的包到全局的生产依赖,命令如下:
pnpm add axios -w
如果是一个开发依赖的话,可以加上 -D 参数,表示这是一个开发依赖,会装到 pacakage.json 中的 devDependencies 中,比如:,命令如下:
pnpm add axios -wD
局部安装
局部安装有两种方式
- 进入到对应项目,使用 pnpm i xxx
- 使用 --filter 指定对应项目进行安装
这里主要讲指定项目安装的方式,命令如下:
命令如下:
pnpm --filter <package_selector> <command>
// 缩写
pnpm -F <package_selector> <command>
- package_selector package.json对应的name 字段
- command 要执行的命令,可以是 安装、打包等命令
例如我们需要在@packages/components安装lodash,命令如下:
pnpm -F @packages/components add lodash
再往@packages/utils中安装一个dayjs,命令如下:
pnpm --filter @packages/utils add dayjs
项目之间引用
现在我们就来实现package间的相互引用,首先我们在@packages/utils/index.js中写入如下内容:
import dayjs from 'dayjs'
export function format(time, f = 'YYYY-MM-DD') {
return dayjs(time).format(f)
}
然后我们执行如下命令:
pnpm -F @packages/components add @packages/utils@*
这个命令表示在@packages/components安装@packages/utils,其中的@*表示默认同步最新版本,省去每次都要同步最新版本的问题。
安装完成后@packages/components/package.json内容如下:
{
"name": "@packages/components",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"type": "module",
"license": "ISC",
"dependencies": {
"@packages/utils": "workspace: *",
"lodash": "^4.17.21"
}
}
然后我们在@packages/components/index.js写入如下内容:
import { format } from '@packages/utils'
console.log(format(new Date()))
然后我们在项目根目录运行如下命令
node packages/components
即可打印出当前的日期。
只允许pnpm
当在项目中使用 pnpm 时,如果不希望用户使用 yarn 或者 npm 安装依赖,可以将下面的这个 preinstall 脚本添加到工程根目录下的 package.json中:
{
"scripts": {
"preinstall": "npx only-allow pnpm"
}
}
preinstall 脚本会在 install 之前执行,现在,只要有人运行 npm install 或 yarn install,就会调用 only-allow 去限制只允许使用 pnpm 安装依赖。
Release工作流
在 workspace 中对包版本管理是一个非常复杂的工作,遗憾的是 pnpm 没有提供内置的解决方案,一部分开源项目在自己的项目中自己实现了一套包版本的管理机制,比如 Vue3、Vite 等。
pnpm 推荐了两个开源的版本控制工具:
这里我采用了 changesets 来做依赖包的管理。选用 changesets 的主要原因还是文档更加清晰一些,个人感觉上手比较容易。
按照 changesets 文档介绍的,changesets主要是做了两件事:
Changesets hold two key bits of information: a version type (following semver), and change information to be added to a changelog.
简而言之就是管理包的version和生成changelog。
配置changesets
- 安装
$ pnpm add -Dw @changesets/cli
- 初始化
$ pnpm changeset init
执行完初始化命令后,会在工程的根目录下生成 .changeset 目录,其中的 config.json 作为默认的 changeset 的配置文件。
修改配置文件如下:
{
"$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"linked": [["@packages/*"]],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
}
功能配置说明,如下:
- changelog: changelog 生成方式
- commit: 不要让
changeset在publish的时候帮我们做git add - linked: 配置哪些包要共享版本
- access: 公私有安全设定,内网建议 restricted ,开源使用 public
- baseBranch: 项目主分支
- updateInternalDependencies: 确保某包依赖的包发生 upgrade,该包也要发生 version upgrade 的衡量单位(量级)
- ignore: 不需要变动 version 的包
- ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: 在每次 version 变动时一定无理由 patch 抬升依赖他的那些包的版本,防止陷入 major 优先的未更新问题
如何使用changesets
一个包一般分如下几个步骤:
为了便于统一管理所有包的发布过程,在工程根目录下的 pacakge.json 的 scripts 中增加如下几条脚本:
- 编译阶段,生成构建产物
{
"build": "pnpm --filter=@packages/* run build"
}
- 清理构建产物和
node_modules
{
"clear": "rimraf 'packages/*/{lib,node_modules}' && rimraf node_modules"
}
- 执行
changeset,开始交互式填写变更集,这个命令会将你的包全部列出来,然后选择你要更改发布的包
{
"changeset": "changeset"
}
- 执行
changeset version,修改发布包的版本
{
"version-packages": "changeset version"
}
这里需要注意的是,版本的选择一共有三种类型,分别是 patch、minor 和 major,严格遵循 semver 规范。
这里还有个细节,如果我不想直接发 release 版本,而是想先发一个带 tag 的 prerelease版本呢(比如beta或者rc版本)?
这里提供了两种方式:
- 手工调整
这种方法最简单粗暴,但是比较容易犯错。
首先需要修改包的版本号:
{
"name": "@qftjs/monorepo1",
"version": "1.0.2-beta.1"
}
然后运行:
$ pnpm changeset publish --tag beta
注意发包的时候不要忘记加上 --tag 参数。
-
通过
changeset提供的Prereleases模式利用官方提供的 Prereleases 模式,通过
pre enter <tag>命令进入先进入 pre 模式。
常见的tag如下所示:
| 名称 | 功能 |
|---|---|
| alpha | 是内部测试版,一般不向外部发布,会有很多Bug,一般只有测试人员使用 |
| beta | 也是测试版,这个阶段的版本会一直加入新的功能。在Alpha版之后推出 |
| rc | Release Candidate) 系统平台上就是发行候选版本。RC版不会再加入新的功能了,主要着重于除错 |
$ pnpm changeset pre enter beta
之后在此模式下的 changeset publish 均将默认走 beta 环境,下面在此模式下任意的进行你的开发,举一个例子如下:
# 1-1 进行了一些开发...
# 1-2 提交变更集
pnpm changeset
# 1-3 提升版本
pnpm version-packages # changeset version
# 1-4 发包
pnpm release # pnpm build && pnpm changeset publish --registry=...
# 1-5 得到 1.0.0-beta.1
# 2-1 进行了一些开发...
# 2-2 提交变更集
pnpm changeset
# 2-3 提升版本
pnpm version-packages
# 2-4 发包
pnpm release
# 2-5 得到 1.0.0-beta.2
完成版本发布之后,退出 Prereleases 模式:
$ pnpm changeset pre exit
- 构建产物后发版本
{
"release": "pnpm build && pnpm release:only",
"release:only": "changeset publish --registry=https://registry.npmjs.com/"
}
一些方便的命令
- 清理所有 node_modules
{
"clear": "rimraf packages/*/{lib,node_modules} && rimraf node_modules"
}
- 安装所有依赖
{
"install-all": "pnpm install --filter="@packages/*" && pnpm install"
}
- 执行所有打包命令
{
"build": "pnpm --filter=@packages/* run build"
}