背景
24年差不多一整年都在跟pnpm
的多包打交道,在公司的两款ai
产品中都使用的是pnpm
来搭建,其中第三版重构的小程序ai
技能也是踩了不少坑(有机会的话后面会写篇文章来探讨下为适应公司ai
产品发展方向所设计的前端架构),现在在做的前端工程化从pnpm8
升级到pnpm9
又是踩了不少坑
三个阶段
创建多包流程(大家都知道的)
在项目中创建pnpm-workspace.yaml
,声明你的多包目录
yaml
packages:
- "cli/*"
然后在根目录下创建一个cli
文件夹,在里面创建你的多包
tree
cli
├───📁 core/
│ ├───📁 bin/
│ │ └───...
│ ├───📁 dist/
│ │ └───...
│ ├───📁 node_modules/
│ │ └───...
│ ├───📁 src/
│ │ └───...
│ ├───📄 CHANGELOG.md
│ ├───📄 package.json
│ ├───📄 rollup.config.js
│ └───📄 tsconfig.json
├───📁 plugin-create/
│ ├───📁 dist/
│ │ └───...
│ ├───📁 node_modules/
│ │ └───...
│ ├───📁 scripts/
│ │ └───...
│ ├───📁 src/
│ │ └───...
│ ├───📁 templates/
│ │ └───...
│ ├───📄 .gitignore
│ ├───📄 CHANGELOG.md
│ ├───📄 package.json
│ ├───📄 rollup.config.js
│ └───📄 tsconfig.json
├───📁 utils/
│ ├───📁 dist/
│ │ └───...
│ ├───📁 src/
│ │ └───...
│ ├───📄 CHANGELOG.md
│ ├───📄 package.json
│ ├───📄 rollup.config.js
│ └───📄 tsconfig.json
└───📄 README.md
对多包创建关联关系
bash
cd cli/core
pnpm i @xxx/cli-utils -S
这样你就能得到这样的一个依赖关系(注:pnpm9
需要在.npmrc
中配置link-workspace-packages=true
)
在使用pnpm publish
的时候会自动将workspace: ^
转成@xxx/cli-utils
相对应的版本,不用担心发布npm
包之后的版本问题
需要注意的是,在调试@xxx/cli-utils
的时候,需要时刻保持utils
的dist
文件是最新的,也就是你的dev
命令是运行的
相较于传统的npm link
的模式,pnpm
的多包调试省去了对每个包都进行npm link
命令,极大提升了调试体验
发包流程(可能也许大概估计差不多大家都知道的)
官方推荐使用changesets
来管理变更集,所以我也是遵循官方建议
初始化
bash
# 安装@changesets/cli
pnpm add -Dw @changesets/cli
# 初始化
pnpm changeset init
这时候就会自动创建出changeset
的配置文件
记录变更
pnpm changeset
,会出现一系列Prompt
问题,会列出改动和未改动的npm
包方便开发者选择
消费变更
pnpm changeset version
,会将临时生成的变更信息回填到上一步选择的npm
包的CHANGELOG.md
文件中,并更新版本
发布
pnpm changeset publish
,本质上就是对npm publish
做了一次封装,同时会检查对应的registry
上有没有对应包的版本,如果已经存在了,就不会再发包了,如果不存在会对对应的包版本执行一次npm publish
更多细节内容可以参考这两篇文章:
细节控制(细,不是细狗的细)
公共依赖的版本控制
在根路径下的package.json
中安装的依赖,我们可以在任意子包中使用而不需要再次安装,比如:根路径安装了lodash
,这时候可以在子包A/B/C中直接使用lodash
。但问题在于发布子包后,子包的package.json
中没有lodash
依赖,这时候在业务中安装子包的时候就会出现实际依赖跟package.json
不一致的情况,可以算是另类的"幽灵依赖"了
解决方案有两种:
- 在子包中再安装一次
lodash
,但可能会多个包使用lodash
的情况下出现版本不一致的问题 - 使用pnpm9.5.0新出的catalog特性管理公共依赖
我现在采用的就是第二种方案,在pnpm-workspace.yaml
中添加catalog
yaml
## pnpm-workspace.yaml
# 单个依赖的catalog声明
catalog:
rollup: ^4.28.0
# 系列依赖的catalog声明
catalogs:
eslint8:
eslint: ^8.57.1
eslint-config-alloy: ^5.1.2
eslint-plugin-import: ^2.31.0
依赖声明
json
// utils/package.json
"devDependencies": {
"rollup": "catalog:",
"eslint": "catalog:eslint8",
"eslint-config-alloy": "catalog:eslint8",
"eslint-plugin-import": "catalog:eslint8",
}
跟workspace一样,在发布阶段会自动改写成相对应的版本
但该方案其实也有弊端,详情可以看我另外一篇文章:使用pnpm搭建monorepo的踩坑日志
构建命令配置
背景 :目前手上的这几个pnpm
项目,子包都在十个左右,且都有持续增加的可能。CI/CD
使用的是coding
的云原生构建,构建命令只能有一个。
如何管理这个唯一的构建命令是一个问题,解决方案有:
- 在根路径下使用
npm-run-all
,每新增一个子包,就加一个构建命令 - 使用
pnpm --filter <package_selector> <command>
对子包进行依赖构建
目前选用的是方案2,我主要使用了pnpm --filter <package_selector>...
和pnpm --filter ...<package_selector>
这两个命令,简单说明下这两个命令的区别:
pnpm --filter foo... run build
:运行foo
及其所有依赖的build
命令,向下递归pnpm --filter ...foo run build
:运行foo
以及依赖它的所有包的build
命令,向上递归
这时候我们只需要选择一个底层的基础包或者最上层的应用包运行build
命令就可以,无论新增了多少依赖包,都不需要调整命令
特例 :有一种情况比较特殊,我在工程化架构的设计中,对每个基础功能都提炼为单独的npm
包,这些包都是相对独立的,也就是没有共同依赖,这时候就不好写filter
命令了
既然没有共同依赖,那就创建一个好了。按照filter
的机制,可以创建一个基础包或者总包。
如果是基础包的话,每新增一个功能包,都需要在新包中添加基础包依赖,功能包的依赖就变"脏"了
如果是总包的话,每新增一个功能包,只需要在总包中添加功能包依赖,总包没有实际用处,不影响其他
因为是多人开发,所以这个机制可能会因为人工的关系而被遗漏掉,所以我写了一个自动脚本,在pre-commit
的时候自动检测是否有新的功能包,如果有,自动添加到总包的依赖中
这样,在根路径的构建命令中,我只需要添加pnpm --filter all... run build
即可
总结
总的来说,使用pnpm
来管理多包还是挺简单的,不过对于首次接触pnpm
的开发者来说,也有不少坑在里头。
最后:与君共勉