1. 包(package)和模块(module)
A package is a file or directory that is described by a
package.json
.
A module is any file or directory that can be loaded by Node.js'require()
.
一般而言,npm package都可以使用require
来引入,但不尽然,
例如,某些cli package,它们只包含了命令行接口,并不能被require
导入,因此不是module。
2. 模块管理
2.1 npm v2
npm v2,采用了如下的模块组织方式,
将模块的依赖,放到该模块的子文件夹node_modules
中,
最终形成一个树形结构。
假设有三个模块,A,B,C,
A v1.0依赖了B v1.0,C v1.0依赖了B v2.0,如图所示,
又假设,我们的应用同时依赖了A v1.0和C v1.0,
很显然,我们必须保留模块B的两个不同版本,
npm v2将B v1.0和B v2.0,分别放到模块A v1.0和C v1.0,各自的子文件夹中,
然而,虽然从文件的组织方式上来看,我们可以按照上图来处理,
但是,某些module loader却不能同时向内存中载入不同版本的模块。
幸运的是,node的module loader没有这种限制。
2.2 npm v3
npm v3采用了不同的模块组织方式,
在文件系统中,npm v3将一些二级依赖直接放到了一级依赖的位置,
因此,文件系统的树形结构,与模块的依赖关系,并不是同构的,
并且,文件系统的树形结构,与模块的安装顺序有关。
假设我们有两个模块,A,B,其中A v1.0依赖了B v1.0,
那么依赖了模块A的应用,将按下图方式组织文件,
将B v1.0(二级依赖),直接放到了和A v1.0(一级依赖)并列的位置,
如果我们现在引入了新的模块C v1.0,它依赖了B v2.0,
由于在一级依赖的位置上,已经有了B的某个版本,
npm v3将会把B v2.0,放到C v1.0的子文件夹中,
可以看到,先安装A和先安装C是不同的,
先安装A,会将B v1.0放到一级依赖的位置,
而先安装C,会将B v2.0放到一级依赖的位置。
2.3 去重复
接着上面的例子,假设我们又引入了新的模块D,其中D v1.0依赖了B v2.0,
由于在一级依赖的位置上,已经有了B的某个版本,
和模块C的情况类似,npm v3将会把B v2.0,放到D v1.0的子文件夹中,
此外,如果E v1.0依赖了B v1.0,
则最终文件结构如下,
现在事情发生变化了,A升级了版本,它所依赖的B版本也发生了变化,A v2.0依赖了B v2.0,
不难想象,文件结构会变成这样,和新增一个A v2.0模块相似,
然而,假设E也升级了,E v2.0也依赖B v2.0了,会怎样,
npm v3会删除一级依赖处的B v1.0,因为没有模块依赖它了,
它还会安装B v2.0到一级依赖处,因为一级依赖位置没有B模块的任何版本了,
可是,这样下来,B v2.0发生了很多重复依赖,
我们可以执行以下命令,消去重复,
npm dedupe
结果,文件结构如下,
3. 文件结构的不确定性
我们看到,由npm v3模块管理策略所得到的文件结构,是与安装顺序相关的,
先安装的模块,其二级依赖会被放置到一级依赖处,
后安装的模块,其二级依赖会被放置到它的子文件夹中,
因此,不同的开发者,由于历史原因,更新模块的顺序不同,会导致工程的文件结构不同。
但这对开发工作是没有影响的。
文件系统只能体现模块组织的物理结构,其依赖关系的逻辑结构是相同的。
如果想要每个开发者的文件系统相同,
可以删除node_modules
重新全量npm install
,
这样就能保证,使用package.json
中指定的,相同的安装顺序安装模块,
得到的文件系统也将是相同的。
参考
Packages and Modules
npm v2 Dependency Resolution
npm v3 Dependency Resolution
npm3 Duplication and Deduplication
npm3 Non-determinism