pnpm workspace项目实战中的高效管理技巧

令狐爱香 前端 阅读 904
赞 24 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

我最近在搞一个中型项目,包含三个前端应用和两个公共组件库。最开始是用 npm 配合 lerna 搞的,结果每次装依赖慢得像蜗牛,团队协作还老因为 node_modules 结构不一致出问题。折腾了两天终于下定决心换成 pnpm workspace,现在回头看:早该这么干。

pnpm workspace项目实战中的高效管理技巧

先说结论——pnpm 的硬链接 + 符号链接机制是真的香,特别是搭配 workspace 使用时,本地包引用几乎零成本,安装速度提升至少 3 倍。但前提是配置得对,否则坑比好处还多。

我现在项目的目录结构长这样:

/
├── apps/
│   ├── web/
│   ├── admin/
│   └── mobile/
├── packages/
│   ├── ui/
│   └── utils/
├── pnpm-workspace.yaml
└── package.json

根目录下的 pnpm-workspace.yaml 我是这么写的:

packages:
  - 'apps/**'
  - 'packages/**'
  - '!**/test/**'
  - '!**/__mocks__/**'

重点来了:不要偷懒只写 **,一定要显式排除测试或 mock 目录。我之前没加这个,结果某个测试文件夹里有个 package.json,被误识别成 workspace 包,导致一堆诡异的 peer dependency 警告,排查了快一个小时才定位到。

另外,根目录的 package.json 一定要加上 "private": true,不然万一谁手滑 publish 上去了,麻烦就大了。虽然看起来是常识,但我真见过同事踩过这坑。

这种写法更靠谱:统一版本管理 + 自动 hoist

很多人不知道的是,pnpm 默认会把能共享的依赖提升到根节点的 node_modules,减少重复安装。但如果你在子项目里手动写了 "nohoist": ["react-native"] 这种配置(常见于某些 CLI 工具生成的项目),记得检查是否真的需要。

我在一个项目里发现打包后体积异常大,最后查出来是因为多个 app 都装了自己的 react 拷贝,原因就是每个子项目的 .npmrc 里都有 nohoist 配置,而实际根本用不到。删掉之后 bundle size 直接小了 15%。

所以我的建议是:除非你明确知道某个包必须隔离(比如不同版本的 babel 插件),否则不要主动加 nohoist。

还有个关键点:版本统一。我一般会在根目录用 pnpm add -w xxx 来全局安装通用依赖,比如:

pnpm add -w typescript eslint prettier @types/node --filter=@my-scope/*

这里的 -w 表示写入 workspace 根目录,--filter 可以批量操作符合条件的包。这样所有子项目共用同一套 tsconfig 和 lint 规则,避免出现“为什么你的 eslint 报错我不报”的扯皮情况。

对于私有组件库之间的依赖,比如 @my-scope/ui 依赖 @my-scope/utils,我是直接在 packages/ui/package.json 里正常写:

{
  "name": "@my-scope/ui",
  "version": "0.1.0",
  "dependencies": {
    "@my-scope/utils": "workspace:*"
  }
}

注意这里用了 workspace:*,这是 pnpm 特有的语法,意思是“用工作区内最新的版本”,不用管具体数字。改完 utils 后,其他依赖它的项目重新 build 就能生效,不需要先 publish 再 install。

这招特别适合本地联调,但也容易出事——比如你提交代码时忘记同步更新版本号,CI 环境拉下来就会报错找不到包。解决办法是在 CI 脚本里加一句:

pnpm install --frozen-lockfile

确保 lockfile 是最新的,如果有未提交的变更,这步会直接失败,提醒你先跑一遍完整 install 再提交。

这几种错误写法,别再踩坑了

先说最常见的:有人为了省事,在各个子项目里各自运行 pnpm init,然后手动改 name 和 dependencies。听着没啥问题对吧?结果就是版本混乱、命名不规范、scripts 五花八门。

举个真实案例:我们团队曾经有两个项目都用了 zustand,但一个是 4.1,一个是 4.3,看着差距不大,结果因为内部状态机实现变了,导致共享的 store 初始化行为不一致,debug 到凌晨两点才发现根源在这里。

正确做法是:通过脚本或模板初始化新项目。我现在都是写个简单的 bash 脚本,自动生成标准结构的 app 或 package,并自动注册到 workspace。

另一个作死操作是:在子项目里直接 pnpm add ../packages/utils。虽然也能 work,但生成的是相对路径依赖,比如:

"dependencies": {
  "@my-scope/utils": "link:../packages/utils"
}

问题在哪?这种写法在 CI 或别人 clone 项目后容易出问题,尤其是当路径结构调整时。而且 pnpm 官方推荐使用 workspace: 协议,它更稳定,支持版本通配符,还能配合 filter 使用。

还有一种迷惑性很强的写法:

"dependencies": {
  "@my-scope/utils": "*"
}

看似指向最新版,其实完全脱离了 workspace 管控,npm/yarn/pnpm 都会去远程仓库找,本地修改根本不会被识别。这种写法上线后你会发现改了半天代码,跑的还是旧逻辑,血崩。

实际项目中的坑

第一个大坑是 TypeScript 联合编译。你以为只要设置了 path alias 就万事大吉?too young。

我在 tsconfig.base.json 里写了:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@my-scope/utils": ["packages/utils/src"],
      "@my-scope/ui": ["packages/ui/src"]
    }
  }
}

然后在各个子项目 extend 它。理论上没问题,但实际上 VSCode 经常识别不了,跳转到声明时打开的是 dist 文件而不是 src。解决办法是在根目录的 tsconfig.json 里添加:

{
  "references": [
    { "path": "packages/utils" },
    { "path": "packages/ui" }
  ]
}

并且每个 package 的 tsconfig.json 要启用 "composite": true。这样才能让 IDE 正确理解项目引用关系。

第二个坑是构建产物覆盖。假设你有两个 app 都用了 dist/ 输出,如果同时启动开发服务器,可能会互相干扰。我的做法是在每个 app 的 package.json scripts 里明确指定输出路径:

"scripts": {
  "build": "vite build --outDir dist/web"
}

或者干脆用 pnpm 的并发执行能力:

"scripts": {
  "build:all": "pnpm --filter ./apps/* run build"
}

第三个容易忽略的是 git 提交校验。你有没有遇到过这种情况:本地开发好好的,push 上去 CI 却报错“找不到 @my-scope/utils”?大概率是你忘了提交某个 package 的 dist 文件。

我现在强制要求所有公共包都把 dist 提交进仓库(除了 demo 和测试),并在 pre-commit 加了个简单检查:

# 在 .husky/pre-commit
if git diff --cached --quiet; then
  exit 0
else
  echo "请确认所有变更已正确提交"
  # 可选:增加对特定目录的检查
fi

虽然不是万无一失,但至少能拦住低级失误。

最后一点建议:别追求完美架构

说实话,折腾 workspace 最怕的就是“过度设计”。我见过有人为了实现“极致解耦”,给每个小工具函数都单独建 package,结果项目还没上线,光维护 workspace 就花了两周时间。

我的经验是:功能稳定、复用频率高的模块才值得放进 packages。比如 UI 组件、网络请求封装、路由配置这些。至于那种只被一处使用的“utils”,老老实实放在就近目录就行。

还有一个现实问题是:不是所有团队成员都熟悉 pnpm。你得准备好文档,哪怕只是几条命令行速查:

  • pnpm install —— 安装所有依赖
  • pnpm --filter ./apps/web dev —— 启动 web 项目
  • pnpm --filter @my-scope/utils run build —— 构建工具库
  • pnpm up —— 更新所有依赖(慎用)

以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论