likes
comments
collection
share

npm包安装机制历史演变过程

作者站长头像
站长
· 阅读数 24

一、前言

NPM是Node.js的包管理器,作为前端开发人员,对于该工具的使用每个人都了然于心。本文将从npm install命令出发,分享npm包安装机制历史演变的过程以及不同版本之间的差异性。

二、v2版本——嵌套结构

早期的包安装方式简单粗暴,通过层层嵌套的方式,递归遍历安装。这种方式的优点是依赖树的目录结构清晰明了,一眼便看出包之间的依赖关系。缺点也非常明显,就是容易造成嵌套层级过深,造成磁盘占用空间过大。

嵌套层级过深容易理解,磁盘占用过大主要是因为大量重复包的安装,造成node_modules目录过大,删除依赖时,极有可能出现卡死现象。

出于演示目的,笔者创建了四个测试包,用于展示npm不同版本的安装方式。它们分别是test-pack-atest-pack-btest-pack-ctest-pack-d,依赖关系如下:

npm包安装机制历史演变过程

打开终端,通过以下命令创建测试项目。

mkdir npm-test
cd npm-test
npm init -y
npm install npm@2.15.12 --no-save
npx npm install @cjsong2021/test-pack-a
  • -y 可以跳过询问内容,直接创建package.json文件
  • npm@2.15.12版本是在v2版本中随便挑的,不用在意,你也可以挑选自己喜欢的版本,具体查看npm发行版本的信息可以通过npm view npm versions查看。
  • --no-save参数可以只安装包而不在package.json中生成依赖信息。
  • @cjsong2021是作用域名称,这里对应的是笔者的npm账号名cjsong2021,所有演示包都放在此作用域下,目的是避免包重名。
  • npx npm可直接运行node_modules/.bin/npm 可执行文件,对应的就是安装的npm@2.x版本,这样才能查看v2版本生成依赖的结构。

npm包安装机制历史演变过程

通过示例可以清楚的看出包的嵌套结构。

// A { B }, B { C }, C { D }
node_modules 
- test-pack-a 
  - node_modules 
    - test-pack-b 
      - node_modules 
        - test-pack-c 
          - node_modules 
            - test-pack-d

三、v3版本——扁平结构

npm从v3版本开始,由原来的嵌套结构改为扁平结构。就是说不管依赖有多深,通通打平,将模块都安装在同级位置。查看如下示例:

mkdir npm-v3-test
cd npm-v3-test
npm init -y
npm install npm@3.9.6 --no-save
npx npm install @cjsong2021/test-pack-a

npm包安装机制历史演变过程

不同包之间的依赖关系采用了扁平化,那如果遇到相同包不同版本的依赖关系又会是怎么样的呢?

npm包安装机制历史演变过程

npm包安装机制历史演变过程

// A { B, D@1 }, B { C }, C { D@2 }
node_modules 
- test-pack-a  
- test-pack-b
- test-pack-c
  - node_modules
    - test-pack-d@2    
- test-pack-d@1

很显然,相同包不同版本的依赖关系还是通过嵌套方式来解决。当安装A遇到依赖B、D@1时会安装至同级,而安装C依赖遇到D@2时,由于同级已经存在D@1,因此只能将它放置于C之下。

虽然扁平化的方式一定程度上缓解了依赖过深,但同时又引发新的问题。

  • 隐式依赖风险

    扁平化依赖在同级下安装许多我们并未在package.json声明的包,这些多出来的包实际上也可以在项目中导入使用,这就存在一定风险。

    回想一下npm早期使用过程中:明明同一个项目,同一份代码,却经常出现在我这里正常运行,在你那边跑不起来的问题。这基本都是依赖惹的祸,问题有可能出在package.json中并未声明某个包,代码却导入引用了,而它的宿主包很可能被删除,我这边正常是因为引用的包还存在,你重新npm install宿主包不在了,对应的依赖包也不会安装,这样一来,就会抛出一列表的错误信息。

  • 依赖结构变化

    上面举了个实际开发中的例子,隐式依赖是引发问题的其中一点,另一个原因是依赖结构的变化。npm在这个版本里还未出现锁的功能(package-lock.json)。同一包不同版本之间的安装顺序是会出现变化的。这是因为npm install会根据package.json声明包的顺序进行安装,遇到已安装的会对比包的版本范围(semver规范)是否匹配可以共用,无法共用则在该包的node_modules目录下再进行安装。

    举个例子:

    test-pack-atest-pack-b,它们分别依赖不同版本的test-pack-c@1test-pack-c@2

npm包安装机制历史演变过程

在项目开发之初A同学依次安装了test-pack-a和test-pack-b,此时node_modules的依赖结构是:

npm包安装机制历史演变过程 当B同学拉取项目,npm install依赖时,此时的node_modules依赖结构是: npm包安装机制历史演变过程

乍一看,似乎也没什么不妥,但仔细看,如果项目中出现隐式依赖,导入并引用了test-pack-d包,那极有可能因为包版本问题导致出错。这也就是早期项目为什么老是因为依赖问题频频出错的诱因之一。

四、v5版本——锁定依赖结构

为了解决依赖结构变化问题,v5版本新增了锁定依赖功能,也即是package-lock.json文件,它会根据包的安装顺序记录它们的依赖关系,用于保证项目再次安装时依赖结构的一致性。

npm包安装机制历史演变过程

package-lock.json文件

  • nameversion和package.json里的字段一致,描述当前包的名称和版本。

  • lockfileVersion: 锁文件的版本,npm v7版本前是1,v7、v8是2,v9是3。因为不同版本的lockfile文件结构不同,通过该字段区分,用作向下兼容处理。

  • packages:描述包的信息。是一个对象,以键值对的方式来记录,key对应位置信息,value对应详情信息,包括名称、版本、依赖等。

    • resolved:该包所在的远程服务下载地址。
    • integrity:hash值,基于 Subresource Integrity 来验证已安装的软件包资源完整性。
    • bundled:如果为true,则这是绑定的依赖项,将由父级包安装。对应package.json的bundleDependencies字段所声明的依赖。
    • dev: 如果为true,则该依赖关系要么是顶层包的仅开发依赖关系,要么是一个包的传递依赖关系。
    • optional:布尔值,如果为true,则此依赖关系要么是顶级模块的可选依赖,要么是一个模块的传递依赖。对应package.json的devDependencies字段所声明的依赖。
    • dependencies:一个对象,记录该包的子依赖信息。
  • dependencies:与packages字段基本一致,lockfileVersion 3版本已经去除该字段。

使用建议

在项目开发阶段,建议把package-lock.json 文件提交到git仓库中,从而保证团队成员在开发过程中安装依赖包的一致性。

而在开发一个npm包时,就不建议将package-lock.json文件上传。原因是一旦依赖包锁定了版本,使用该包就无法和其它依赖共用同一 semver 范围内的依赖,造成冗余安装。不过npm发布时默认是不上传package-lock.json文件的。

五、总结

npm包安装机制经历三个阶段:

  • 1、嵌套结构:依赖包层层嵌套,造成目录过深,冗余安装。
  • 2、扁平结构:依赖包打平安装,不同版本仍然嵌套,目录不清晰,出现隐式依赖、结构变化问题。
  • 3、锁定结构:package-lock.json记录包版本信息以及安装顺序,用于保证依赖结构一致性。
转载自:https://juejin.cn/post/7249163421694083129
评论
请登录