目录
核心问题
问题场景
当一个包(pkgA)构建时将其依赖(pkgB)的代码打包进bundle后,如果pkgA的package.json中仍将pkgB声明为dependencies,那么用户安装pkgA时仍会下载安装pkgB,造成:
- 磁盘空间浪费(重复代码)
- 潜在的版本冲突
- 安装时间增加
依赖类型对比
| 依赖类型 | 谁安装 | 何时使用 | 示例场景 |
|---|---|---|---|
| dependencies | npm自动安装 | 包运行时必需,不期望用户提供 | 工具函数、纯JS库 |
| peerDependencies | 用户安装 | 需要与用户项目共享实例 | React组件库、插件系统 |
| devDependencies | npm开发时安装 | 仅开发、测试、构建需要 | TypeScript、测试框架 |
| optionalDependencies | 可选安装 | 功能增强,非必需 | 平台特定优化 |
打包策略
策略一:完全打包(自包含)
// package.json
{
"name": "pkgA",
"devDependencies": {
"pkgB": "^1.0.0" // 仅开发需要
}
// 不声明 dependencies 或 peerDependencies
}
配置示例(Webpack):
// 不配置 externals,所有依赖都打包
module.exports = {
// ... 无 externals 配置
};
适用场景:
- 工具库、独立应用
- 依赖体积小、API稳定
- 希望用户安装简单
优点:
- 用户安装简单:
npm install pkgA - 无版本冲突风险
- 自包含,无外部依赖
缺点:
- bundle体积较大
- 依赖无法单独更新
策略二:外部化 + peerDependencies
// package.json
{
"name": "pkgA",
"peerDependencies": {
"pkgB": "^1.0.0" // 用户需要安装
},
"devDependencies": {
"pkgB": "^1.0.0" // 开发测试用
}
}
配置示例(Webpack):
module.exports = {
externals: {
pkgB: {
commonjs: "pkgB",
commonjs2: "pkgB",
amd: "pkgB",
root: "PkgB",
},
},
};
适用场景:
- 插件、组件库
- 依赖体积大、频繁更新
- 需要与用户项目共享依赖实例
优点:
- bundle体积小
- 用户可控制依赖版本
- 依赖可单独更新
缺点:
- 用户需要额外安装
- 可能版本不兼容
策略三:混合策略
// package.json
{
"peerDependencies": {
"react": "^18.0.0", // 外部化,用户安装
"lodash": "^4.0.0" // 外部化,用户安装
},
"dependencies": {
"tiny-utils": "^1.0.0" // 完全打包,不外部化
}
}
peerDependencies详解
核心概念
peerDependencies表示:"我的包需要这些依赖,但我不自己安装,我希望使用我的人来安装这些依赖。"
实际示例
// React组件库示例
{
"name": "my-react-components",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
}
npm版本行为差异
| npm版本 | 行为 |
|---|---|
| npm 6及以下 | 只警告,不自动安装peerDependencies |
| npm 7+ | 默认自动安装peerDependencies |
强制指定为可选
{
"peerDependencies": {
"some-optional-dep": "^1.0.0"
},
"peerDependenciesMeta": {
"some-optional-dep": {
"optional": true
}
}
}
常见错误与解决方案
错误1:代码已打包,仍声明为dependencies
// ❌ 错误做法
{
"dependencies": {
"pkgB": "^1.0.0" // 代码已打包,但还声明依赖
}
}
结果: 用户安装两份pkgB(一份打包,一份独立)
错误2:dependencies和peerDependencies重复声明
// ❌ 错误做法(npm 7+会报错)
{
"dependencies": {
"pkgB": "^1.0.0"
},
"peerDependencies": {
"pkgB": "^2.0.0" // 版本冲突!
}
}
解决方案:
// ✅ 正确做法
{
"peerDependencies": {
"pkgB": "^2.0.0" // 只保留一个
},
"devDependencies": {
"pkgB": "^2.0.0" // 开发用
}
}
错误3:忘记配置externals
// package.json
{
"peerDependencies": {
"react": "^18.0.0"
}
}
// webpack.config.js - ❌ 忘记配置externals
module.exports = {
// 缺少 externals 配置,react仍会被打包
};
错误4:相同版本同时声明(反模式)
// ❌ 技术可行但逻辑混乱
{
"dependencies": {
"pkgB": "^1.0.0"
},
"peerDependencies": {
"pkgB": "^1.0.0" // 完全相同版本
}
}
问题:
- dependencies 说"我硬依赖这个,已打包到代码中"
- peerDependencies 说"我需要用户提供这个,与用户共享实例"
- 两个声明自相矛盾,虽然技术上可行但不推荐
解决方案: 选择其中一种策略,不要混淆
npm依赖安装机制深度解析
npm 扁平化策略(npm 3+)
modern npm 采用扁平化安装策略来避免重复安装和过深的目录结构:
正常情况(扁平化):
node_modules/
myPkg/
pkgB/ ← 被提升到顶级,多个包可共享
版本冲突(嵌套):
node_modules/
myPkg/
node_modules/
pkgB@1.0.0/ ← 无法共享,嵌套安装
pkgB@2.0.0/ ← 用户要求的版本
各种配置组合的完整分析
组合1:仅 dependencies
{
"dependencies": { "pkgB": "^1.0.0" }
}
| 场景 | 安装结果 | 位置 | 说明 |
|---|---|---|---|
| 用户项目不需要 pkgB | ✅ 安装 | node_modules/pkgB/ | 扁平化,顶级安装 |
| 用户需要 pkgB@1.0.0 | ✅ 安装 | node_modules/pkgB/ | 扁平化,共享一个副本 |
| 用户需要 pkgB@2.0.0 | ✅ 安装(两个版本) | node_modules/myPkg/node_modules/pkgB@1.0.0/ + node_modules/pkgB@2.0.0/ | 版本冲突,嵌套安装 |
特点: 标准硬依赖,没有问题
组合2:仅 peerDependencies
{
"peerDependencies": { "pkgB": "^1.0.0" }
}
| 场景 | 安装结果 | 位置 | 说明 |
|---|---|---|---|
| 用户项目不需要 pkgB | ❌ 不安装 | — | npm 7+ 会警告,用户使用时会报错 |
| 用户需要 pkgB@1.0.0 | ✅ 安装 | node_modules/pkgB/ | 用户安装后,myPkg 可以访问 |
| 用户需要 pkgB@2.0.0 | ❌ 版本不兼容 | — | 可能运行时报错 |
特点: 用户必须自己提供依赖,常用于插件系统
组合3:同版本的 dependencies + peerDependencies
{
"dependencies": { "pkgB": "^1.0.0" },
"peerDependencies": { "pkgB": "^1.0.0" }
}
| 场景 | 安装结果 | 位置 | 说明 |
|---|---|---|---|
| 用户项目不需要 pkgB | ✅ 安装 | node_modules/pkgB/ | 扁平化,顶级安装 |
| 用户需要 pkgB@1.0.0 | ✅ 安装 | node_modules/pkgB/ | 扁平化,共享一个副本 |
| 用户需要 pkgB@2.0.0 | ⚠️ 版本冲突 | node_modules/myPkg/node_modules/pkgB@1.0.0/ + node_modules/pkgB@2.0.0/ | 虽然技术可行,但逻辑混乱 |
特点: 反模式,技术可行但不推荐(npm 7+ 允许但提示冗余)
组合4:不同版本的 dependencies + peerDependencies
{
"dependencies": { "pkgB": "^1.0.0" },
"peerDependencies": { "pkgB": "^2.0.0" }
}
| npm 版本 | 安装结果 | 说明 |
|---|---|---|
| npm 6 及以下 | ⚠️ 警告但继续 | 允许但有警告,可能导致意外行为 |
| npm 7+ | ❌ 报错 | 直接拒绝,要求修复配置 |
特点: 配置冲突,绝对不能这样用
组合5:dependencies + peerDependencies + peerDependenciesMeta(optional)
{
"dependencies": { "pkgB": "^1.0.0" },
"peerDependencies": { "pkgB": "^1.0.0" },
"peerDependenciesMeta": { "pkgB": { "optional": true } }
}
| 场景 | 安装结果 | 位置 | 说明 |
|---|---|---|---|
| 用户项目不需要 pkgB | ✅ 安装 | node_modules/pkgB/ | 扁平化,dependencies 优先,optional 被忽视 |
| 用户需要 pkgB@1.0.0 | ✅ 安装 | node_modules/pkgB/ | 扁平化,共享一个副本 |
| 用户需要 pkgB@2.0.0 | ⚠️ dependencies 优先 | node_modules/myPkg/node_modules/pkgB@1.0.0/ + node_modules/pkgB@2.0.0/ | optional 标记在此情况下无效 |
特点: dependencies 硬依赖优先级更高,optional 标记被忽视
配置方案综合对比
| 配置方案 | 是否下载 | 安装位置(无冲突) | 是否可共享 | 用户体验 | 推荐指数 |
|---|---|---|---|---|---|
| 仅 dependencies | ✅ | node_modules/pkgB/ | ✅ | 自动,简单 | ⭐⭐⭐⭐⭐ |
| 仅 peerDependencies | ❌ | — | ✅ | 需自己装 | ⭐⭐⭐⭐ |
| 两者同版本 | ✅ | node_modules/pkgB/ | ✅ | 自动,简单 | ⭐⭐ (反模式) |
| 两者不同版本 | ❌ | npm 7+ 报错 | ❌ | 配置冲突 | ⭐ (禁用) |
| dependencies + optional | ✅ | node_modules/pkgB/ | ✅ | 自动,简单 | ⭐ (反模式) |
实际使用建议
-
优先使用「仅 dependencies」(大多数场景)
- 简单清晰,无版本冲突
- 用户零配置
- npm 扁平化自动处理重复安装
-
选择「仅 peerDependencies」(特定场景)
- 插件系统、中间件框架
- React/Vue 组件库
- 需要用户与包共享同一实例
-
避免所有混合配置
- 如果需要同时声明,代表设计有问题
- 重新评估是否真的需要同时配置两个
最佳实践总结
决策流程图
开始构建包
↓
评估依赖特点
├── 体积小、稳定、纯JS → 完全打包 + 不声明依赖
├── 体积大、频繁更新 → 外部化 + peerDependencies
└── 混合情况 → 分类处理
具体指南
-
完全打包策略(适合工具库)
- 不配置externals
- 不声明dependencies(可在devDependencies声明)
- 优点:用户安装简单
-
外部化策略(适合插件/组件库)
- 配置externals
- 声明peerDependencies
- 在devDependencies声明开发版本
- 优点:用户控制版本
-
绝对避免:
- 代码打包了,还声明为dependencies
- 同时在dependencies和peerDependencies声明同一依赖
- 忘记配置externals但使用peerDependencies
发布前检查清单
- 是否所有打包的依赖都已从dependencies移除?
- externals配置是否正确?
- peerDependencies版本范围是否合理?
- 是否在devDependencies中声明了开发版本?
- README是否说明了安装要求?
工具与验证
检查命令
# 查看依赖树
npm list [package-name]
# 查看包大小
du -sh node_modules/[package-name]
# 查看重复包
npm ls --depth=10 | grep -E "dedupe|multiple"
# 检查实际发布内容
npx npm-packlist
验证脚本
// scripts/verify-deps.js
const fs = require("fs");
const pkg = require("./package.json");
console.log("🔍 检查依赖声明...\n");
// 检查重复声明
const deps = Object.keys(pkg.dependencies || {});
const peerDeps = Object.keys(pkg.peerDependencies || {});
const conflicts = deps.filter((dep) => peerDeps.includes(dep));
if (conflicts.length > 0) {
console.error("❌ 发现重复声明:");
conflicts.forEach((dep) => {
console.error(
` ${dep}: dependencies=${pkg.dependencies[dep]}, peerDependencies=${pkg.peerDependencies[dep]}`,
);
});
process.exit(1);
}
console.log("✅ 依赖声明检查通过");
测试安装
# 1. 链接本地包
cd /path/to/pkgA
npm link
# 2. 在测试项目中使用
cd /path/to/test-project
npm link pkgA
npm install
# 3. 检查安装结果
npm list pkgB
记住黄金法则:要么完全打包(不声明依赖),要么完全外部化(使用peerDependencies)。避免中间状态导致的重复安装和版本冲突。