前言
作为一名前端开发者,每天都会接触 xxx install,包管理器是必不可少的工具。让我们先来回顾一下前端包管理的发展历程
- 2010年之前:前端依赖项需要开发着手动下载再保存到存储库中
- 2010年1月: npm第一个版本正式发布;
- 2015年5月: npm发布v3.x 版本;
- 2016年1月: pnpm 提交诞生并发布第一个版本;
- 2016年: yarn发布第一个v0.x 正式版本;
- 2016年: npm 发布v4.x 版本;
- 2017年: npm 发布v5.x 版本;
- 2018年: npm 发布v6.x 版本;
- 2020年: yarn 发布正式版本 2;
- 2020年: npm 发布v7.x 版本;
- 2021年: yarn 发布正式版本 3;
- 2021年: pnpm发布6.0 版本;
1.npm是什么
1、npm是javaScript世界的包管理工具。
2、NodeJs直接内置了npm,随同NodeJs一起安装的包管理工具。通过npm可以安装、共享、分发代码、管理项目依赖关系。
2.npm的由来
1、在GitHub还没有兴起的时候,人们通过网址来共享代码,比如当你想使用JQ的时候,你可以去JQ官网下载链接使用JQ。当GitHub兴起之后,社区中也会有人使用GitHub的下载功能
2、当项目依赖的代码越来越多,你会发现一件很繁琐的事情
-
去JQ官网下JQ
-
去BootStorap官网下BootStarp
-
...
3、npm 的出世
当有困难发生时,总会有一位先行者出现 —— Isaac Z. Schlueter(npm创始人),其给出了一个解决方案:用一个工具把这些东西集中到一起来管理,这个工具就是npm,全称 Node Package Manager
4、npm的思路:
-
建立一个代码仓库,里面存放了所有需要被共享的代码
-
通知JQ,BootStarp等的作者,让其把代码提交到仓库中,然后分别给他们取个名字,例:jQuery,BootStarp等
-
当有人想使用这些代码时,就可以使用npm来下载代码了
-
这些被使用的代码就叫做包[package],也是npm的名字由来
3.npm的应用
1、当 Isaac Z. Schlueter 通知其他作者加入到 npm 时,作者们会答应吗? —— 这个就不一定了,但当社区里的人都使用 npm 的时候,作者们才会开始考虑加入到 npm
2、npm 的逆袭
-
这里就不得不提到 node.js 了,作者是 Ryan Dahl
-
npm 的发展和 node.js 的发展相辅相成 , node.js 缺少一个包管理工具,于是他们一拍即合,最终node.js内置了npm
-
后来 node.js 火了,随着 node.js 的火爆,大家开始使用 npm 来共享 js 代码,于是JQ等的作者们也将自己的东西发布到了npm上,所以现在大家可以使用 npm install xxx 来下载 相对应的 xxx 代码了
1.删除node_modules后,执行npm i,packe.json中的依赖安装顺序?package-lock.json中显示顺序和什么相关?
2.依赖A和依赖B同时依赖c,但对c的版本要求不同,此时如何安装?
3.npm缓存机制
4.node_modules下@开头的包,以及文件排序
一、npm版本差异分析
npm
npm 全称,Node Package Manager node包管理工具
执行npm install 之后。npm 帮我们下载对应的依赖包并解压到本地缓存,然后构造node_modules目录结构,写入依赖文件,对应的node_modules内部结构也经历了几个版本的变化。
npm v1/v2 嵌套依赖
刚开始 npm 的设计非常的简单:有一个标准的包管理器供大家下载和查阅,将开发人员从那个“网上下载资源,在手动解压添加近项目”的年代中解放出来。所以初版 npm 使用了很简单嵌套结构来进行版本管理。
1.如何理解嵌套依赖
Application 依赖 packageA 和 packageC,而 packageA 和 packageC 都依赖了 packageB,则 node_modules 目录结构如下图
如上图所示,对于 Application 的依赖,node_modules 的目录结构是层层嵌套的。这样的设计其实很符合直觉,依赖包的安装和目录结构都十分清晰且可预测,但是却带来了两个比较严重问题:
1.依赖包重复安装
这个问题在上图中就可以很明显的看出来,B 包(v1版本)被 A 依赖,同时也被 C 所依赖,因此 B 包就在 A 和 C 之下分别被安装了一次,其实是两个完全相同的东西。这都是没有考虑包版本问题存在的情况下,依赖包都会被重复安装。此种设计结构直接导致了 node_modules 体积过度膨胀,这也是臭名昭著的 node_modules hell 问题。
2.嵌套层级太深形成嵌套地狱
同样如上图所示,应用依赖 A 和 C,而 A 的 node_modules 中又安装了 B,如果 B 也依赖其他包,那么 B 又会存在一个 node_modules 中来存放其他依赖包,如此层层递进。
而 Windows 以及一些应用工具无法处理超过 260 个字符的文件和文件夹路径,嵌套层级过深则会导致相应包的路径名很容易就超出了能处理的范围 ,因此会导致一系列问题。比如在想删除相应的依赖包时,系统就无法处理了。(参见:Node's nested node_modules approach is basically incompatible with Windows)
总结:
好处:依赖结构层次清晰
不足:
1.重复包越下载越多,造成了空间浪费,导致前端本地项目node_modules 动辄上百M
2.因为安装的依赖重复,会造成在安装依赖时,安装时间过长
3.因为.嵌套层级过深,导致文件路径过长,会在windows
系统下删除node_modules
文件,出现删除不掉的情况
npm v3 扁平化
为了将嵌套的依赖尽量打平,避免过深的依赖树和包冗余,npm v3.x. 首先遍历所有的项目依赖关系,然后再决定如何生成扁平的node_modules目录结构。npm必须为所有使用到的模块构建一个完整的依赖关系树,这是一个耗时的操作,是npm安装速度慢很重要的一个原因。npm v3 将子依赖「提升」(hoist),采用扁平的 node_modules 结构,子依赖会尽量平铺安装在主依赖项所在的目录中。但是项目的依赖们,真的平等了吗?
同样拿之前的依赖结构为例,npm v2 和 npm v3 的安装目录就完全不一致了。
hoist 机制: npm v3 在处理 A 的依赖 B 时,会将其提升到顶级依赖,即A 的依赖项B被提升到了顶层,如果后续有安装包也依赖B,会去上一级的node_modules查找,如果有相同版本的包,则不会再去重复下载,直接从上一层拿到需要的依赖包B
说明:为什么自己的node_modules没有B,也能在上层访问到B呢?
require 寻找第三方包,会每层级依次去寻找node_modules,所以即便本层级没有node_moudles,上层有,也能找到扁平化方式解决了相同包重复安装的问题,也一定程度上解决了依赖层级太深的问题。
Application -> A_v1->B_v1
-> c_v1->B_v2
Application 依赖 packageA 和 packageC,而 packageA 依赖了 packageB_v1,而packageC依赖了 packageB_v2, 则 npm_v3扁平化的后目录结构为:
如上图所示,在依赖分析过程中,检查到 A v1 依赖了 B v1,因此将 B v1 提升到了顶层。再检查到 C v1 依赖了 B v2 时,发现顶层已经存在了 B v1,因此 B v2 无法提升到顶层,那么只能接着放在 C v1 之下,如果后续又有D v1依赖B v2,由于B v2没有提升,也只能接着放在 D v1 之下,可以看出,如果出现了同一依赖的不同版本的话,实际也无法做到完全的扁平化。
为什莫提升的是B v1而不是B v2 , 提升哪个版本的依据是什么?
执行npm install,此时的下载顺序是由package.json中的字母顺序(a,b,c,d...)决定的,第一个下载的就是A_v1,随即把B_v1也到了顶层。
提升的依据:根据先来先服务原则,先安装的先提升,不是根据使用量优先提升,这会导致不确定性问题,随着项目迭代,npm i 之后得到的node_modules目录结构,有可能不一样。
新的问题
通过扁平化设计 node_modules 来尽量规避同一版本依赖包重复安装的问题和减少层级嵌套过深的问题。但是这个设计也不是十全十美的的,在解决旧有问题的同时也产生了新问题。
- 幽灵依赖:我们把C,提升到了顶层,即使项目package.json,没有声明过C,但是也可以在项目中引用到C,这就是幽灵依赖问题。
- 双生不确定性: 根目录的依赖包版本会随着依赖的顺序不同而不同;
- 依赖分身 Doppelgangers:B v2 又被安装了一份在 D v1 下面。C v1 和 D v1 的依赖都是 B v2 版本,不存在任何差别,但是却依然被重复安装了两遍
Application -> A_v1->B_v1
-> c_v1->B_v2
-> d_v1->B_v2
作为前端工程师你真的知道 npm install 原理么? - 知乎
npm V5 lock
npm v5 借鉴yarn的思想,新增了 package-lock.json。
该文件里面记录了package.json依赖的模块,以及模块的子依赖。并且给每个依赖标明了版本、获取地址和验证模块完整性哈希值。
通过package-lock.json,保障了依赖包安装的确定性与兼容性,使得每次安装都会出现相同的结果。
这个就解决了不确定性的问题
在本地存在 package-lock.json 文件的情况下,npm 就不需要再去请求查看依赖包的具体信息和满足要求的版本,而是直接通过 lock 文件中内容先去查找文件缓存。若发现没有缓存则直接下载并进行完整性校验,如若无误,则安装
1.package-lock.json文件字段说明
- name:项目的名称;
- version:项目的版本;
- lockfileVersion:lock文件的版本;
- requires:使用requires来跟踪模块的依赖关系;
- dependencies:项目的依赖
-
- version表示实际安装的版本;
- resolved用来记录下载的地址,registry仓库中的位置;
- requires记录当前模块的依赖;
- integrity用来从缓存中获取索引,再通过索引去获取压缩包文件
npm install 全过程
npm install先检测是有package-lock.json文件:
- 没有package-lock.json文件
- 分析依赖关系,这是因为我们可能包会依赖其他的包,并且多个包之间会产生相同依赖的情况;
- 从registry仓库中下载压缩包(如果我们设置了镜像,那么会从镜像服务器下载压缩包);
- 获取到压缩包后会对压缩包进行缓存(从npm5开始有的, npm config get cache 可以查看地址)
- 将压缩包解压到项目的node_modules文件夹中
- 有package-lock.json文件
- 检测lock中包的版本是否和package.json中是否一致
- 不一致,那么会重新构建依赖关系,直接会走上面的流程;
- 一致的情况下,会去优先查找缓存
- 缓存没有找到,从registry仓库下载,直接走上面流程;
- 命中缓存会获取缓存中的压缩文件
- 将压缩文件解压到node_modules文件夹中;
- 检测lock中包的版本是否和package.json中是否一致