npm install之依赖冗余

首先这是一个问题,但通过标题没法很好的表述出来,而大多数童鞋应该不会遇到这样的问题,然而我还是决定记录下来。

我们知道,npm3把所有依赖模块路径拍扁了(工程目录下的node_modules出现了很多package.json中没有声明的模块),解决了windows下路径名过长的问题,更使得公共依赖被充分共享。但如果多个模块依赖了同一个模块的不同版本,后安装的模块,为了不和别人冲突,就只能将这个依赖安装在自己的node_modules下。

这个问题来源于我们实际某个基于VueJS的复杂项目的某次意外,为了使广大前端更容易理解,我们使用jquery作为栗子。

假设,我们用webpack开发一个小页面,需要用到jquery和一个jquery插件(假设叫jplugin)。假设这个jquery插件,遵循了ES6规范,它的package.json声明依赖的jquery版本是^2.0.0(即>=2.0.0, <3.0.0):

1
2
3
4
5
{
"dependencies": {
"jquery": "^2.0.0"
}
}

它的主模块,应该会有类似这么几句:

1
2
3
4
5
import $ from 'jquery'
$.fn.jplugin = function () {
// 插件代码
}

而我们的项目直接依赖的jquery版本是^1.11.0(即>=1.11.0, <2.0.0):

1
2
3
4
5
{
"dependencies": {
"jquery": "^1.11.0"
}
}

很明显,^2.0.0^1.11.0并不能兼容。那么,在npm i之后,依赖目录大致是这样的(无关文件已省略):

1
2
3
4
5
6
7
8
.
├── node_modules
│   ├── jquery // ^1.11.0, 假定安装的是1.11.0版本
│   └── jplugin
│   ├── node_modules
│   │   └── jquery // ^2.0.0,假定安装的是2.0.0版本
│   └── package.json
└── package.json

我们的主模块代码应该会有类似这么几句:

1
2
3
4
5
6
import $ from 'jquery'
import 'jplugin'
// 其他代码
$('#someId').jplugin()

最终运行代码,浏览器会报错,提示jplugin不是一个function,也就是$('#someId').jplugin()这句出错。

为什么呢?

webpack打包依赖时,与node查找依赖的方式一致,顶级依赖写法(比如这里的jquery)首先从当前目录的node_modules下查找,如果不存在,则向上级目录的node_modules 下查找,如果不存在则继续向上,以此类推。

在我们这个例子里,jplugin模块本身查找到的jquery版本是2.0.0,在这个2.0.0jquery上才会存在jplugin这个扩展。而我们的主模块查找到的jquery版本是1.11.0版本,在这个1.11.0版本的jquery上调用jplugin就出错了。

说到这里,还并没有引出本文的主题,不过也能够解释依赖版本冲突导致发生奇怪问题的现象了。

继续这个例子,当我们发现版本依赖有问题时,我们要处理它。假设我们把项目直接依赖的jquery升级到2.0.0,并不会导致其他兼容问题,我们执行npm i jquery@2.0.0 --save. 我们看一下现在的目录结构:

1
2
3
4
5
6
7
8
.
├── node_modules
│   ├── jquery // ^2.0.0,假定安装的是2.0.0版本
│   └── jplugin
│   ├── node_modules
│   │   └── jquery // ^2.0.0,假定安装的是2.0.0版本
│   └── package.json
└── package.json

上层的jquery已经升级到2.0.0了。我们很兴奋的再次运行页面,发现仍然还是报同样的错。

其实问题的原因是一样的,虽然大家都是2.0.0版本的jquery,但 此jquery非彼jquery,大家拿到的并不是同一个jquery对象。

其实,此时只要把根目录的node_modules删除,再次执行npm i,看看最终的目录结构:

1
2
3
4
5
6
.
├── node_modules
│   ├── jquery
│   └── jplugin
│   └── package.json
└── package.json

如此一来,大家拿到的就是同一个jquery对象啦,最后页面运行正常了~

这个问题,才是本文要说的 依赖冗余 的问题,即,曾经由于依赖冲突导致安装了同个模块的多个版本,修正依赖版本后,再次npm i,仍然解决不了问题,删除全部node_modules重新安装依赖才正常。遗憾的是,npm并没有这么聪明,或者有其他原因,并没有在安装新模块后,自动帮我们梳理依赖关系。

那么,遇上这样的问题,就只能删除全部依赖彻底重新安装一次依赖吗?有没有更优雅的方式?

答案是npm dedupe(或简写npm ddp).

这个命令会重新计算所有依赖关系,把可以公用的依赖上升到顶级node_modlues,并删除底层不必要的 副本。注意,需要整理的模块越多,所花的时间也越长。

因此,建议在你的构建过程中添加此命令,从而避免这种问题的发生。

ps: npm update命令也同样存在此问题。

ps: 在写这篇文章之前,google到 玩转npm 这篇文章, 蛮有参考价值,建议阅读。