npm yarn pnpm 的详细对比&安装依赖的相关知识点

1,711 阅读13分钟

一、 解决问题

  • npm安装依赖为什么会丢包?yarn和pnpm为什么比npm快?
  • 为什么不建议混用包管理工具?
  • package.json版本号的规则,~,^的含义
  • .lock文件的作用
  • 何为幽灵依赖?什么情况下会出现幽灵依赖?
  • 安装依赖的底层原理和机制
  • 安装依赖时常见问题
  • 淘宝源和npmrc,以及修改host解决的问题
  • 常用命令

二、前言

每种主流编程语言都有包管理工具,比如 java 的 Maven、Gradle,Python 的 pip。而主流的前端包管理工具有 npm、yarn、pnpm,这些包管理器都是基于 nodejs。

三、发展史

2010:npm 发布,支持 Node.js。

2016:yarn 发布,生成 yarn.lock 文件用于确定 repos 的精确版本,并且比 npm 性能更好。
2017:npm 5 发布,提供类似 yarn.lock 的 package-lock.json 文件。
2017:pnpm 发布,pnpm 具有 yarn 相对于 npm 的所有附加功能,并解决了 yarn 没有解决的磁盘空间问题。
2018:npm 6 发布,在 npm 在安装依赖项之前检查安全漏洞,提高了安全性。
2020:yarn 2 和 npm 7 发布,这两个软件包都具有出色的新功能。
2021:yarn 3 发布并进行了各种改进

时间线 image.png

参考:JavaScript 包管理器简史(npm/yarn/pnpm)

四、npm yarn pnpm 的详细对比

npm1、npm2

node_modules是嵌套的

node_modules
- package-A
-- node_modules
--- package-B
----- node_modules
------ package-C
-------- some-really-really-really-long-file-name-in-package-c.js

这种方式的优点是模块依赖关系清晰。 缺点也比较明显:

  • 依赖层级太深,会导致文件路径过长的问题,尤其在 window 系统下,最多260多个字符。
  • 大量重复的包被安装,文件体积超级大。比如跟 foo 同级目录下有一个baz,两者都依赖于同一个版本的lodash,那么 lodash 会分别在两者的 node_modules 中被安装,也就是重复安装。
  • 模块实例不能共享。比如 React 有一些内部变量,在两个不同包引入的 React 不是同一个模块实例,因此无法共享内部变量,导致一些不可预知的 bug。

当时npm还没解决这些问题,社区就出来一个新的解决方案了,那就是 yarn。

yarn

node_modules平铺到最外面一层

node_modules
- package-A
- package-B
- package-C
-- some-file-name-in-package-c.js

yarn解决了上述嵌套方案的缺陷。这样,一个原来很长的文件路径名就从 ./node_modules/package-A/node_modules/package-B/node-modules/some-file-name-in-package-c.js变成了./node_modules/some-file-name-in-package-c.js

后面npm3 + 也采用类类似方案实现

所有的依赖都被拍平后,不再有很深层次的嵌套关系。在安装时,根据 node require 机制,会不停往上级的node_modules当中去找,如果找到相同版本的包就不会重新安装,解决了大量包重复安装的问题,而且依赖层级也不会太深。

然而扁平化的处理方式有哪些问题呢?

  • 依赖结构的不确定性。
  • 扁平化算法本身的复杂性很高,耗时较长。
  • 幽灵依赖,项目中可以直接使用依赖的包的包。

npm和yarn的比较

  1. 并行安装:yarn安装包会同时执行多个任务,npm 需等待上一个任务安装完成才能运行下一个任务(按照在package.json中声明的顺序),所以npm install 下载速度慢,即使是重新 install 时速度依旧慢
  2. 离线模式:如果你已经安装过一个包,用 yarn 再次安装会从缓存中获取,而 npm 会从网络下载
  3. 版本锁定:yarn 默认有一个 yarn.lock 文件锁定版本,保证环境统一,npm是通过package-lock.json
  4. 更简洁的输出:yarn 安装包时输出的信息较少,npm 输出信息冗余

npm和yarn同时混用会有什么问题

前置知识

各依赖的版本格式

npm 包采用语义化格式, a.b.c 语义:

  1. a 代表主版本号,做了不兼容的 API 修改
  2. b 次版本号,做了向下兼容的功能性新增
  3. c 修订号,做了向下兼容的问题修正

