人人都会AI编程

15.2 package.json 核心字段全解

更新时间:2026-07-10

在 Node.js 项目中,package.json 不仅是项目的描述文件,更是一份依赖关系蓝图。其中两个核心概念直接影响依赖的安装行为与项目的可复现性:版本语义化规范(Semantic Versioning,简称 semver)依赖类型划分。正确理解并运用它们,是避免“在我电脑上能跑,换个环境就报错”的关键。

15.2.1 语义化版本规范(Semantic Versioning)

Node.js 生态中的包几乎都遵循 semver 规范,版本号格式为 主版本号.次版本号.修订号(MAJOR.MINOR.PATCH):

  • 主版本号(MAJOR):当你做了不兼容的 API 修改,递增第一位。比如 2.0.0 相对于 1.x.x,意味着弃用或大幅修改了旧功能。
  • 次版本号(MINOR):当你做了向下兼容的功能新增,递增第二位。例如从 1.2.01.3.0,新增了可选的函数或参数,但老代码仍能正常运行。
  • 修订号(PATCH):当你做了向下兼容的问题修正(Bug 修复),递增第三位。比如 1.2.31.2.4,只改了内部实现,不改变任何公开行为。

遵循规范的包会通过这三段数字向使用者传达变更幅度。在实际的 package.json 依赖声明中,我们很少写死一个确切版本,而是使用版本范围来描述可接受的版本区间:

{
  "dependencies": {
    "express": "^4.18.2",
    "lodash": "~4.17.21",
    "axios": "1.x",
    "chalk": "*"
  }
}

常见的版本前缀符号有:

  • ^(插入号):锁定主版本号,允许次版本和修订号自由升级。例如 ^4.18.2 等价于 >=4.18.2 <5.0.0。这是 npm 的默认安装行为,适用于大多数情况,因为次版本和修订版本理论上不会破坏兼容性。
  • ~(波浪号):锁定主版本和次版本,只允许修订号升级。例如 ~4.17.21 等价于 >=4.17.21 <4.18.0。当确信某个包的次版本号不稳定,或者希望更严格控制时使用。
  • *x**:表示任意版本,不推荐在项目依赖中使用,因为它会导致不同环境安装的版本完全不可控。1.x 表示接受主版本为 1 的任意版本。
  • 精确版本:直接写 4.18.2,只安装这个精确版本。通常配合 package-lock.json 使用,可避免意外升级。
  • 比较运算符:也可以使用 >=<|| 等,例如 ">=1.2.0 <2.0.0",但这在实际项目依赖中较少直接手写。

真实提醒:语义化规范依赖于包作者的自觉。不少 npm 包在次版本中不小心引入了破坏性变更,这被称为“semver 灾难”。因此即便声明了 ^,也不能完全信赖自动升级。此时锁文件(package-lock.jsonyarn.lockpnpm-lock.yaml)显得尤为重要,它记录了精确的版本号和校验和,确保整个团队和 CI 环境安装的依赖树完全一致。真正可靠的复现依赖装 npm ci 而非 npm install。

15.2.2 依赖类型区分

package.json 将依赖分为多个字段,每种类型对应不同的安装场景。理解它们的区别能避免把测试工具打进生产镜像,或者漏掉运行时必需但未声明的包。

dependencies(生产依赖)

这是项目运行时必不可少的包。当你在代码中使用 require('express')import express from 'express' 时,express 就应放在 dependencies 中。用户或部署环境执行 npm install(不加其他参数)时,这些包会被安装;如果使用 --productionNODE_ENV=production,则只安装 dependencies 中的包。

安装命令:

npm install express
# 或明确指定
npm install express --save

devDependencies(开发依赖)

只在开发、测试、构建过程中需要的包,例如测试框架(Jest)、代码检查工具(ESLint)、TypeScript 编译器、构建工具(Webpack、Vite)等。它们不会被最终用户使用,也不应出现在生产环境的 node_modules 中。

安装命令:

npm install jest --save-dev
# 或者 -D
npm install eslint -D

