依赖清单的生成与维护:requirements.txt、package.json、pom.xml
每个项目都有一份“食材清单”——依赖清单。Python 看 requirements.txt,Node.js 看 package.json,Java 看 pom.xml。但它们真的只是列个名字吗?我在项目里踩过坑才知道,生成的时机、格式、版本锁定……每一个细节都能让你半夜被报警电话吵醒。
说白了,依赖清单不是“写”出来的,而是“管”出来的。你想想看,团队里十个人,有人用 pip install 最新版,有人手动改版本号,结果 CI 时而绿时而红——这种痛,经历过的人都懂。
为什么依赖清单需要“生成”而非“手写”?
我习惯把依赖分为两层:直接依赖和传递依赖。直接依赖是你主动引用的库,传递依赖是那些库自己带的“小弟”。手写清单时,大多数人只写了直接依赖,却漏掉了传递依赖的版本。后果是什么?
- 开发环境一个版本,CI 环境另一个版本,测试通过但上线崩溃。
- 某次 npm install 后包版本自动升级,新引入的 bug 导致服务大面积报错。
三大生态的依赖清单对比
| 生态 | 源清单 | 锁定清单 | 生成命令 | 推荐做法 |
|---|---|---|---|---|
| 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. 锁定清单必须进版本控制
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 直接红了,他才知道锁文件不是摆设。
依赖清单的生命周期流程图
维护依赖清单的避坑指南
下面这些坑,我几乎每个都亲身体验过,分享出来希望你能绕过。
- 不要用 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:“这次构建用这些版本,我说了算。”
我常跟新人说:别把依赖清单当成包袱,它是你的安。你觉得它麻烦,是因为你还没被它救过。等你经历过一次“因为漏锁版本导致线上回滚”的夜晚,你就懂了。