一、 解决问题
- 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 发布并进行了各种改进
时间线
参考: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的比较
- 并行安装:yarn安装包会同时执行多个任务,npm 需等待上一个任务安装完成才能运行下一个任务(按照在package.json中声明的顺序),所以npm install 下载速度慢,即使是重新 install 时速度依旧慢
- 离线模式:如果你已经安装过一个包,用 yarn 再次安装会从缓存中获取,而 npm 会从网络下载
- 版本锁定:yarn 默认有一个 yarn.lock 文件锁定版本,保证环境统一,npm是通过package-lock.json
- 更简洁的输出:yarn 安装包时输出的信息较少,npm 输出信息冗余
npm和yarn同时混用会有什么问题
前置知识
各依赖的版本格式
npm 包采用语义化格式, a.b.c 语义:
- a 代表主版本号,做了不兼容的 API 修改
- b 次版本号,做了向下兼容的功能性新增
- 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 依赖包为例
-
原项目是用npm来进行包管理,从而生成package-lock.json文件,里面存储了各个依赖的具体来源和版本号,其中typescript的版本号为4.2.4,所以今后使用npm进行安装依赖时都会安装typescript的4.2.4版本,不会进行自动升级
-
如开发者使用yarn命令来进行包依赖安装,则package-lock.json文件无效,只看package.json中的文件,但typescript版本号为^4.2.4,从而会安装4.x.x版本中最新版本即为4.6.3版本,同时生成对应yarn.lock文件
-
启动项目会出现typescript类型报错,其原因是因为原项目是在4.2.4typescript版本环境下编写,但使用yarn进行依赖安装把typescript版本自动更新成了 4.6.3版本,同时4.2.4和4.6.3版本的typescript在类型校验上进行了较大的改进和优化
如何选择正确的包管理工具以及安装依赖
- 将项目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 目录
.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,那么这个依赖在多个项目中使用,在每次安装的时候都会被重新下载一次。
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,且用法及其简单。
五、性能对比
| action | cache | lockfile | node_modules | npm | pnpm | Yarn | Yarn PnP |
|---|---|---|---|---|---|---|---|
| install | 51s | 14.4s | 39.1s | 29.1s | |||
| install | ✔ | ✔ | ✔ | 5.4s | 1.3s | 707ms | n/a |
| install | ✔ | ✔ | 10.9s | 3.9s | 11s | 1.8s | |
| install | ✔ | 33.4s | 6.5s | 26.5s | 17.2s | ||
| install | ✔ | 28.3s | 11.8s | 23.3s | 14.2s | ||
| install | ✔ | ✔ | 4.6s | 1.7s | 22.1s | n/a | |
| install | ✔ | ✔ | 6.5s | 1.3s | 713ms | n/a | |
| install | ✔ | 6.1s | 5.4s | 41.1s | n/a | ||
| update | n/a | n/a | n/a | 5.1s | 10.7s | 35.4s | 28.3s |
以上测试结果可以看出,首次执行 npm install 安装依赖时 pnpm 比 npm 和 yarn 大约快 3 倍左右,在有缓存和已安装过依赖的情况,比 npm 也快了不少,yarn 则是更快,其他场景 pnpm 也是占了很大优势。
六、npm、yarn、pnpm常用命令
| 命令 | npm | yarn | pnpm |
|---|---|---|---|
| 查看版本号 | npm -v | yarn -v | pnpm -v |
| 初始化 | npm init | yarn init | pnpm init |
| 运行脚本 | npm run | yarn run | pnpm run |
| 发布脚本 | npm publish | yarn publish | pnpm publish |
| 清除缓存 | npm cache clean | yarn cache clean | pnpm cache clean |
| 安装所有依赖 | npm install | yarn | pnpm 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 update | yarn upgrade | pnpm 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
解决方式
- npm config set strict-ssl false
- 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
修改本地host文件
10.157.146.206 cnshagithubpackagesprd.blob.core.chinacloudapi.cn
输入url之后到渲染的过程
- DNS解析,将url解析成ip
- 通过ip找到真正的服务器地址
- 服务器返回数据(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/