前言
我们常常使用 npm 或者yarn,关于 npm 的安装机制和一些设计哲学,以及开发时候遇到一些 npm 的问题或者操作,我们是否了解其背后做了什么呢?
npm 或 Yarn 在工程项目中,除了负责依赖的安装和维护以外,还能通过 npm scripts 串联起各个职能部分,让独立的环节自动运转起来。
无论是 npm 还是 Yarn,它们的体系都非常庞大,在使用过程中你很可能产生如下疑问:
-
项目依赖出现问题时,删除大法好,即删除
node_modules和lockfiles,再重新install,这样操作是否存在风险? -
把所有依赖都安装到
dependencies中,不区分devDependencies会有问题吗? -
我们的应用依赖了公共库 A 和公共库 B,同时公共库 A 也依赖了公共库 B,那么公共库 B 会被多次安装或重复打包吗?
-
一个项目中,既有人用
npm,也有人用Yarn,这会引发什么问题? -
我们是否应该提交
lockfiles文件到项目仓库呢? 关于这些疑惑,我们自然要了解一些npm的原理,那让我们开始吧,先从npm的安装机制入手 ~
设计哲学
npm的安装机制秉承的设计哲学与很多包管理工具不同。
它会优先安装依赖包到当前项目目录,使得不同应用项目的依赖各成体系,同时还减轻了包作者的 API 兼容性压力,但这样做的缺陷也很明显:如果我们的项目 A 和项目 B,都依赖了相同的公共库 C,那么公共库 C 一般都会在项目 A 和项目 B 中,各被安装一次。这就说明,同一个依赖包可能在我们的电脑上进行多次安装。
安装机制
总体的流程图:

检查config(.npmrc)
npm install 执行之后,首先,检查并获取 npm 配置,这里的优先级为:项目级的 .npmrc 文件 > 用户级的 .npmrc 文件> 全局级的 .npmrc 文件 > npm 内置的 .npmrc 文件。
检查package-lock.js
-
如果有:则检查
package-lock.json和package.json中声明的依赖是否一致:- 一致,直接使用
package-lock.json中的信息,从缓存或网络资源中加载依赖; - 不一致,按照
npm版本进行处理(不同npm版本处理会有不同,具体处理方式如图所示)。
- 一致,直接使用
-
如果没有:则根据
package.json递归构建依赖树。然后按照构建好的依赖树下载完整的依赖资源,在下载时就会检查是否存在相关资源缓存:- 存在,则将缓存内容解压到
node_modules中; - 否则就先从
npm远程仓库下载包,校验包的完整性,并添加到缓存,同时解压到node_modules。 - 最后生成
package-lock.json。
- 存在,则将缓存内容解压到
构建依赖树
当前依赖项目不管其是直接依赖还是子依赖的依赖,都应该按照扁平化原则,优先将其放置在 node_modules 根目录(最新版本 npm 规范)。在这个过程中,遇到相同模块就判断已放置在依赖树中的模块版本是否符合新模块的版本范围,如果符合则跳过;不符合则在当前模块的 node_modules 下放置该模块(最新版本 npm 规范)。
注:由于不同的npm版本有不同的规则,因此同一个项目团队,应该保证 npm 版本的一致。
前端工程中,依赖嵌套依赖,一个中小型项目中的 node_modules 安装包可能已经非常多了。如果每次 install 过程都通过网络下载获取,必然是增加了时间成本。对于网络下载问题,缓存是一个很好的解决方案。
缓存机制
对于一个依赖包的同一版本进行本地化缓存,是当代依赖包管理工具的一个常见设计。使用时要先执行以下命令:
npm config get cache
得到配置缓存的根目录在 /Users/cehou/.npm( Mac OS 中,npm 默认的缓存位置) 当中。我们 cd 进入 /Users/cehou/.npm 中可以发现_cacache文件。事实上,在 npm v5 版本之后,缓存数据均放在根目录中的_cacache文件夹中。
(_cacache文件)
我们可以使用以下命令清除 /Users/cehou/.npm/_cacache 中的文件:
npm cache clean --force
你可以点击这里看看其中对应的 npm 源码。
接下来打开_cacache文件,看看 npm 缓存了哪些东西,一共有 3 个目录:
-
content-v2 -
index-v5 -
tmp
其中 content-v2 里面基本都是一些二进制文件。为了使这些二进制文件可读,我们把二进制文件的扩展名改为 .tgz,然后进行解压,得到的结果其实就是我们的 npm 包资源。
而 index-v5 文件中,我们采用跟刚刚一样的操作就可以获得一些描述性的文件,事实上这些内容就是 content-v2 里文件的索引。
这些缓存如何被储存并被利用的呢?
这就和 npm install 机制联系在了一起。当 npm install 执行时,通过pacote把相应的包解压在对应的 node_modules 下面。npm 在下载依赖时,先下载到缓存当中,再解压到项目 node_modules 下。pacote 依赖npm-registry-fetch来下载包,npm-registry-fetch 可以通过设置 cache 属性,在给定的路径下根据IETF RFC 7234生成缓存数据。
接着,在每次安装资源时,根据 package-lock.json 中存储的 integrity、version、name 信息生成一个唯一的 key,这个 key 能够对应到 index-v5 目录下的缓存记录。如果发现有缓存资源,就会找到 tar 包的 hash,根据 hash 再去找缓存的 tar 包,并再次通过pacote把对应的二进制文件解压到相应的项目 node_modules 下面,省去了网络下载资源的开销。
注意,这里提到的缓存策略是从 npm v5 版本开始的。在 npm v5 版本之前,每个缓存的模块在 ~/.npm 文件夹中以模块名的形式直接存储,储存结构是:{cache}/{name}/{version}。
结语
了解这些相对底层的内容可以直接帮助开发者排查 npm 相关问题,更多的npm内容我们可以查看官网npm。
参考:《前端基础建设与架构》- LucasHC(侯策)