package.json中^,~的详细说明

  • 指定版本:比如"axios": “0.21.0”,表示安装0.21.0的版本;

  • ~符号代表,它会自动更新到中间那个数字的最新版本,比如:“~2.2.0”,库就会更新到2.2.X的最新版本,但是不会更新到2.3.X版本

  • ^指定版本:比如 “antd”: “^3.1.4”,,表示安装3.1.4及以上的版本,但是不安装4.0.0,也就是说安装时不改变大版本号。

.lock.json的作用

package-lock.json

首次安装npm各库包的时候会自动生成 package-lock.json。是用来记录当前状态下实际安装的各个npm package的具体来源和版本号。目的确保你项目中的依赖不会在你不知不觉中自动升级。

yarn.lock

yarn.lock作用和package-lock.json作用相同,但yarn.lock是yarn安装依赖时生成的。

pnpm-lock.yaml 同上

注: 在使用npm进行依赖安装时yarn.lock文件不生效,在使用yarn进行依赖安装时package-lock.json文件不生效

在同一个项目混用yarn和npm时会出现的问题

这里以 typescript 依赖包为例

  1. 原项目是用npm来进行包管理,从而生成package-lock.json文件,里面存储了各个依赖的具体来源和版本号,其中typescript的版本号为4.2.4,所以今后使用npm进行安装依赖时都会安装typescript的4.2.4版本,不会进行自动升级

  2. 如开发者使用yarn命令来进行包依赖安装,则package-lock.json文件无效,只看package.json中的文件,但typescript版本号为^4.2.4,从而会安装4.x.x版本中最新版本即为4.6.3版本,同时生成对应yarn.lock文件

  3. 启动项目会出现typescript类型报错,其原因是因为原项目是在4.2.4typescript版本环境下编写,但使用yarn进行依赖安装把typescript版本自动更新成了 4.6.3版本,同时4.2.4和4.6.3版本的typescript在类型校验上进行了较大的改进和优化

如何选择正确的包管理工具以及安装依赖

  1. 将项目clone到本地后,观察其根目录决定使用什么包管理工具
  • 如有yarn.lock文件则项目是以yarn 来进行包管理
  • 如有package-lock.json文件则项目是以npm来进行包管理
  • 如有pnpm-lock.yaml文件则项目是以pnpm来进行包管理

Phantom dependencies幽灵依赖

Phantom dependencies 被称之为幽灵依赖或幻影依赖),解释起来很简单,即某个包没有在package.json 被依赖,但是用户却能够引用到这个包。

比如yarn打包: A依赖B, B依赖C,那么 A 就算没有声明 C 的依赖,由于有依赖提升的存在,C 被装到了 A 的node_modules里面,在A里面引入C,没什么问题的。 但是在以下几种情况下还是会有问题:

第一, B 的版本是可能随时变化的,假如之前依赖的是C@1.0.1,现在发了新版,新版本的 B 依赖 C@2.0.1,那么在项目 A 当中 npm/yarn install 之后,装上的是 2.0.1 版本的 C,而 A 当中用的还是 C 当中旧版的 API,可能就直接报错了。

第二,如果 B 更新之后,可能不需要 C 了,那么安装依赖的时候,C 都不会装到node_modules里面,A 当中引用 C 的代码直接报错。

第三,在 monorepo 项目中,如果 子项目A 依赖 X,子项目B 依赖 X,还有一个 子项目C,它不依赖 X,但它代码里面用到了 X。由于依赖提升的存在,npm/yarn 会把 X 放到根目录的 node_modules 中,这样 C 在本地是能够跑起来的,因为根据 node 的包加载机制,它能够加载到 monorepo 项目根目录下的 node_modules 中的 X。但试想一下,一旦 C 单独发包出去,用户单独安装 C,那么就找不到 X 了,执行到引用 X 的代码时就直接报错了。

pnpm

通过pnpm安装依赖后node_modules目录结构如下,它的优势是速度很快、节约空间,创建非扁平的 node_modules 目录

image.png

.pnpm
├── lock.yaml
├── node_modules
│   ├── .bin
│   ├── accepts
│   ├── array-flatten
│   ├── body-parser
│   ├── bytes
│   ├── call-bind
│
│  
├── registry.npmmirror.com+accepts@1.3.8
│   └── node_modules
├── registry.npmmirror.com+array-flatten@1.1.1
│   └── node_modules
├── registry.npmmirror.com+body-parser@1.20.1
│   └── node_modules
├── registry.npmmirror.com+bytes@3.1.2
│   └── node_modules
├── registry.npmmirror.com+call-bind@1.0.2
│   └── node_modules

