背景
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的开发者来说,也有不少坑在里头。
最后:与君共勉