谈谈npm依赖管理
2019-11-07

引言

现在的前端开发几乎都离不开nodejs的包管理器npm,比如前端在搭建本地开发服务以及打包编译前端代码等都会用到。在前端开发过程中,经常用到npm install来安装所需的依赖,至于其中的技术细节未做过多的理解,下面就来说说node包管理器npm。

依赖安装npm install

使用npm来管理nodejs的包依赖,需要在项目根目录下提供一个package.json文件,其中与包依赖相关的字段有:

dependencies: 指定项目运行时所依赖的模块devDependencies: 指定项目开发时所需要的模块peerDependencies:指定当前模块所在的宿主环境所需要的模块及其版本

其中,大家应该都知道:通过命令npm install --save $package来安装运行时依赖的模块,npm install --save-dev $package来安装本地开发时所依赖的模块。

需要指出的是,通过npm install来安装包依赖时,dependenciesdevDependencies是有区别的,具体如下:

dependencies指定的包只在以下两种情况下被安装:在一个含有package.json的目录下执行npm install在任意一个目录中执行 npm install $packagedevDependencies指定的包被安装的情况如下:在一个含有package.json的目录下执行npm install但是,执行 npm install --production该命令是不会安装devDependencies指定的包的执行 npm install $package --dev时会安装对应的依赖包; 但是, 执行npm install $package时是不会安装devDependencies指定的包的

所以:

我们经常在通过npm install $package来安装一个依赖包时,npm只会安装该依赖包的package.json文件中的devDependencies所指定的依赖包,devDependencies是不会被安装的。

另外,通过npm install $package在安装指定依赖包时,该包package.json中的peerDependencies所指定的依赖包及其版本,则是对依赖该$package包的宿主环境的要求。

若宿主环境没有安装对应的包或者安装的版本不满足要求,npm在安装过程中给出错误警告。

npm2与npm3+ 安装依赖的区别

npm在安装依赖包时,会将依赖包下载到当前的node_modules目录中。每个包安装过后都会有自己的node_modules吗?这又涉及到不同版本的npm其对包依赖的目录组织结构有所不同。

npm2 依赖安装

npm2依赖安装的时候比较简单直接,直接按照包依赖的树形结构下载填充本地目录结构,也就是说每个包都会将该包的依赖组织到当前包所在的node_modules目录中。

npm2这样设计的原因可能是引用文章[2]的一句话:

因为 npm 设计的初衷就是考虑到了包依赖的版本错综复杂的关系,同一个包因为被依赖的关系原因会出现多个版本,简单地填充结构保证了无论是安装还是删除都会有统一的行为和结构。

这样,一个项目App 里模块 A 和 C 都依赖 B,无论被依赖的 B 是否是同一个版本,都会生成下面的目录结构:

很明显:

这种依赖的组织结构,虽然简单的实现多版本兼容,但是可能造成目录结构嵌套比较深,并且很可能造成相同模块的大量冗余问题。

npm3+依赖安装

npm3则对依赖安装进行了改造,采用”扁平结构“的思路来组织依赖包的目录结构。具体的就是npm install时:

按照 package.json 里依赖的顺序依次解析,遇到新的包就把它放在第一级目录,后面如果遇到一级目录已经存在的包,会先判断版本,如果版本一样则忽略,否则会按照 npm2 的方式依次挂在依赖包目录下。

以上面的例子看一下对比结果:

这样,npm3+采用这种扁平结构部分的解决了npm2的痛点

为什么说是部分解决呢,npm3+并没有完美解决npm2中的问题,在某些情况下甚至会退化到npm2的行为。

例如项目App里依赖模块A、C、D、E, 其中A、C、D依赖模块B v2.0, E依赖模块B v1.0,生成的npm3结构如下

可以看到B、C、D模块下包含了各自依赖的B v2.0,存在代码冗余的情况。

那么是否可以解决这代码冗余问题呢,在E模块依赖的模块B升级到V2.0前提下,我们可以通过npm dedupe把所有二级的依赖模块B v2.0重定向到一级模块B下,如下图所示:

node_modules路径查找

上面说到了npm2与npm3依赖包组织结构的不同;那么如何找到对应的依赖包呢,例如项目访问webapck依赖包:

const webapck = require("webpack")

那么nodejs是怎么找到webpack模块的呢,这就是涉及到依赖包的路径查找问题。具体如下:

如果传递给require()的参数不是nodejs的核心模块,也不是以/.././开头,那么nodejs会尝试从当前模块所在目录开始,尝试在它的 node_modules 文件夹里加载相应模块,根据模块的package.json来加载对应的js文件;如果没有找到,那么就再向上一级目录移动,直到文件系统的根目录为止。

例如,假设在/home/wonyun/projects/foo.js 文件里调用了 require("bar.js") ,那么 nodejs 查找其位置的顺序依次为:

/home/wonyun/projects/node_modules/bar.js/home/wonyun/node_modules/bar.js/home/node_modules/bar.js/node_modules/bar.js

若果追踪到文件系统的根目录也没有找到对应的依赖,那么nodejs就会找不到对应模块的报错。

声明一下:以上图片使用文献[2]的图片加以说明。

参考文献

1、【npm】详解npm的模块安装机制2、npm2 npm3 yarn 的故事3、npm&&npmscript&&gulp&&webpack4、What"s the difference between dependencies, devDependencies and peerDependencies in npm package.json file?

, 1, 0, 9);