.pnmp为虚拟存储目录,该目录通过<package-name>@<version>来实现相同模块不同版本之间隔离和复用,由于它只会根据项目中的依赖生成,并不存在提升,所以它不会存在之前提到的 幽灵依赖 问题

pnpm如何跟文件资源进行关联的呢?又如何被项目中使用呢?

前置知识:

inode:每一个文件都有一个唯一的 inode,它包含文件的元信息,在访问文件时,对应的元信息会被 copy 到内存去实现文件的访问。

hard link:硬链接可以理解为是一个相互的指针,创建的 hardlink 指向源文件的 inode,系统并不为它重新分配 inode。硬链接不管有多少个,都指向的是同一个 inode 节点,这意味着当你修改源文件或者链接文件的时候,都会做同步的修改。每新建一个 hardlink 会把节点连接数增加,只要节点的链接数非零,文件就一直存在,不管你删除的是源文件还是 hradlink。只要有一个存在,文件就存在。(同一个文件的不同引用)

soft link:软链接可以理解为是一个单向指针,是一个独立的文件且拥有独立的 inode,永远指向源文件,这就类比于 Windows 系统的快捷方式。删除源文件,软链接就会失效。

Store

pnpm在全局通过Store来存储所有的 node_modules 依赖,并且在 .pnpm 中存储项目的hard links

在使用 pnpm 对项目安装依赖的时候,如果某个依赖在 sotre 目录中存在了话,那么就会直接从 store 目录里面去 hard-link,避免了二次安装带来的时间消耗,如果依赖在 store 目录里面不存在的话,就会去下载一次。

假如全局的包变得非常大怎么办?使用 pnpm store prune 便可以删除一些不被全局项目所引用到的 packages

Links(hard link & symbolic link)

如果是 npm 或 yarn,那么这个依赖在多个项目中使用,在每次安装的时候都会被重新下载一次。

image.png

pnpm 之所以能做到如此大的性能提升,一部分原因是使用了计算机当中的 Hard link ,它减少了文件下载的数量,从而提升了下载和响应速度

hard link

  • 硬链接不会新建 inode(索引节点),源文件与硬链接指向同一个索引节点
  • 硬链接不支持目录,只支持文件级别,也不支持跨分区
  • 删除源文件和所有硬链接之后,文件才真正被删除

symbolic link

  • 符号链接中存储的是源文件的路径,指向源文件,类似于 Windows 的快捷方式
  • 符号链接支持目录与文件,它与源文件是不同的文件,inode 值不一样,文件类型也不同,因此符号链接可以跨分区访问
  • 删除源文件后,符号链接依然存在,但是无法通过它访问到源文件

当foo和bar同时依赖于lodash的时候,就会像下图这样的结构。

.
└── node_modules
    ├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
    └── .pnpm
        ├── foo@1.0.0
        │   └── node_modules
        │       ├── foo -> <store>/foo
        │       ├── bar -> ../../bar@1.0.0/node_modules/bar
        │       └── lodash -> ../../lodash@1.0.0/node_modules/lodash
        ├── bar@1.0.0
        │   └── node_modules
        │       ├── bar -> <store>/bar
        │       └── lodash -> ../../lodash@1.0.0/node_modules/lodash
        └── lodash@1.0.0
            └── node_modules
                └── lodash -> <store>/lodash

Monorepo

monorepo 是管理多个项目的 git 仓库。

通过 workspace 实现对 Monorepo 的支持。npm7+、yarn 都支持 monorepo。重点关注 pnpm 对 monorepo 的支持。

pnpm 一开始就支持 workspace,只需在项目根目录创建 pnpm-workspace.yaml 文件。

packages:
  - 'packages/*'

关于 pnpm的优势不止是安装速度快,节省磁盘,现代计算机貌似都不在乎这些,重要的是天然的支持 monorepo,且用法及其简单。

五、性能对比

actioncachelockfilenode_modulesnpmpnpmYarnYarn PnP
install51s14.4s39.1s29.1s
install5.4s1.3s707msn/a
install10.9s3.9s11s1.8s
install33.4s6.5s26.5s17.2s
install28.3s11.8s23.3s14.2s
install4.6s1.7s22.1sn/a
install6.5s1.3s713msn/a
install6.1s5.4s41.1sn/a
updaten/an/an/a5.1s10.7s35.4s28.3s

