likes
comments
collection
share

老生常谈的前端三大包管理工具npm、yarn、pnpm

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

随着前端技术的不断更新和发展,前端工程化工具也在不断的更新和产生。我们的日常开发中一直有用的包管理工具有npmyarnpnpm。这三种先后顺序诞生,但是我们在使用的过程中,有没有真正的了解过他们的原理以及之间的差异?为什么会最后会产生pnpm呢?那本文就带着疑问一起去学习看看,来解答我们的疑问🤔️吧~

npm

npm众所周知了,是随着node官方一起出现的一个包管理工具。在平时的开发中,不免会使用到npm install命令来安装各种npm 包。在我们环境中,安装了node就已经自动安装了npm 命令工具,不再需要单独安装。

三大模块

npm的功能很强大,主要分为三大部分吗

  • 网站:这个就是我们将npm包发布到的网站,网站上可以查找各种各样能够满足你需求的包,便于使用。
  • 注册表(registry):是一个巨大的数据库,保存了每个package的信息。例如https://registry.npmjs.org/react
  • 命令行工具(CLI):开发者使用的命令行工具npm,进行包的安装,发布等。

npm进化史

嵌套结构依赖

在最早期的时候v1/v2的时候和现在的区别比较大,之前的时候node_modules的目录管理,采用最直接的嵌套式结构。

老生常谈的前端三大包管理工具npm、yarn、pnpm

以上的层级嵌套就是相当于项目依赖了A、B、C包,但是里面都依赖了package D。相当于有重复的依赖安装。这就导致了node_modules的体积变大。并且如果package D包还依赖了其他包,那么会造成层级依赖过深,直到当前package没有依赖包为止。

总结一下早起的npm的问题:

  • 重复依赖安装
  • 层级依赖过深,导致体积变大
  • 缓存能力出现问题,无离线模式
  • 版本管理不稳定

扁平化

针对上面所说的npm前期存在的问题,在npm v3后针对这些问题采取解决方案——— 扁平化。何谓扁平化,其实看图中可以看到,按照安装的顺序,如果遇到相当版本的依赖,那么这个依赖会被提升到顶层,安装到相同模块时,根据node require机制,会逐级往上寻找node_modules,判断已安装的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的node_modules下安装该模块。

但是这种扁平化也存在问题,如果出现下图的package D1.0和packag D2.0,这种情况,package D2.0 因为版本不一致无法提升到顶层,所以还是保留在packageC 下,这样还是会重复打包。

那上面的经历中,衍生了几种现象

  • 幽灵依赖 :指在package.json中引用的依赖除此之外,还有其他的包安装。比如packageD1.0还依赖了package H,那这个package H并没有在package.json中引用。这种情况就是幽灵依赖。幽灵依赖可能导致依赖丢失或者版本兼容差异。

  • 依赖分身: packageD1.0和packageD2.0是同一种依赖,但是不同版本,却需要被重复安装两次,这种情况就叫做依赖分身。有文章也说叫做“双胞胎陌生人”~

    这种情况会产生几种问题:

    • 仍然出现少数重复包依赖安装,增加node_modules的体积;
    • 无法共享库实例,引用的得到的是两个独立的实例;
    • 如果其中的ts声明文件,会导致使用混乱,导致编译器报错;
  • 依赖不幂等: 意味着当你多次执行相同的安装命令时,可能会得到不同的结果,或者会产生不一致的行为。

老生常谈的前端三大包管理工具npm、yarn、pnpm

lock诞生

在npm v5的时候,npm参考yarn的思路,采取了lock锁的思想,将npm安装依赖锁定版本,来解决依赖不幂等的问题。通过lockfile来锁定安装的版本,使得每次执行npm installd的时候,依赖的版本都是相同的~

