NPM 依赖解析
执行完 npm install 后,项目里多了两个东西:node_modules 文件夹和 package-lock.json 文件。
它们是什么?有什么用?这篇来拆解清楚。
node_modules 文件夹
node_modules 是所有依赖包实际存放的地方。你执行 npm i,所有代码就下载到这里。
my-app/
├── node_modules/ ← 所有依赖都在这里
│ ├── axios/
│ ├── lodash/
│ ├── react/
│ └── ...
├── package.json
└── package-lock.json
为什么 node_modules 特别大
一个项目往往有几十上百个依赖,每个依赖又有自己的依赖,层层叠加。比如你装了 10 个包,实际下载的可能有 100 个。
你的项目
├── axios
│ └── follow-redirects ← axios 的依赖
├── react
└── lodash
axios 本身又依赖了 follow-redirects,NPM 会一起装进来。这就是“间接依赖”或“子依赖”。
node_modules 的结构
早期 NPM(v2)是嵌套结构:
node_modules/
├── package-a/
│ └── node_modules/
│ └── lodash/ ← package-a 自己的 lodash
└── package-b/
└── node_modules/
└── lodash/ ← package-b 自己的 lodash
问题很明显:lodash 被装了两次,重复占用空间。
现在 NPM(v3+)使用扁平化结构:
node_modules/
├── lodash/ ← 所有包共用同一个 lodash
├── package-a/
├── package-b/
能扁平就扁平,实在不行(版本冲突)才保留嵌套。
版本冲突时怎么办
项目依赖了两个包,一个要 lodash@4,一个要 lodash@5:
node_modules/
├── lodash/ ← 5.x 版本(提升到顶层)
├── package-a/
│ └── node_modules/
│ └── lodash/ ← 4.x 版本(自己的私有版本)
└── package-b/ ← 用顶层的 5.x
NPM 会把最常用或最先安装的版本放在顶层,冲突的版本放在需要它的包的子目录里。
作为新手,你不需要记住这些细节,只需要知道两点:
node_modules是依赖的实际存放位置,正常情况下不要手动修改- 如果
node_modules出了问题,直接删掉重新npm i能解决大部分问题
package-lock.json 文件
node_modules 是依赖的“实体”,package-lock.json 是依赖的“清单”。
package.json 不够吗
package.json 里记录的是版本范围(比如 ^4.17.21),而不是精确版本。不同时间执行 npm i,实际装到的版本可能不一样。
| package.json | 实际安装 | |
|---|---|---|
| 记录 | "lodash": "^4.17.21" |
可能是 4.17.21、4.18.0、4.19.2…… |
| 确定性 | ❌ 模糊 | ✅ 精确 |
问题来了:你本地跑得好好的,同事拉代码后 npm i 装到了新版本,程序崩了——但代码明明一样。
package-lock.json 就是为了解决这个问题。
lock 文件做了什么
它完整记录了当时安装的精确版本和依赖树结构。
{
"name": "my-app",
"lockfileVersion": 3,
"packages": {
"node_modules/axios": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
"dependencies": {
"follow-redirects": "^1.15.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz"
}
}
}
关键信息:
- 每个包的精确版本号(
1.6.0而不是^1.6.0) - 下载地址(
resolved) - 依赖树结构
锁文件的核心价值
| 场景 | 有 lock 文件 | 没有 lock 文件 |
|---|---|---|
| 团队成员安装 | 装的版本完全一致 | 可能装到不同版本 |
| CI/CD 构建 | 稳定可重现 | 每次可能不同 |
| 问题排查 | 知道用的什么版本 | 版本说不清 |
一句话总结:package-lock.json 锁住的是“确定性”,让所有人、所有环境装出来的依赖一模一样。
日常使用规则
| 场景 | 要不要提交 lock 文件 | 做法 |
|---|---|---|
| 应用项目(网站/App/服务) | ✅ 必须提交 | 提交到 Git,保证团队一致 |
| 库/包(你要发布到 NPM 的) | ❌ 不提交 | 发布时会自动忽略,让使用者自行解析 |
| 新增/删除依赖 | ✅ 提交更新后的 lock | npm i 会自动更新 lock 文件 |
package.json vs package-lock.json
| 对比维度 | package.json | package-lock.json |
|---|---|---|
| 作用 | 声明需要什么包 | 记录实际装的精确版本 |
| 版本记录 | 范围(如 ^1.6.0) |
精确(如 1.6.0) |
| 需要手动改吗 | 部分场景需要 | 不需要,NPM 自动维护 |
| 提交到 Git | ✅ 必须 | ✅ 必须(应用项目) |
| 依赖关系 | 只记录直接依赖 | 记录完整的依赖树 |
几个实用命令
查看已安装的依赖
# 列出所有依赖(顶层)
npm list --depth=0
# 查看某个包的版本
npm list lodash
查看 package-lock.json 中某个包的版本
# 不需要专门命令,直接搜索 lock 文件
cat package-lock.json | grep "lodash"
手动重建 node_modules
当 node_modules 疑似出问题时:
# 删掉重装,简单粗暴有效
rm -rf node_modules package-lock.json
npm i
新手常见问题
Q:package-lock.json 需要手动编辑吗?
完全不需要。 它由 NPM 自动生成和更新,手动改只会自找麻烦。
Q:Git 冲突时 package-lock.json 怎么办?
两个人都在 main 分支安装了不同的包,合并时 lock 文件可能冲突。解决方案:
- 直接用
npm install重新生成(删掉 lock 再npm i,让 NPM 重新解析) - 或用
npm install --package-lock-only更新 lock 文件
Q:可以删除 package-lock.json 吗?
可以删,但执行 npm i 后会重新生成。如果团队成员版本不一致,删掉后重新生成反而能统一。
Q:node_modules 能删吗?
可以随便删。 删掉后执行 npm i 就能重新下载。如果你的项目仓库体积太大,通常做法就是删除 node_modules 再重新安装。
Q:一个项目有多个 package-lock.json 吗?
正常情况下只有根目录一个。如果项目里有多个子包(monorepo),可能会有多个。
Q:package-lock.json 存在时,npm i 就一定按它装吗?
默认是的。但如果 package.json 和 package-lock.json 记录的范围不一致,npm i 会以 package.json 为准,更新锁文件。如果你希望绝对按锁文件安装,可以改用 npm ci 命令。
本篇小结
node_modules 和 package-lock.json 是两个容易混淆的概念,记住这句就够了:
package.json说“我想要什么”,package-lock.json说“我装了什么”,node_modules放“装好的东西”。