image.png

以上测试结果可以看出,首次执行 npm install 安装依赖时 pnpm 比 npm 和 yarn 大约快 3 倍左右,在有缓存和已安装过依赖的情况,比 npm 也快了不少,yarn 则是更快,其他场景 pnpm 也是占了很大优势。

六、npm、yarn、pnpm常用命令

命令npmyarnpnpm
查看版本号npm -vyarn -vpnpm -v
初始化npm inityarn initpnpm init
运行脚本npm runyarn runpnpm run
发布脚本npm publishyarn publishpnpm publish
清除缓存npm cache cleanyarn cache cleanpnpm cache clean
安装所有依赖npm installyarnpnpm install/i
安装某个依赖npm install [package]yarn add [package]pnpm add [package]
安装开发依赖npm install --save-dev/-D [package]yarn add --dev/-D [package]pnpm add --dev/-D [package]
卸载依赖npm uninstall [package]yarn remove [package]pnpm remove/rm [package]
更新全部依赖npm updateyarn upgradepnpm update/up
更新某个依赖npm update [package]yarn upgrade [package]pnpm update/up [package]

七、安装常见问题

npm install yarn -g 报错

报错信息:request to registry.npmjs.org/yarn/-/yarn… failed. reason: self-signed certifificate in certificate chain

解决方式

  1. npm config set strict-ssl false
  2. npm install yarn -g

.npmrc的作用

.npmrc,可以理解成npm running cnfiguration, 即npm运行时配置文件。简单点说, .npmrc 可以设置 package.json 中依赖包的安装来源,既从哪里下载依赖包。

优先级: 项目配置文件: /project/.npmrc > 用户配置文件:~/.npmrc > 全局配置文件:$PREFIX/etc/npmrc > npm 内置配置文件 /path/to/npm/npmrc

//cnshautmplv024.asia.pwcinternal.com/_registry/npm/:_authToken=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
registry=https://registry.npmmirror.com
@pwc-cn-gts-bs-fcs:registry=https://cnshautmplv024.asia.pwcinternal.com/_registry/npm
shamefully-hoist = true
strict-ssl=false
strict-peer-dependencies=false
  • shamefully-hoist 是否提升依赖,如果某些工具仅在根目录的node_modules时才有效,可以将shamefully-hoist设置为true来提升那些不在根目录的node_modules,就是将你安装的依赖包的依赖包的依赖包的...都放到同一级别(扁平化)。说白了就是不设置为true有些包就有可能会出问题。

  • strict-peer-dependencies 当 peerDependencies错误时,命令是否成功

tdh-pro安装依赖时报403

MicrosoftTeams-image.png 修改本地host文件

10.157.146.206 cnshagithubpackagesprd.blob.core.chinacloudapi.cn

输入url之后到渲染的过程

  1. DNS解析,将url解析成ip
  2. 通过ip找到真正的服务器地址
  3. 服务器返回数据(html)渲染

我们修改host文件目的就是劫持,在请求dns服务解析ip之前,提前把url和对应服务器ip地址的映射关系配置好。

客户端查询DNS的过程

本地host文件—本地缓存—-DNS服务器

Host文件用于本地DNS解析,并且优先于寻找网络上的DNS服务器

在Windows中,它的目录通常在Windows目录\system32\drivers\etc\下。需copy到桌面修改完成后再替换

在Mac中,它的目录是/etc/hosts.

npm 安装依赖报错

  • npm ERR! code ECONNRESET
  • npm ERR! network tunneling socket could not be established, cause=connect ECONNREFUSED
  • npm ERR! code ECONNRESET
  • npm ERR! code ETIMEOUT
  • npm ERR! code ENOFFOUND

这些错误的原因很有可能是npm使用默认的源下载安装包,而默认的安装源是国外网站,国内访问不了无法获取依赖包信息。

这时只需要更换为国内的安装源即可,可在命令行更换为国内淘宝的源:

# 查看自己的安装源
npm config get registry

# 更换npm源为国内淘宝镜像
npm config set registry http://registry.npm.taobao.org/

# 或者更换为国内npm官方镜像
npm config set registry http://registry.cnpmjs.org/

# 还原npm源
npm config set registry https://registry.npmjs.org/

八、参考文章

  1. npm、cnpm、yarn、pnpm
  2. 基础15:npm、yarn、pnpm
  3. 一文带你了解前端包管理工具npm、yarn和pnpm
  4. pnpm 中文网