dependencies、devDependencies 和 peerDependencies 有什么区别?
简化版
dependencies 是运行时需要的依赖,devDependencies 是开发、构建、测试阶段需要的依赖,peerDependencies 表示当前包要求使用方提供某个宿主依赖,常见于 React 组件库、Webpack 插件、Babel 插件等。
详细版
应用项目里,React、axios 这类运行时代码会进 dependencies;TypeScript、ESLint、Vite、测试工具通常放 devDependencies。库项目要更谨慎:如果把 React 打进组件库的 dependencies,使用方可能加载两份 React,因此通常把 React 放到 peerDependencies,再在本库开发时放一份到 devDependencies。
peerDependencies 的含义不是“我会安装它”,而是“我需要宿主环境有一个兼容版本”。npm 7 以后会尝试自动安装 peer,但工程上仍要理解它表达的是兼容契约。
回答时最好结合包类型:应用、库、插件的依赖策略不同,不能机械背字段定义。
完整版教学
一、依赖字段本质是在描述包的运行边界
package.json 不是简单的清单,它描述当前包在不同阶段需要什么。应用项目、组件库、构建插件面对的“运行环境”不同,所以依赖字段也不同。
应用最终会部署给用户,运行时代码需要的包必须能进入产物或运行环境。库会被别人安装,它要避免把宿主已经存在的框架重复带进去。插件则依赖某个宿主工具,比如 Webpack、Vite、Babel 或 ESLint。
记忆钩子:dependencies 管自己运行,devDependencies 管自己开发,peerDependencies 管和宿主的兼容关系。
二、dependencies 放运行时真正需要的包
如果你的业务代码在浏览器运行时调用 axios.get(),那么 axios 是运行时依赖。构建产物虽然可能把 axios 打进 bundle,但从语义上它仍是应用运行需要的一部分。
{
"dependencies": {
"axios": "^1.7.0",
"react": "^18.2.0"
}
}
对 Node 服务也是一样,Express、数据库驱动、日志库如果线上启动需要,就属于 dependencies。判断标准不是“会不会被打包”,而是“没有它,线上功能能不能跑”。
三、devDependencies 放开发构建阶段需要的包
devDependencies 通常包括编译器、测试工具、格式化工具、类型包、构建工具等。它们服务于开发和交付流程,不应该成为业务运行时的必要条件。
| 依赖 | 常见字段 | 原因 |
|---|---|---|
| typescript | devDependencies | 编译检查工具 |
| eslint | devDependencies | 代码质量工具 |
| vite | devDependencies | 本地开发和构建工具 |
| vitest | devDependencies | 测试工具 |
| @types/react | devDependencies | 类型声明 |
注意,前端应用生产构建时仍会安装 devDependencies,因为构建步骤需要它们。不能说“生产环境一定不安装 devDependencies”,这取决于部署流程是先构建再只发布产物,还是服务器上现场构建。
四、peerDependencies 表达宿主依赖契约
组件库最典型。一个 React 组件库不应该强行带一份自己的 React,否则应用里可能出现两份 React,导致上下文、hooks 或渲染器行为异常。因此组件库会声明自己需要使用方提供 React。
{
"peerDependencies": {
"react": ">=18 <20",
"react-dom": ">=18 <20"
},
"devDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
这表示:开发组件库时本地需要 React,所以放 devDependencies;发布给用户时要求宿主应用提供兼容 React,所以放 peerDependencies。
五、插件类包为什么经常使用 peerDependencies
Webpack loader、Babel 插件、ESLint 插件、Vite 插件都运行在宿主工具体系内。插件通常要和宿主工具的 API 版本匹配,因此会用 peer 说明兼容范围。
例如一个 ESLint 插件可能要求 eslint >=8。如果它自己带一份 ESLint,就可能和用户项目实际运行的 ESLint 不是同一个实例,规则加载和配置解析会出问题。
用户项目 eslint
-> 加载 eslint-plugin-xxx
-> 插件声明 peerDependencies: eslint >= 8
peer 的价值是把版本冲突暴露给安装阶段,而不是等到运行时报一个模糊的插件 API 错误。
六、optionalDependencies 和 overrides 也常被追问
optionalDependencies 表示依赖安装失败也不一定让整个安装失败,常用于跨平台原生包或可选能力。比如某些平台专用二进制包,只在对应系统上需要。
overrides 用于在根项目强制覆盖传递依赖版本,常见场景是修补安全漏洞或统一依赖版本。它很有用,但也可能打破上游包的兼容假设,所以需要配合测试。
| 字段 | 核心含义 | 常见场景 |
|---|---|---|
| optionalDependencies | 可选安装 | 平台相关能力 |
| overrides | 根项目覆盖传递依赖 | 安全漏洞修补、版本统一 |
| peerDependenciesMeta | 标记 peer 可选 | 插件支持多个宿主 |
七、应用项目和库项目的策略不同
应用项目以可运行为目标,依赖更偏“我自己要用什么”。库项目以被集成为目标,依赖更偏“哪些应该由使用方决定”。这就是很多人把库项目依赖写错的原因。
假设组件库把 React 放进 dependencies,应用本身也装了 React。打包配置如果没有 external 掉 React,用户可能多下载 40 KB 以上的重复代码;更严重的是运行时可能出现两个 React 实例。
库项目常见策略是:框架放 peer,构建工具放 dev,小型纯函数运行依赖可以放 dependencies,发布构建时把 peer external 掉。
八、常见误区与追问
- 误区:devDependencies 线上一定不会被安装。 如果线上环境负责构建,就仍可能安装;区别在语义和部署阶段。
- 误区:peerDependencies 是开发依赖。 peer 表达宿主兼容契约,开发本地通常还要配一份 devDependencies。
- 误区:所有库依赖都应该放 peer。 只有需要宿主共享或版本协同的依赖适合 peer,小型内部工具依赖可以放 dependencies。
- 追问:React 组件库为什么把 React 放 peer? 为了避免多 React 实例,并让宿主应用控制框架版本。
- 追问:overrides 适合解决什么问题? 适合根项目临时统一或修补传递依赖版本,但需要测试验证兼容。
- 追问:怎么判断依赖放哪里? 看它是运行时必须、开发构建必须,还是需要使用方提供的宿主依赖。
九、加强记忆
把依赖字段记成“三个边界”:运行边界用 dependencies,开发边界用 devDependencies,宿主兼容边界用 peerDependencies。应用项目关注自己能跑,库和插件关注别人集成时不重复、不冲突、版本可协商;回答时带 React 组件库或 ESLint 插件例子最稳。