Node.js 包管理与脚本:npm、lockfile 与 monorepo 心智

npm install 人人会敲,但「装的是什么、锁的是谁、CI 里凭什么可复现」,一问就容易含糊。
这一篇把 包管理器这层 摊开来讲:依赖树怎么长、lockfile 在保什么、package.json 的 scripts 在工程里扮演什么角色,以及 workspace / monorepo 下路径和版本怎么才不会乱。

三者都围绕 npm 注册表(或私有源)拉包,差别主要在:

  • 磁盘布局与去重pnpm 用内容寻址 + 硬链接,严格时还能拦住幽灵依赖;
  • 锁文件格式package-lock.jsonyarn.lockpnpm-lock.yaml 各自一套,不要混用同一目录多种锁文件
  • 安装速度:与缓存、并行、链接策略有关——选型往往是团队习惯 + monorepo 规模,而不是「谁绝对更快」。

心智模型可以记:

  • 声明package.json(想要什么范围);
  • 冻结在 lockfile(这次到底解析成哪些具体版本)。

粗分:

  • dependencies:线上进程 require/import 得着的包;
  • devDependencies:测试、构建、lint、类型声明(若只在编译期用)等。

边界模糊的情况:

  • 某些 CLI 只在 部署脚本 里用,却写进了 dependencies → 镜像变大;
  • 运行时库 误放 devDependencies → 生产装包缺模块。

以「线上 node 进程要不要加载它」为准,比死记规则更稳。

没有 lockfile 时:

  • ^1.2.3 可能在两个月后解析到 不同的补丁/次版本
  • 本地「能跑」、CI「偶发红」,很难查。

有 lockfile 时:

  • 安装器按锁解析,同一锁在同一 major 版本的安装器下应得到一致树(细节以各工具文档为准)。

实践:

  • 应用仓库:锁文件 进 Git
  • 库(library):有的团队 不提交 lock,让下游自己解析——这是策略选择,要在 README 里说明白。

npm run xxx 本质是:

  • 安装了本地 node_modules/.bin 的 PATH 下跑一段 shell 命令;
  • 常用串联:buildtestlintdev(带 watch)。

好处:

  • 新人一条命令对齐环境;
  • CI 与本地 同一入口,减少「我机器上好好的」。

注意点:

  • 脚本里 硬编码路径(如 ../../foo)在 monorepo 里很脆;
  • 长命令建议拆成 独立脚本文件任务运行器(如 turbonx),可读性更好。

workspace 把多个 package.json 放在一棵 repo 里:

  • 内部包用 workspace:* 或协议 互相引用;
  • 根目录统一 install,共享提升依赖。

容易踩坑的地方:

  • 版本不同步:改了 A 没发版,B 还在引用旧 workspace 范围;
  • 循环依赖:A→B→A,构建顺序和类型检查会打架;
  • 工具不认 workspace:老工具只认单层 node_modules

治理思路:

  • 明确 哪些包对外发布、哪些只内部用;
  • 变更集(changesets) 或等价流程管版本与发布说明。
  • 声明 + lock 解决「装得一致」;
  • dependencies 分区 解决「镜像与攻击面」;
  • scripts + workspace 解决「多人协作与多包协作」。

把这套理顺,后面接 Docker 镜像、CI 缓存、私有 registry,都是在同一层上加约束。