本篇文章分享来自小伙伴「huanxing」的一次学习总结分享,希望跟社区的同学一起探讨。
什么是 NPM
npm(Node 包管理器)是 JavaScript 运行时 Node.js 的默认程序包管理器。
它也被称为 "Ninja Pumpkin Mutants"、"Nonprofit Pizza Makers",以及许多其他名称,可以在 npm-expansions 上探索这些名称。
npm 由两个主要部分组成:
- 用于发布和下载程序包的 CLI(命令行界面)工具
- 托管 JavaScript 程序包的 在线存储库
为了更直观地解释,我们可以将存储库 npmjs.com 视为一个物流集散中心,该中心从 npm 包裹的作者,那里接收货物的包裹,并将这些货物分发给 npm 包裹的用户。
NPM 安装流程
- 第一步:执行安装命令之后,npm 首先会去查找 npm 的配置信息。 其中,我们最熟悉的就是安装时候的源信息。npm 会在项目中查找是否有 .npmrc 文件,没有的话会再检查全局配置的 .npmrc ,还没有的话就会使用 npm 内置的 .npmrc 文件。
- 第二步:获取完配置文件之后,就会构建依赖树。 首先会检查下项目中是否有 package-lock.json 文件:存在 lock 文件的话,会判断 lock 文件和 package.json 中使用的依赖版本是否一致,如果一致的话就使用 lock 中的信息,反之就会使用 npm 中的信息;那如果没有 lock 文件的话,就会直接使用 package.json 中的信息生成依赖树。
- 第三步:在有了依赖树之后,就可以根据依赖树下载完整的依赖资源。 在下载之前,会先检查下是否有缓存资源,如果存在缓存资源的话,那么直接将缓存资源解压到 node_modules 中。如果没有缓存资源,那么会先将 npm 远程仓库中的包下载至本地,然后会进行包的完整性校验,校验通过后将其添加的缓存中并解压到 node_modules 中。npm 默认不会将依赖安装到全局,只会安装到当前的路径下,这样设计是为了不同的项目之间进行依赖隔离,互不影响。当然,也可以选择安装到全局,只需要在安装命令后带上 -g 参数即可。
- 第四步:生成 package-lock.json 文件。 那么这个文件是干什么的呢?我们都知道,在 package.json 文件中,如果我们在依赖的版本号前增加 ^ 标志的话,比如 ^3.1.6 意味着安装的时候会安装 3.x.x 的大版本中最新的小版本。这样,不同的时间执行安装操作就会有不同的结果。所以 lock 这个文件会将本次安装的依赖的版本信息记录下来,在下次再安装的时候,或者其他伙伴使用该包,或者通过 CI 工具的时候,就会安装相同版本的依赖。这样就会避免 package.json 中的内容一致但是实际上安装依赖的版本不一致而造成 Bug 出现的情况。
如下图:
如何生成依赖树
不同的 npm 版本生成依赖树的方式是有区别的,其中主要是 v2、v3 和 v5 的版本之间的区别。
npm 2.X 在 npm 2.X 时期,还是使用的最简单的循环遍历方式,递归下载所有的依赖包,只要有用到的依赖,都进行安装。但是不可避免的造成了,如果依赖包之间存在相同的依赖,就会造成大量的冗余依赖包。
npm 3.X 在 3.X 版本,npm 团队就对生成依赖树的方式进行了优化:将原有的循环遍历的方式改成了更为扁平的层级结构,将依赖进行平铺。在生成依赖树的时候,首先会遍历所有的依赖并将其放入树的第一层节点,然后再继续遍历每一个依赖。当有重复的模块时,如果依赖版本相同,就丢弃不放入依赖树中。如果依赖版本不一致,那么就将其放在该依赖下。但是造成的问题是,生成的依赖树会因为依赖的顺序不同而不同。
npm 5.X 为了解决 3.x 版本安装的不确定性问题,同时还会有一个风险就是在不同时机可能安装依赖的版本不同(比如我们常见到的在版本号前加 ^ 符号,就只会匹配最大版本的依赖包,自动升级小版本),所以在 npm 5.X 版本中新增了 package-lock.json 锁文件。存储的是确定的依赖信息。也就是说,只要 lock 文件相同,那么每次安装依赖生成的 node_module 就会是相同的。同时,在项目中使用 package-lock.json 还可以减少安装时间。因为在 package-lock.json 中已经存放了每个包具体的版本信息和下载链接,这就不需要再去远程仓库进行查询,优先会使用缓存内容从而减少了大量的网络请求。
NPM 缓存机制
作为一个成熟的包管理工具,缓存机制肯定也是需要考虑的一个常见设计。在前面安装流程的第三步中也提到了,在下载之前,会先检查下是否有缓存资源。
执行 npm config get cache 得到的缓存文件的路径如下:
根据文件夹的名字,我们可以大致猜测出: content-v2 文件是用来存在缓存包的具体内容,index-v5 是用来存储依赖包的索引,根据 index-v5 中的索引去 content-v2 中查找具体的源文件 。
我们单独拿 index-v5 目录下的其中的一个文件进行分析00/1d/6ad8a50107d563822b8a9c8cbee221100ab80e2f1333244cb29d1e2c2911
查找一些资料得知,这是使用 SHA256 加密算法,根据 package-lock.json 中记录的包的一些信息,如integrity、version、name 生成一个唯一的 key,前 4 位用来分目录,目的是为了在文件系统中能快速查找。那到底是怎么去计算的呢?其实我们可以打开这个文件,找到答案:
可以看到其中有一个 key 字段 值是:make-fetch-happen:request-cache:{包的url},找一个sha 256 在线的加密网站得到:
我们可以看到加密之后,得到的就是 index-v5 中对应的目录和文件名。而文件中的这些内容就是 content-v2 文件的索引,如果发现有缓存资源,就会去找到 tar 包对应的 hash 值。根据 hash 再去找缓存中的 tar 包,然后再次通过 pacote 将二进制文件解压缩进我们项目的 node_modules 目录中,这样就省去了资源下载的网络开销。
参考文献
segmentfault.com/a/119000001… jishuin.proginn.com/p/763bfbd655cc