依赖清单的生成与维护:requirements.txt、package.json、pom.xml

每个项目都有一份“食材清单”——依赖清单。Python 看 requirements.txt,Node.js 看 package.json,Java 看 pom.xml。但它们真的只是列个名字吗?我在项目里踩过坑才知道,生成的时机、格式、版本锁定……每一个细节都能让你半夜被报警电话吵醒。

说白了,依赖清单不是“写”出来的,而是“管”出来的。你想想看,团队里十个人,有人用 pip install 最新版,有人手动改版本号,结果 CI 时而绿时而红——这种痛,经历过的人都懂。

为什么依赖清单需要“生成”而非“手写”?

我习惯把依赖分为两层:直接依赖和传递依赖。直接依赖是你主动引用的库,传递依赖是那些库自己带的“小弟”。手写清单时,大多数人只写了直接依赖,却漏掉了传递依赖的版本。后果是什么?

  • 开发环境一个版本,CI 环境另一个版本,测试通过但上线崩溃。
  • 某次 npm install 后包版本自动升级,新引入的 bug 导致服务大面积报错。
核心结论:依赖清单必须由锁定工具自动生成,并在 CI 中校验其完整性。

三大生态的依赖清单对比

生态 源清单 锁定清单 生成命令 推荐做法
Python requirements.in/setup.py requirements.txt pip freeze > requirements.txt 使用 pip-tools 或 poetry 管理,避免直接 freeze
Node.js package.json package-lock.json / yarn.lock npm install --package-lock-only 必须提交 lock 文件到仓库
Java pom.xml pom.xml(含版本) + gradle.lockfile Maven: mvn dependency:resolve 使用 Maven Enforcer 或 Gradle 的依赖锁定插件

生成依赖清单的三个关键动作

我负责过几个老项目的依赖治理,发现最容易出问题的不是“怎么生成”,而是“什么时候生成”以及“生成后怎么用”。下面三个步骤是我反复验证过的。

1. 从源清单出发,不要直接 lock

很多同学直接用 pip freeze > requirements.txt,然后提交。这有个大坑:pip freeze 会把当前环境中所有包都列出来,包括那些你根本没写进项目的系统级包。

# 错误做法(不推荐)
pip freeze > requirements.txt

# 推荐做法:使用 pip-compile
# 先写 requirements.in,只列直接依赖
flask>=2.0
requests==2.28.0

# 然后生成锁定版 requirements.txt
pip-compile requirements.in

我在一个遗留项目中被这种“大乱炖”坑过——requirements.txt 里突然多了个 numpy,结果 CI 镜像装不上,排查半天才发现是本地环境残留。从那以后我坚决要求项目中同时保留 .in 和 .txt。

2. 锁定清单必须进版本控制

血的教训:不要 .gitignore 掉 package-lock.json 或 yarn.lock!我曾经见过一个团队没锁 lock 文件,上线当天 npm registry 发布了某个依赖的 patch 版本,导致生产环境直接 500。锁文件就是你的“时光机”,必须提交。

Java 生态稍微特殊一点——pom.xml 本身就既声明版本又管理传递依赖。但如果用 Gradle,我强烈建议开启 --write-locks,生成 gradle.lockfile 并提交。

// build.gradle 中配置
dependencyLocking {
    lockAllConfigurations()
    lockMode = LockMode.STRICT
}
// 执行:gradle dependencies --write-locks

3. CI 中校验依赖清单一致性

你生成好了清单,但怎么保证没人偷偷改了?我习惯在 CI 里加一个“一致性校验”步骤。比如 Node 项目:

# CI 脚本片段
# 检查 package-lock.json 是否与 package.json 匹配
npm ci --dry-run || { echo "依赖清单不一致,请重新生成"; exit 1; }

这种检查我称之为“撒手锏”。曾经有个同事手动改了 package.json 里的版本号却忘了跑 npm install,CI 直接红了,他才知道锁文件不是摆设。

依赖清单的生命周期流程图

依赖清单在 CI 中的生命流程 源清单 .in / package.json / pom.xml 本地生成锁定 pip-compile / npm install / gradle --write-locks 提交仓库 锁定文件须在版本管理中 CI 触发 步骤2:一致性校验 npm ci --dry-run 等 步骤3:依赖扫描 漏洞/许可证检查 步骤4:构建/部署 使用锁定版本 💡 最佳实践:在本地 prepare-commit-msg 钩子中校验 lock 文件与源清单是否一致

维护依赖清单的避坑指南

下面这些坑,我几乎每个都亲身体验过,分享出来希望你能绕过。

  • 不要用 pip freeze 做生产锁定:它输出的是全局环境快照,包含大量不相关包。我曾经在一个项目里用 freeze 导出了 200 多行,实际项目只需要 30 个包。后来改用 pip-compile,清单直接瘦身。
  • lock 文件不要手动合并冲突:Git 合并 lock 文件简直是灾难。我建议的做法是:合并完 package.json 后,删掉 lock 文件重新生成(或者用 npm install 自动合并)。但要注意,重新生成后会丢失原有传递依赖的锁定顺序,最好用 --prefer-offline 减少 registry 请求。
  • 定期更新依赖,而不是一直锁定:锁定是为了可复现,不是为了封存。我习惯在每个 sprint 开始前用 ncu -u(npm-check-updates)或 pip-compile --upgrade 更新一次,然后 CI 全量跑测试,确保兼容。

小技巧:在 CI 中设置一个“依赖过期检查”的定时任务(比如每周一早8点),自动创建 PR 升级依赖。这样你就不用每天手动盯着 Dependabot 了。

我们真正要维护的是什么?

依赖清单不是死文件,它是你和依赖之间的一份“契约”。每次你生成、提交、校验,都是在告诉团队和 CI:“这次构建用这些版本,我说了算。”

我常跟新人说:别把依赖清单当成包袱,它是你的安。你觉得它麻烦,是因为你还没被它救过。等你经历过一次“因为漏锁版本导致线上回滚”的夜晚,你就懂了。