构建 Docker 镜像时,若使用多阶段构建,可在第一阶段安装 devDependencies 执行测试和构建,最终镜像只复制必要的产物和 dependencies,有效缩小镜像体积。

peerDependencies(同伴依赖)

用于声明当前包需要“宿主”提供某个特定版本的依赖,而不自动安装。最常见于插件包:比如 react-dom 需要与 react 配合使用,eslint-plugin-react 需要宿主项目中已安装 eslint。如果宿主项目的版本不符合,npm 会给出警告(npm 7+ 还会自动安装缺失的 peer 依赖,可能引发版本冲突,需要留意)。

package.json 中的写法:

{
  "peerDependencies": {
    "react": ">=16.8.0"
  }
}

这行声明的意思是:“我这个插件需要 react 的版本在 16.8.0 及以上,请确保宿主已安装。”

真实场景:若你开发一个通用的组件库,并希望消费者项目中只存在一份主框架实例、避免重复打包,就需要使用 peerDependencies。同时,webpack 等打包工具会将 peer 依赖设为 externals,不打包进最终产物,而是从宿主环境引用。

optionalDependencies(可选依赖)

某些包可能提供增强功能,但若安装失败(比如由于系统环境不支持)也不应阻塞整个安装流程。例如,一个跨平台的优化模块 fsevents(macOS 专用文件监听库),在 Windows 或 Linux 上安装会失败,但主功能仍可用,那么它就应该放进 optionalDependencies

npm 在安装 optionalDependencies 时会尽力为之,失败后只打印警告,继续余下安装。这对于需要兼容多种操作系统的工具包尤其重要。

bundledDependencies / bundleDependencies(打包依赖)

这是一个数组,指定哪些依赖将在发包(npm publish)时打包进 tar 文件。通常用于需要保留特定依赖版本、或者发布后无需网络下载的场景。比较少见,一般只在发布 CLI 工具或要在离线下使用的情况下采用。

15.2.3 依赖类型选择的常见误区

  • 把运行时需要的包放在 devDependencies:如 TypeORM、prisma 这样的运行时数据库 ORM,很容易被误认为是“开发工具”而放进 devDependencies。但实际应用在 NODE_ENV=production 下运行时会因找不到模块而崩溃。
  • 过度依赖 dependencies 而忽略清理:在项目迭代中,某些包可能不再使用,但依然留在 dependencies 中,导致 node_modules 膨胀。建议定期用工具(如 depcheck)检查无用依赖。
  • 忘记声明 peerDependencies:如果你写了一个 webpack 插件却未声明 peerDependencies,用户安装多个插件后可能出现多个 webpack 实例,导致运行时错误。
  • 锁文件未提交:如果团队中有人使用 npm install 安装了更低的 patch 版本,而另一个人用的时间点不同,得到的版本不一致,就会出现“我电脑上正常”的怪圈。务必把 package-lock.json 等锁文件纳入版本控制。使用 npm ci 可以严格按锁文件安装,保证环境一致。

15.2.4 pnpm 中的依赖类型特点

pnpm 在处理依赖类型上更严格。默认情况下,pnpm 只会让项目直接依赖的包能够被访问,间接依赖(幽灵依赖)是看不到的。这与 npm / yarn 的传统行为有所区别,也进一步凸显了正确声明依赖类型的重要性:如果你代码中 require 了一个未在 dependencies 中直接声明的包,pnpm 下会报错,而 npm 下可能因为扁平化恰好存在而侥幸运行。因此,pnpm 被认为是更严谨的实践选择,也更能帮助开发者保持依赖清单的准确。

15.2.5 小结

依赖版本语义化规范和类型划分,是 Node.js 工程化的基本素养。记住三个关键数字的含义,明确区分 dependenciesdevDependencies,并且在发布包时审慎使用 peerDependencies,可以让你的项目更加健壮、可复现,也让团队协作少了无数个“版本为什么不对”的排查痛苦。结合锁文件和 npm ci,这些规范才真正成为生产落地的基石。