4. 重复依赖检测

依赖膨胀这事儿,很多时候不是我们主动引入的。你装一个包,它依赖另一个包,那个包又依赖另一个……结果同一个库,被不同版本的依赖链拉进来了好几次。

我见过最夸张的一次,项目里同时装了 7 个版本的 lodash。7 个啊!每个版本都占一份磁盘空间,打包时也各自独立打包。说白了,这就是白花花的银子在燃烧。

为什么会出现重复依赖?

根本原因就一个:依赖树里不同分支对同一个包的要求版本不一致。

举个例子:

  • 包 A 依赖 lodash@^4.17.0
  • 包 B 依赖 lodash@^4.16.0

如果 npm 觉得这两个版本范围不能合并,它就会各自安装一份。嗯,这就是重复的源头。

核心原则:只要两个版本范围有交集,理论上就可以合并成一个版本。npm dedupe 干的就是这事儿。

方法一:npm dedupe

这是最直接的办法。在项目根目录运行:

npm dedupe

它会遍历整个 node_modules,把能合并的包版本合并成一个。我习惯在每次安装新依赖后跑一遍这个命令,尤其是团队协作的项目。

小技巧:npm dedupe 不会改变 package.json 里的声明,它只动 node_modules 的物理结构。所以放心跑,不会搞乱你的配置。

不过要注意,npm dedupe 不是万能的。如果两个版本范围真的没有交集,比如 ^4.0.0 和 ^5.0.0,那它也没办法。这时候就需要手动介入了。

方法二:pnpm dedupe

如果你用的是 pnpm,那更简单。pnpm 天生就是扁平化的依赖管理,重复的情况比 npm 少很多。但也不是完全没有。

pnpm dedupe

pnpm 的 dedupe 更激进一些。它会尝试把所有能合并的版本都合并到 pnpm-lock.yaml 里。我个人觉得 pnpm 在这块做得比 npm 好,因为它有更严格的版本解析策略。

我曾经踩过的坑:有一次在 CI 环境里跑 pnpm dedupe,结果锁文件变了,导致构建缓存失效。后来我学乖了,只在本地开发时跑 dedupe,CI 里只做安装不做优化。

方法三:手动分析 package-lock.json

有时候自动工具搞不定,就得自己动手了。package-lock.json 里记录了所有依赖的完整树形结构,我们可以直接翻它。

怎么找重复?我一般用这个命令:

grep -o '"resolved": "[^"]*"' package-lock.json | sort | uniq -d

它会列出所有被多次引用的 resolved URL。如果同一个包名出现了多次,那就是重复了。

更直观的做法是写个脚本:

const lockfile = require('package-lock.json');
const deps = {};

function walk(depsObj, parent) {
  for (const [name, info] of Object.entries(depsObj)) {
    const key = `${name}@${info.version}`;
    if (!deps[key]) deps[key] = [];
    deps[key].push(parent);
    if (info.dependencies) walk(info.dependencies, key);
  }
}

walk(lockfile.dependencies, 'root');

// 输出所有被多个父包引用的依赖
for (const [key, parents] of Object.entries(deps)) {
  if (parents.length > 1) {
    console.log(`${key} 被 ${parents.length} 个父包引用:`);
    console.log(`  ${parents.join(', ')}`);
  }
}

这个脚本会告诉你:哪个包被多少个不同的父包依赖了。如果同一个版本被多个父包引用,那说明可以合并。如果不同版本被引用,那就得看能不能升级到同一个版本。

实战中的决策树

遇到重复依赖时,我一般按这个顺序处理:

  1. 先跑自动 dedupe:npm dedupe 或 pnpm dedupe,看能不能自动解决。
  2. 检查锁文件:如果自动工具没搞定,手动分析锁文件,找出哪些包重复了。
  3. 判断能否升级:看看重复的版本之间差距大不大。如果只是小版本差异,比如 4.17.0 和 4.18.0,那直接升级到最新的就行。
  4. 考虑 fork 或 alias:如果两个版本真的不兼容,比如一个依赖 2.x 一个依赖 3.x,那就得考虑用 pnpm 的 alias 功能,或者自己 fork 一个兼容版本。

我的经验:80% 的重复依赖都能通过升级子依赖的版本范围来解决。比如某个包声明了 "lodash": "^4.17.0",你把它改成 "lodash": "^4.0.0",就能和另一个需要 4.16.0 的包合并了。

知识体系图

下面这张图帮你理清重复依赖检测的完整流程:

重复依赖检测流程 发现重复依赖 运行 npm/pnpm dedupe 检查锁文件确认结果 是否还有重复? 否 优化完成 是 手动分析锁文件 判断能否升级 或使用 alias 再次检查是否还有重复 重复依赖消除

总结一下

重复依赖检测,说白了就是给 node_modules 做“瘦身”。自动工具能解决大部分问题,但遇到顽固的重复,还是得手动分析锁文件。

我个人建议:每次提交代码前,都跑一遍 dedupe。养成习惯后,你会发现项目体积慢慢就降下来了。别等到 CI 报警了才去处理,那时候可能已经积重难返了。

最后一个小提醒:如果你用 npm,记得把 package-lock.json 提交到版本控制里。这样团队其他人拉代码后,也能享受到你 dedupe 的成果。

好了,这一节就聊到这儿。记住,重复依赖不是 bug,但放任不管就是技术债。早发现早处理,你的项目会感谢你的。