前端知库

dependencies、devDependencies、peerDependencies 和 optionalDependencies 有什么区别?npm install 做了什么?

dependencies 是运行时要用的包,别人安装你的包时也会装上;devDependencies 只在开发包本身时需要,只有在包的根目录执行 npm install 才会安装,--omit=dev 或 NODE_ENV=production 时跳过;peerDependencies 声明“需要和宿主的某个版本一起使用”,npm 7 起默认自动安装,版本冲突会报错;optionalDependencies 安装失败不会让整个安装失败,代码要自己处理它不存在的情况。npm install 先看 lockfile 是否满足 package.json 的版本范围,满足就按 lockfile 装,再按扁平化算法把依赖尽量放在顶层,最后执行 preinstall、install、postinstall 等脚本。

参考答案

简答

比如写一个 React 组件库:运行时要调用 clsx,所以它放 dependencies,别人安装你的组件库时会一起装上;测试用的 vitest、打包用的工具只在开发组件库时需要,放 devDependencies,使用者安装时不会装它们;React 本身应该由使用者的项目提供,组件库只声明“我需要和 React 18 或 19 一起用”,这就是 peerDependencies。

四种依赖的区别在于“谁装、什么时候装”。devDependencies 只有在包的根目录执行 npm install 时才会装,加上 --omit=dev,或者 NODE_ENV 设为 production,也会跳过。peerDependencies 在 npm 3 到 6 里不会自动安装,只给警告;从 npm 7 开始默认会安装,和别的包要求的版本冲突时会报错。optionalDependencies 是装不上也没关系的依赖,安装或构建失败不会让整个安装失败,但代码要自己处理它不存在的情况。

npm install 不带参数时,先比较 package.json 和 package-lock.json:lockfile 里的版本满足 package.json 的范围,就按 lockfile 装;不满足就重新解析并更新 lockfile。然后按扁平化算法安排目录,依赖尽量放在顶层的 node_modules,版本冲突的才嵌套到使用它的包下面。最后执行 preinstall、install、postinstall 这些生命周期脚本。

要注意,--omit 只是不把包装到磁盘上,这些依赖仍然会被解析并写进 lockfile。另外 npm 文档建议 peerDependencies 的范围写宽一些,比如 ^1.0,写死补丁版本容易和别的插件冲突。

四种依赖装在哪里

先看一个组件库的 package.json:

json
{
  "name": "my-ui",
  "dependencies": {
    "clsx": "^2.1.0"
  },
  "devDependencies": {
    "vitest": "^3.0.0"
  },
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0"
  },
  "optionalDependencies": {
    "fsevents": "^2.3.0"
  }
}

使用者在自己的项目里执行 npm install my-ui,会装上 clsx 和 react(npm 7 起自动安装 peer),fsevents 装得上就装,装不上也不报错;vitest 不会装。只有维护 my-ui 的人在它的根目录执行 npm install,才会装上 vitest。

npm 文档对 dependencies 有一句明确提醒:不要把测试框架、编译工具这类开发时用的东西放进 dependencies。放进去的话,所有使用者都要多下载一堆用不到的包。

为什么 React 不能直接放 dependencies?如果组件库自己依赖一份 React,使用者的项目又有一份,两份版本不兼容时,npm 会把组件库那份嵌套安装在它自己下面,页面里就同时存在两份 React。peerDependencies 的意思是“我不自己带,用宿主项目那份”,npm 会检查宿主提供的版本是否在范围内。

peerDependencies 的冲突和可选 peer

npm 7 开始自动安装 peer,带来一个变化:如果两个插件要求的宿主版本没有交集,依赖树就解不出来,安装会报错。这时可以用 --legacy-peer-deps,npm 会像 3 到 6 版那样完全忽略 peerDependencies。npm 文档不推荐这样做,因为它不再保证 peer 的版本约定,有些包可能依赖这个约定才能正常工作。

有些宿主是可选的,比如一个工具同时支持 Sass 和 Less,不需要使用者两个都装。可以在 peerDependenciesMeta 里把它标成 optional: true,npm 就不会自动安装这个 peer:

json
{
  "peerDependencies": {
    "sass": "^1.0.0",
    "less": "^4.0.0"
  },
  "peerDependenciesMeta": {
    "less": { "optional": true }
  }
}

optionalDependencies 要自己兜底

npm 文档的例子是用 try...catch 去加载可选依赖,加载失败就把它设为 null,后面用之前先判断。原因是 npm 只保证“装不上不报错”,不会替你处理“没装上时代码怎么办”。另外,同名的包如果同时写在 dependencies 和 optionalDependencies 里,后者会覆盖前者,所以最好只写在一处。加上 --omit=optional 可以完全不装可选依赖。

npm install 的流程

不带参数执行 npm install,大致是下面几步:

  1. 比较 package.json 和 package-lock.json。lockfile 里解析出的版本满足 package.json 的范围,就用 lockfile 的确切版本;不满足,比如把 ^7.0.0 改成了 ^8.0.0,就重新解析并更新 lockfile。
  2. 构建依赖树。npm 文档给的例子是,A 依赖 B 和 C,B 也依赖 C,那么 C 只装一份在顶层,B 直接用它。如果 B 要 D@1、C 要 D@2,D@1 放顶层,D@2 就嵌套到 C 下面。
  3. 下载并解压到 node_modules,--omit 里列出的类型不写到磁盘。
  4. 执行生命周期脚本。preinstall 在依赖下载解压之前运行,install 和 postinstall 在依赖装好之后运行。

扁平化带来的幽灵依赖问题见《npm、yarn 和 pnpm 有什么区别?幽灵依赖是什么?》,lockfile 和 npm ci 的区别见《为什么需要提交 lockfile?npm ci 和 npm install 有何区别?》。

常见追问

怎样把包安装到不同的依赖类型里?

npm install 包名 默认写进 dependencies;加 -D(--save-dev)写进 devDependencies;加 -O(--save-optional)写进 optionalDependencies;加 --save-peer 写进 peerDependencies。

线上部署时怎样不装开发依赖?

执行 npm install --omit=dev。NODE_ENV 为 production 时,omit 的默认值就是 dev。npm 文档还说明,最终的 omit 列表包含 dev 时,npm 会把生命周期脚本里的 NODE_ENV 设为 production。要注意,前端项目通常在构建时需要 devDependencies 里的打包工具,所以构建阶段不能省略它们。

bundleDependencies 是做什么的?

它是一个包名数组,执行 npm pack 时,这些依赖会被一起打进 tarball。适合要离线分发、或者希望用一个文件就能装好的场景。版本仍然由 dependencies 决定。