package-lock.json中包含以下几个字段:

  • version:唯一版本号

  • resolved:包的安装源

  • integrity:用于验证包是否失效的完整性hash值,由两部分组成: 加密hash函数-摘要dgest,加密函数有两种sha512或者sha1,dgest等于base64(hashfn(content))

  • dev:是否为开发时依赖项

  • requires:当前包的dependencies依赖项

  • dependencies:当前包的node_modules依赖树(比如:某个子依赖包存在多版本时,当前包下生成的node_modules结构)

"yargs": {
      "version": "16.2.0",
      "resolved": "https://registry-npm.myscrm.cn/repository/pkg/yargs/-/yargs-16.2.0.tgz",
      "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
      "dev": true,
      "requires": {
        "cliui": "^7.0.2",
        "escalade": "^3.1.1",
        "get-caller-file": "^2.0.5",
        "require-directory": "^2.1.1",
        "string-width": "^4.2.0",
        "y18n": "^5.0.5",
        "yargs-parser": "^20.2.2"
      },
      "dependencies": {
        "is-fullwidth-code-point": {
          "version": "3.0.0",
          "resolved": "https://registry-npm.myscrm.cn/repository/pkg/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
          "dev": true
        },
        "string-width": {
          "version": "4.2.3",
          "resolved": "https://registry-npm.myscrm.cn/repository/pkg/string-width/-/string-width-4.2.3.tgz",
          "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
          "dev": true,
          "requires": {
            "emoji-regex": "^8.0.0",
            "is-fullwidth-code-point": "^3.0.0",
            "strip-ansi": "^6.0.1"
          }
        }
      }
    },

虽然上述解决了依赖不幂等的问题,但是扁平化的处理,并没有解决到幽灵依赖等本质问题。

npm init

在执行npm init命令的时候,会初始化一个package.json文件,文件中会有以下几个部分组成

