6

简单探究npm相关机制

 3 years ago
source link: https://segmentfault.com/a/1190000040835104
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

从这篇文章中能获得什么?

  1. npm的安装机制,即在执行install命令时都做了什么事?
  2. npm的缓存策略,这里做了简单介绍,没深入。
  3. npm的依赖管理,即我们每次安装的依赖都是怎么在node_modules进行管理的?
  4. lock文件究竟有什么用?我们到底需要吗?

如果大家对上面三点都很清楚,那就不用浪费太多时间,简单回顾一下就好。
如果不清楚的话可以沉下心看8分钟。

npm依赖安装机制

首先我们来了解一下npm依赖安装的设计哲学:

npm安装依赖的时候会优先安装在当前项目的目录下,保持各个项目依赖的独立性。

优点: 可以减少开发者的心智负担,方便维护各个项目下的不同依赖。

缺点:A项目和B项目都依赖一个包,这时候就会产生依赖重复安装,造成电脑内存资源的浪费。

当然像webpackcreat-react-app这些依赖也可以安装在全局,这样做的目的是为了方便设置环境变量path,让我们可以在任何地方调用对应的命令

了解了这些以后我们来探究一下npm依赖安装的核心机制。


通过上图我们可以看到npm安装依赖的整个脉络,接下来我们逐个分析

当我们执行npm install命令时,会首先去查找对应的配置文件,来告诉npm要以什么样的规则去下载文件。
config文件有多个,但是遵循一个优先级:
项目级的.npmrc > 用户级的.npmrc > 全局的.npmrc > npm内置的.npmrc文件

我们可以通过npm config ls -l来查看当前的配置

确定了config之后,就会看当前项目下有没有package-lock.json文件。

如果有package-lock.json文件,那么会对比pacakge.jsonlock文件:

  1. 如果依赖版本规范兼容的话,会按照lock文件中的信息进行资源的下载
  2. 如果依赖版本规范不兼容的话,不同版本会有不同的处理(详情看上图)

    其实大部分情况下两个版本规范都是兼容的,所以大部分都是按照lock文件去安装依赖。
    除非你手动更改依赖版本号,可能就会出现不兼容的情况。
    总结来说就是:
    规范兼容按照**lock**下载依赖。
    不兼容按照**package.json**下载依赖,并且最后更新lock文件。

这里有个问题就是怎么来判断两个依赖的版本是否兼容?
这里需要知道一个知识点,什么是semver? 官方文档

如果没有package-lock.json文件,那么就会按照package.json文件去进行依赖树的构建。

构建依赖树的时候,遵循扁平化的原则,也就是会优先将依赖放在node_modules的根目录下。后面在安装其他依赖的时候,会判断该依赖是否已经存在,如果存在的话,就会比较两个依赖的版本规范是否兼容,如果兼容则跳过该依赖的安装(忽略该依赖,因为已经存在),如果不兼容的话,会将该版本的依赖单独放在其父级的node_modules

然后在获取依赖资源的时候会优先考虑缓存,这样可以加快依赖的下载速度。

npm依赖缓存策略

我们可以通过下面的命令来获取到缓存地址,我们可以在对应的__caches文件夹里查看相关文件。

npm config get cache

那在我们每次install的时候都是怎么进行缓存的读取的呢?

在我们install的时候,npm会先将资源下载到缓存当中,然后才会解压的项目对应的node_modules中。

之后在每次安装资源的时候都会按照package-lock.json文件中的versionintegrityname信息来生成一个key值,这个key值可以命中index-v5中的缓存资源的信息。如果命中缓存了,就会找到对应的tar包,解压到项目中的node_modules中。

这里其实可以探究一下这个key值是怎么生成的?
留一个思考:就是如果没有lock文件,是不是就意味着不会从缓存里拿资源了?而是每次都通过网络下载?

npm依赖管理机制

首先我们来看一下v2版本的npm依赖管理是怎么做的。

当一个项目中有AB两个依赖的时候,会在node_modules中依次安装这两个依赖。
并且这两个依赖都有一个共同的依赖项C,那么在v2中就会在AB文件夹的node_modules中各自安装C

npm-install.png

这样就会造成一个问题,那就是依赖地狱。

  1. 依赖地狱会造成内存资源的浪费
  2. 而且重复安装依赖,会造成安装进度缓慢

为了解决这样的问题,npmv3开始,就采取了扁平化的结构(其实当前npm很多机制都是借鉴了yarn的设计)。

在扁平化的结构下,npm是怎么进行管理依赖的呢?我们接下来一步步屡清楚。
首先依赖A 有一个依赖项C 版本号为v1,那么在安装依赖的时候就是这样的:
image.png

然后在安装依赖B的时候,他也依赖v1版本的C,那么他就会从node_modules根目录中寻找该依赖,此时的依赖结构如下:

image.png

那么如果依赖B,它依赖的C版本是V2,那么结构就会发生变化,B会在自己目录下的node_modules安装对应v2版本的C:

image.png

根据上面的这种情况,继续往下看如果此时我们需要升级依赖Av2,该版本的A,不再依赖v1版本的C,而是依赖V2版本的C。那么此时npm会怎么做呢?

  1. 删除A
  2. 因为v1版本的C已经没有包依赖了,所以也会删除
  3. 安装v2版本的A
  4. 因为此时根目录下没有v2Cv2版本的C会安装在根目录下

image.png

到这里可能有个问题,为啥都依赖v2版本的C,依赖B还要在自己的目录下单独安装v2C

因为B依赖是安装的,安装B的时候已经在根目录里存在了v1版本的C的包,所以v2版本就安装在了它自身的目录。

从这里就可以看出,npm里依赖的安装顺序对依赖结构的影响会非常大,可能会影响node_modules的文件大小。

那么我们有办法让它完全扁平化吗?

当然是可以的,npm提供了一个命令,可以帮助我们处理这些重复文件,也可以理解为帮我们扁平化

npm dedupe

执行之后结构就变成了更优雅的形式:

image.png

这里值得一提的是,yarn在安装依赖的时候会自动执行dedupe命令,帮我们自动拍平。

这里给大家看一个实际案例,我初始化了一个新的项目,这个项目只安装了一个依赖find-css-import(这个包是我写的,所以比较熟悉),这个包是只有一个依赖strip-comments v2.0

当我们首次安装这个依赖的时候,项目中的node_modules文件夹就会一次出现这两个文件。

QQ20211019-170429-HD.gif

这种现象也是符合上面我们分析的结果的。

然后我们手动的去改一下这个目录结构,把strip-comments文件夹放到find-css-importnode_modules中去,然后在执行一下dedupe来模拟一下扁平化的这个过程。

手动修改后的的目录结构是这样的:
image.png

然后我们去执行一下dedupe

QQ20211019-171124.gif

可以看到它成功的将文件结构给拍平了~

这篇文章其实是一篇笔记,对自己这两天看到的知识进行一个归纳总结。

其实说起来想包管理工具虽然日常我们经常使用,但是有很多细节都是忽略的,也是希望通过这篇文章来捡起来一些东西。

文章始发与公众号:前端程序喵,欢迎大家关注

image.png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK