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:
{
"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:
{
"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,大致是下面几步:
- 比较
package.json和package-lock.json。lockfile 里解析出的版本满足package.json的范围,就用 lockfile 的确切版本;不满足,比如把^7.0.0改成了^8.0.0,就重新解析并更新 lockfile。 - 构建依赖树。npm 文档给的例子是,A 依赖 B 和 C,B 也依赖 C,那么 C 只装一份在顶层,B 直接用它。如果 B 要 D@1、C 要 D@2,D@1 放顶层,D@2 就嵌套到 C 下面。
- 下载并解压到
node_modules,--omit里列出的类型不写到磁盘。 - 执行生命周期脚本。
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决定。