{
  "name": "npm-test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
  • name: 包的名称
  • version: 包的版本信息
  • description: 关于包的描述信息
  • main: 包的入口文件
  • script:关于包的脚本命令
  • author: 作者
  • license: 开源的声明

当然还可以配置其他的参数,此处不做详细的讲解,可去官网进行查看。

npm install

很多时候我们在拉取仓库代码之后,需要执行的第一个命令就是npm install。那么在执行这个命令之后我们知道会在node_modules下安装依赖包。

老生常谈的前端三大包管理工具npm、yarn、pnpm

  • 首要很重要的会优先去检查npm的config配置。npm的配置.npmrc文件

    • .npmrc配置文件即npm运行配置的文件,在项目文件中使用npm的时候,其实看不到.npmrc文件,那么我们可以通过命令行来查看配置信息。

      npm config list // 查看主要配置信息
      npm config ls -l // 查看所有的配置信息
      
    • 在我们的电脑上其实不止存在一个npm的配置文件,而是有多个。在我们执行npm install 的时候,会按照以下顺序来进行读取配置文件。 项目级别的.npmrc文件 > 用户级别的.npmrc文件 > 全局的.npmrc文件 > npm内置的.npmrc文件

      • 项目配置文件:你可以在项目的根目录下创建一个.npmrc文件,只用于管理这个项目的npm安装。
      • 用户级别的配置文件:登陆这台电脑的时候,为当前用户创建的这个文件,通过npm config get userconfig获取文件的位置。
      • 全局的配置文件: 一台电脑有多个用户,这个其实也叫做公共的配置,可以通过npm config get prefix获取配置信息。
      • npm 内置配置文件 (/path/to/npm/npmrc)
  • 读取package.json的中的配置信息,检查是否存在package-lock.json文件,此文件会非常精准的描述每个安装包的版本信息等。在这一步中,如果存在package-lock.json文件,那么会根据package.json文件声明的依赖和锁文件中的进行对比,如果一致,那么从锁文件中获取版本信息,进行构建依赖。 如果不一致,那么那么就按照上面所说的扁平化进行处理,根据递归一次生成依赖树。

  • 在执行npm install过程中,除了将安装依赖放到node_modlues目录下,还会在本地的缓存目录下缓存一份,可以通过npm config get cache获取缓存目录。再次安装依赖的时候,会根据 package-lock.json 中存储的 integrity、version、name 信息生成一个唯一的 key,然后拿着key去目录中查找对应的缓存记录,如果有缓存 资源,就会找到tar包的hash值,根据 hash 再去找缓存的 tar 包,并把对应的二进制文件解压到相应的项目 node_modules 下面,省去了网络下载资源的开销。

npm run

  • npm run命令的核心功能就是执行package.json中执行的脚本。

  • 在我们的package.json文件中有一个script字段,这个字段中可以自定义脚本,使用npm运行他们。

  • npm run的运行流程

    老生常谈的前端三大包管理工具npm、yarn、pnpm

    • 举例,比如package.json文件中有一个构建命令,执行npm run xxx命令的时候,首先会去配置文件package.json文件中查找对应的script命令

    • 其实在我们安装依赖的时候,会在node_modules的.bin目录中创建好xxx为名的可执行文件(软链接)。打开看看可以看到依赖同名的文件,比如我项目package.json文件中有taro依赖,那么在执行命令中声明了一个script叫做"build:weapp": "taro build --type weapp",在node_modules目下的.bin文件可以看到一个可执行文件。(其实一个同名的依赖,会有三个执行文件,分为index.js执行文件、window的执行文件、ios执行文件。根据我们所处的系统环境去执行对应的脚本) 老生常谈的前端三大包管理工具npm、yarn、pnpm 这个文件里面开头用了#! /usr/bin/env node指定执行脚本的解释器。同时在node_modules下还有一个taro目录,taro下有对应的依赖文件信息,在其中有一个自己依赖包中包含的package.json中,可以看到在bin中声明了

      老生常谈的前端三大包管理工具npm、yarn、pnpm 所以在 npm install 时,npm 读到该配置后,就将该文件软链接到 ./node_modules/.bin 目录下,而 npm 还会自动把node_modules/.bin加入$PATH,这样就可以直接作为命令运行依赖程序和开发依赖程序,不用全局安装了。

    总结来说:就是npm install的时候npm就帮我们把软连接配置好了,相当于一种映射,执行npm run xxx命令的时候,就会到node_modules/.bin文件找到对应的映射文件,然后再找到对应js文件来执行。

yarn

yarn的诞生是在npm处于v3的时期的时候,yarn的包管理器横空诞生。npm在v3时期还没有package-lock.json文件,安装的时候会出现

  • 重复依赖安装

  • 层级依赖过深,导致体积变大

  • 缓存能力出现问题,无离线模式

  • 版本管理不稳定

  • ……

等问题,那么yarn的出现很好的解决了这些问题,后期npm也是参考yarn的lock文件的方案,解决了这些问题。

  • 采用模块扁平化,将不同版本的依赖包,按照一定的策略,归纳版本,避免了重复安装
  • 采用缓存机制,实现离线模式解决缓存能力出现的问题。
  • 生成package-lock锁文件,解决每次install依赖的时候版本的不稳定。即使是不同的安装顺序,相同的依赖关系在任何的环境和容器中,都可以以相同的方式安装
  • 网络性能更好: yarn采用了请求排队的理念,类似于并发池连接,能够更好的利用网络资源;同时也引入了一种安装失败的重试机制。

package-lock

  • npm使用的是json结构
  • yarn采用的一种自定义的标记方式。yarn.lock结尾

老生常谈的前端三大包管理工具npm、yarn、pnpm

yarn install

yarn 安装的流程可以在cmd中清晰的看到具体有几个流程。在我们初始化项目的时候,执行yarn install可以看到如图所示步骤提示,每完成一次步骤,都能够得到结果 老生常谈的前端三大包管理工具npm、yarn、pnpm

  • 检查配置信息:可以在终端看到,会去检查lockfile文件等一些配置信息以及运行环境是否符合运行条件

  • Resolving packages(解析包):这里是进行依赖的整合

    • 首层依赖:收集当前所属项目下的package.json文件中定义的dependenciesdevDependenciesoptionalDependencies中的内容。
    • 遍历所有依赖且收集依赖的具体信息:从首层依赖出发,递归查找每个依赖下嵌套依赖的版本信息,并且将解析过的包的信息通过set数据结构进行存储。如果有lock文件,那么会从lock文件中获取版本信息,并且标记已解析,如果没有则会向registry发起请求获取满足版本范围的包的信息,获取后将其标记为已解析。
  • 拉取包:这一步会去检查混存中是否有当前依赖的包,同时将缓存中不存在的包下载到缓存的目录中。yarn 的缓存目录可通过yarn cache dir查询,缓存条目以{slug}/node_modules/{packageName}命名的目录形式保存。值得一提的是,slug 由版本、哈希值、uid构成,因此 yarn 以同级平铺的形式存放缓存条目。

    老生常谈的前端三大包管理工具npm、yarn、pnpm

    yarn在拉取包的时候对网络请求耶进行了优化

    • 以urlToPromise的形式做了cache,避免了向同一url发送请求,造成不必要的开销
    • 支持最大并行发送 8 个请求
    • 自动重发策略(请求失败尝试重发,单请求重发上限为5次)
  • 连接依赖:将下载的依赖复制到node_modules下,在此期间做了三件事

    • 处理peerDependencies,peerDependencies一般用在要分发的包中,当一个包必须以另一个包作为地基,适合使用,指定两者的关系。比如 react 需要依托 react-dom、koa 插件需要依托 koa。
    • 扁平化依赖树:前面说过npm v2中出现依赖嵌套,那么yarn在此过程中,在扁平化依赖树阶段分析同一依赖包不同版本的使用频率,选择利用率最大的版本放置在顶层,这一过程称为 dedupe。
    • 拷贝依赖到node_modules下。这里分为从resgistry拉取的依赖包和从缓存中的包。
  • 构建安装:在构建阶段执行install相关的生命周期勾子,比如preinstall、postinstall等。同时依赖包中存在二进制包需要进行编译,也会在这一步中进行。

yarn的基础命令

  • yarn init 初始化项目
  • yarn add packageName 安装依赖
    • –-dev安装到devDependencies下,代表开发环境需要的依赖包
  • yarn remove packageName 移除依赖包
  • yarn upgrade packageName 更新目录下指定的依赖包
  • yarn global add packageName 在全局安装依赖包
  • yarn config get key 查看配置key的值
  • yarn config set key 设置配置项key的值
  • yarn list 查询所有依赖
  • yarn info packageName 查看依赖包的信息
  • yarn config list 获取配置信息
  • ……

pnpm

npm/yarn的诞生,虽然说很多问题得到了解决,但是难道这样就没有问题了吗?并不是,依赖扁平化的方案也有相应的问题,比如幽灵依赖,明明没有声明在dependencies中的依赖,也被拉取了下来。而且版本出现很多个的时候,依赖包只会提升一个,那么其他版本的包还是安装了多次,那么仍然会浪费很多空间。

直到pnpm的出现,又打开了一扇大门。pnpm官网映入眼帘的是这句话:快速的,节省磁盘空间的包管理工具

老生常谈的前端三大包管理工具npm、yarn、pnpm

上图可以看到官网列举了pnpm的4个标志性特点:

  • 快速: pnpm比npm的速度要快两倍
  • 高效:node_modules中的文件为复制或链接自特定的内容寻址存储库
  • 支持monorepos:pnpm内置支持单仓多包
  • 严格:pnpm默认创建了一个非平铺的node_modules,因为代码无法访问任意包

接下来会针对上面的几个特点进行解说

安装

执行npm install -g pnpm进行全局安装

基本命令

简单过一下pnpm的基础常用命令

  • pnpm add 安装包
    • --save-dev, -D 安装为 devDependencies,也就是开发环境使用的包
    • --save-peer 安装为 peerDependencies ,同时安装到 devDependencies,
    • peerDependencies 通常是和该包一起使用的其他包,需要使用者同时下载
    • --global, -g 安装为全局依赖
  • pnpm install
    • --offline 仅从 store 中离线下载
    • --dev, -D 只下载 devDependencies 依赖
    • --frozen-lockfile 不会生成 lockfile
    • --shamefully-hoist 创建扁平结构,类似于 npm ,不推荐
  • pnpm update 更新包
  • pnpm remove移除依赖包
  • pnpm link 使当前本地包可在系统范围内或其他位置访问。
  • pnpm unlink 如果不带参数调用,则当前项目内的所有链接的依赖项会被取消链接。
  • pnpm import
  • pnpm rebuild重建一个package
  • pnpm prune移除不需要的packages
  • pnpm fetch将 lockfile 中列出包下载到虚拟存储中,包清单被忽略
  • pnpm install-test 和pnpm install差不多
  • pnpm dedupe 执行安装时,如果有新版本可用,则会从锁定文件中移除旧依赖项。
  • ……

快速

pnpm分为三个阶段

  • 依赖解析resovling: 首先他们会解析依赖树,决定要下载哪些安装包,仓库中没有的依赖都被识别并获取到仓库
  • 目录结构计算fetching:下载依赖的tar包。这个阶段可以同时下载多个,来增加速度。
  • 链接依赖项linking: 解压包,根据文件构建出真正的依赖树,这个阶段需要大量文件IO操作。

pnpm 使用符号链接技术和并行安装,因此能够显著提高依赖包的安装速度,尤其对于大型项目而言,这意味着能够大幅缩短依赖安装的时间成本。

这里类似我们桌面快捷键,其本质就是一个软链接,软链接所产生的文件是无法更改的,它只是存储了目标文件的路径,并根据该路径去访问对应的文件。

高效

node_modules中的文件为复制或链接自特定的内容寻址存储库

  • 节省磁盘空间,使用npm时,依赖每次被不同的项目使用,都会重新安装一次。在pnpm时,依赖会被存储在内容可寻址的存储中。如果有多个不同的版本的依赖包,如果发现了其中一个版本更新有修改,那么pnpm update只需要添加这个修改的文件到存储中。
  • 官方原话:所有文件都会存储在硬盘上的某一位置。 当软件包被被安装时,包里的文件会硬链接到这一位置,而不会占用额外的磁盘空间。 这允许你跨项目地共享同一版本的依赖。

支持monorepos

它要求在代码仓的根目录下存有 pnpm-workspace.yaml 文件指定哪些目录作为独立的工作空间,这个工作空间可以理解为一个子模块或者 npm 包。

packages: 
  - "packages/*"

在我们搭建组件库的时候,或者有用到monorepos的时候,声明一个workspace文件工作空间,那么会将你声明的位置作为工作空间,

通过pnpm --filter basic-components i -D @babel/preset-env命令可以将依赖安装到固定的工作区间下,当然前提是你的子包也需要声明package.json,并且命名name为basic-components

非扁平的结构

yarn和npm通过扁平化处理,会将依赖包提升到根结构,那么这个时候,源码可以直接访问和修改依赖,而不是作为只读的项目依赖。那么在pnpm 使用符号链接将项目的直接依赖项添加到模块目录的根目录中。

老生常谈的前端三大包管理工具npm、yarn、pnpm

node_modules.pnpm下每个包的每个文件都是来自内容可寻址存储的硬链接。

老生常谈的前端三大包管理工具npm、yarn、pnpm

总结

文章中说明了npm、yarn以及pnpm的一些演变以及原理和使用,那么再来总结加深一下印象。

资料贡献:

转载自:https://juejin.cn/post/7379784414459904051
评论
请登录