Node.js 包管理与脚本:npm、lockfile 与 monorepo 心智
npm install人人会敲,但「装的是什么、锁的是谁、CI 里凭什么可复现」,一问就容易含糊。
这一篇把 包管理器这层 摊开来讲:依赖树怎么长、lockfile在保什么、package.json的 scripts 在工程里扮演什么角色,以及 workspace / monorepo 下路径和版本怎么才不会乱。
npm / yarn / pnpm:同一套生态,不同的「放包方式」
三者都围绕 npm 注册表(或私有源)拉包,差别主要在:
- 磁盘布局与去重:
pnpm用内容寻址 + 硬链接,严格时还能拦住幽灵依赖; - 锁文件格式:
package-lock.json、yarn.lock、pnpm-lock.yaml各自一套,不要混用同一目录多种锁文件; - 安装速度:与缓存、并行、链接策略有关——选型往往是团队习惯 + monorepo 规模,而不是「谁绝对更快」。
心智模型可以记:
- 声明在
package.json(想要什么范围); - 冻结在 lockfile(这次到底解析成哪些具体版本)。
dependencies 与 devDependencies:运行时与工具链
粗分:
dependencies:线上进程 require/import 得着的包;devDependencies:测试、构建、lint、类型声明(若只在编译期用)等。
边界模糊的情况:
- 某些 CLI 只在 部署脚本 里用,却写进了
dependencies→ 镜像变大; - 把 运行时库 误放
devDependencies→ 生产装包缺模块。
以「线上 node 进程要不要加载它」为准,比死记规则更稳。
lockfile:可复现构建的底线
没有 lockfile 时:
^1.2.3可能在两个月后解析到 不同的补丁/次版本;- 本地「能跑」、CI「偶发红」,很难查。
有 lockfile 时:
- 安装器按锁解析,同一锁在同一 major 版本的安装器下应得到一致树(细节以各工具文档为准)。
实践:
- 应用仓库:锁文件 进 Git;
- 库(library):有的团队 不提交 lock,让下游自己解析——这是策略选择,要在 README 里说明白。
scripts:npm 作为「小 Makefile」
npm run xxx 本质是:
- 在 安装了本地
node_modules/.bin的 PATH 下跑一段 shell 命令; - 常用串联:
build、test、lint、dev(带 watch)。
好处:
- 新人一条命令对齐环境;
- CI 与本地 同一入口,减少「我机器上好好的」。
注意点:
- 脚本里 硬编码路径(如
../../foo)在 monorepo 里很脆; - 长命令建议拆成 独立脚本文件 或 任务运行器(如
turbo、nx),可读性更好。
workspace 与 monorepo:包与包之间的关系
workspace 把多个 package.json 放在一棵 repo 里:
- 内部包用
workspace:*或协议 互相引用; - 根目录统一
install,共享提升依赖。
容易踩坑的地方:
- 版本不同步:改了 A 没发版,B 还在引用旧 workspace 范围;
- 循环依赖:A→B→A,构建顺序和类型检查会打架;
- 工具不认 workspace:老工具只认单层
node_modules。
治理思路:
- 明确 哪些包对外发布、哪些只内部用;
- 用 变更集(changesets) 或等价流程管版本与发布说明。
小结:包管理是「契约 + 可复现」
- 声明 + lock 解决「装得一致」;
dependencies分区 解决「镜像与攻击面」;- scripts + workspace 解决「多人协作与多包协作」。
把这套理顺,后面接 Docker 镜像、CI 缓存、私有 registry,都是在同一层上加约束。