本篇文章是关于项目依赖的一些疑问整理,主要是平时工作中积累的一些问题。比如node_modules的作用、不同包管理器的特性等等。
node_modules
node_modoules是我们npm库存放的地方,它是我们使用三方能力的基础。
比如我们现在需要一个url转图片二维码的能力,要么我们自己去了解二维码生成规则,然后用canvas画一个出来;要么去寻找社区解决方案,使用三方能力。显然实际项目开发过程中,我们都会选择后者。理由很明显,这是一个常见的需求,社区肯定有成熟的解决方案,并且经过社区实践,它足够健壮。另一方面,我们只是简单使用,完全没必要花学习成本在这上面,得不偿失。
经过一番查找,我们选定了qrcode这个包
// 第一步 安装npm包
npm install --save qrcode
// 第二步 引入
import QRCode from 'qrcode'
// 使用
QRCode.toDataURL('https://www.baidu.com')
.then(url => {
console.log(url)
})
.catch(err => {
console.error(err)
})
简单两步,我们就获得了生成二维码的能力(感谢开源库作者)。接下来我们稍微深入的了解下这套系统的运作。
如何找到一个满意的npm包
首先来认识npm包发布网站 www.npmjs.com, 利用它的搜索功能,可以检索发布到npm上面的所有包。
比如我们直接搜索qrcode关键字
一般我们按默认排序就可以了,我们点第一个链接进去。(注:质量quality仅代表npm网站的计算规则所得出来的值,比如版本数量,更新频率,未关闭的issue等,不代表当前包的代码质量。)
基本上根据上面的指标和功能,我们就能确定这个包是否符合我们的要求。
除了上面npm网站以外,我们还可以去开发者社区寻找答案,一般都能找到当前流行的库。之后最好再去网站上看下目标库的指标,根据实际情况选用。
通过package.json认识三方库
熟悉一个三方库,我们一般从它的package.json文件入手,下面列出了需要重点关注的属性。
{
"name": "acorn", // 项目名(重要)
"description": "ECMAScript parser", // 描述信息
"homepage": "https://github.com/acornjs/acorn", // 项目主页
"main": "dist/acorn.js", // 入口文件
"types": "dist/acorn.d.ts", // 类型文件入口
"module": "dist/acorn.mjs", // esmodule风格导出
"exports": { // 声明导出
".": [
{
"import": "./dist/acorn.mjs", // 用import时导出
"require": "./dist/acorn.js", // 用require时导出
"default": "./dist/acorn.js" // 默认导出
},
"./dist/acorn.js"
],
"./package.json": "./package.json"
},
"version": "8.8.0", // 版本(重要)
"engines": { // 项目运行的版本范围
"node": ">=0.4.0"
},
"repository": { // 源码仓库地址
"type": "git",
"url": "https://github.com/acornjs/acorn.git"
},
"license": "MIT", // 协议
"scripts": { // 脚本命令
"prepare": "cd ..; npm run build:main"
},
"devDependencies": { // 开发过程中模块依赖
"c8": "^7.7.3",
"chai": "^4.3.4",
....
},
"dependencies": { // 模块依赖的模块列表
"ajv": "^6.12.4",
"debug": "^4.3.2",
....
},
"bin": { // 可执行模块
"acorn": "./bin/acorn"
}
}
通过以上属性我们对三方库有了一个整体的认识,之后不论是通过README.md学习使用方法,还是通过repository地址,直接学习源码,都有迹可循。
包管理器
当我们安装npm库的时候需要借助包管理器的能力。下图概括了执行npm install的流程,接下来我们就拆开来来学习。
依赖安装结构
我们知道项目依赖的包都安装到了node_modoules文件夹,那么当我们依赖的包本身也依赖其他三方库时,应该安装到什么地方呢?
早期的npm版本,是递归安装,严格安装package.json申明的dependencies列表,在各自的文件夹下面安装依赖。比如会出现这样的文件路径 myapp\node_modules\aggregate-error\node_modules\ndent-string\node_modules\...。
这样的好处是逻辑简单并且有稳定的依赖结构。但是当项目依赖逐步增长时,会出现一些问题:
- 同样的模块会被重复安装,会产生很多小文件,导致我们项目安装依赖非常慢。
- 依赖嵌套层级过深,在
window系统中文件路径最大长度为260个字符,这可能导致不可预知的问题。
所以在npm3之后,嵌套结构优化成了扁平结构。
- 当
npm安装依赖时,不管是自身依赖还是三方库的依赖,都优先安装在顶层node_modoules文件夹中。 - 当遇到重复安装的模块时,如果版本范围兼容,则直接略过。
- 当遇到重复安装的模块时,如果版本范围不兼容,则在当前模块的
node_modules下安装。
npm通过以上策略解决了部分嵌套结构的问题,但是又带来了一个新的问题:结构不确定性。
npm安装包时,是按照 package.json 里依赖的顺序依次解析,那么依赖的放置顺序会直接影响最终安装结构,但是我们开发的时候并不知道当前依赖的放置顺序会引起什么变化,这会导致不确定性。
另外,我们在引入三方库的时候一般只会锁定大版本,那么npm安装的时候总会去安装最新版本的包,这会导致其它重复依赖的版本兼容性,同样也导致了不确定性。
相信大家在实际工作中遇到过这种不确定性带来的问题,并且这些问题往往表现非常奇怪也难以排查。
lock文件
为了解决不确定性,npm5新增了package-lock.json 文件。lock文件同时锁定了版本和结构,只要项目里面有lock文件,那么每次安装的结果一定是完全相同的。
{
"name": "myapp", // 项目名称
"version": "1.1.0", // 项目版本
"lockfileVersion": 2,
"requires": true,
"packages": { // 依赖列表
"node_modules/@commitlint/cli": {
"version": "7.6.1", // 锁定版本
"resolved": "https://registry.npmmirror.com/@commitlint/cli/-/cli-7.6.1.tgz", // 引用地址
"integrity": "sha512-HEJwQ/aK0AOcAwn77ZKbb/GZhlGxBSPhtVp07uoJFVqM12l2Ia2JHA+MTpfHCFdVahKyYGREZgxde6LyKyG8aQ==", // 唯一hash 来验证已安装的软件包是否被改动过、是否已失效
"dev": true, // 被安装位置
"dependencies": { // 和package.json一致
"@commitlint/format": "^7.6.1",
"@commitlint/lint": "^7.6.0",
"@commitlint/load": "^7.6.1",
"@commitlint/read": "^7.6.0",
"babel-polyfill": "6.26.0",
"chalk": "2.3.1",
"get-stdin": "7.0.0",
"lodash": "4.17.11",
"meow": "5.0.0",
"resolve-from": "5.0.0",
"resolve-global": "1.0.0"
},
"bin": {
"commitlint": "lib/cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/caller-callsite": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/caller-callsite/-/caller-callsite-2.0.0.tgz",
"integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==",
"dev": true,
"dependencies": {
"callsites": "^2.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/caller-callsite/node_modules/callsites": { // 版本冲突时重复安装
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/callsites/-/callsites-2.0.0.tgz",
"integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==",
"dev": true,
"engines": {
"node": ">=4"
}
}
}
}
除了锁定依赖版本和机构之外,lock文件还可以增加安装速度,因为已经确定了库的版本和下载地址,节省了npm去远程仓库查询的时间。
我们在实际开发项目时,建议把package-lock.json 加入git版本管理,项目组成员统一,这可以减少很多诡异的问题。
缓存策略
在执行npn install时,除了把包安装到项目目录,还会缓存一份到npm-cache目录。通过 npm config get cache 命令可以查询到缓存路径,windows是C:\Users\用户名\AppData\Local\npm-cache。
在这个目录下又存在两个目录:content-v2、index-v5,content-v2 目录用于存储包的具体信息,包括所有版本信息及缓存位置,而index-v5目录用于存储包的 hash。
npm 在执行安装时,可以根据 package-lock.json 中存储的 integrity、version、name 生成一个唯一的 key 对应到 index-v5 目录下的缓存记录,然后根据特定算法(具体算法可以看npm源码的tar.js),算出content-v2包的位置。
比如随意找一个index-v5目录里面的proto-list包,可以算出sha512信息:a7df35db45f26cf0c6597404450351e3443905a2705f2eda324452d7dcce9facf35d922a0e66395a33643fc0a749ef9c55c1316c9c10d418c8c7435bd84fd9ae (前四位是目录,后面的是文件名) 对应路径 _cacache\content-v2\sha512\a7\df\35db45f26cf0c6597404450351e3443905a2705f2eda324452d7dcce9facf35d922a0e66395a33643fc0a749ef9c55c1316c9c10d418c8c7435bd84fd9ae文件。
npm 提供了几个命令来管理缓存数据:
npm cache clean:删除缓存目录下的所有数据,为了保证缓存数据的完整性,需要加上--force参数。npm cache verify:验证缓存数据的有效性和完整性,清理垃圾数据。
基于缓存数据,npm 提供了离线安装模式,分别有以下几种:
--prefer-offline:优先使用缓存数据,如果没有匹配的缓存数据,则从远程仓库下载。--prefer-online:优先使用网络数据,如果网络数据请求失败,再去请求缓存数据,这种模式可以及时获取最新的模块。--offline:不请求网络,直接使用缓存数据,一旦缓存数据不存在,则安装失败。
文件完整性
npm下载包到本地后,首先就会校验文件的完整性,确保下载过程中没有错误。
执行npm info lodash我们可以得到
lodash@4.17.21 | MIT | deps: none | versions: 114
Lodash modular utilities.
https://lodash.com/
keywords: modules, stdlib, util
dist
.tarball: https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz
.shasum: 679591c564c3bffaae8454cf0b3df370c3d6911c
.integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
maintainers:
- mathias <mathias@qiwi.be>
- jdalton <john.david.dalton@gmail.com>
- bnjmnt4n <benjamin@dev.ofcr.se>
dist-tags:
latest: 4.17.21
其中shasum就是完整性hash,包下载到本地后,本地会再计算一次文件的 hash 值,如果两个 hash 值是相同的,则可以确定下载的包是完整的,如果不同,则进行重新下载。
整体流程
整体流程如下:
-
检查
.npmrc文件:优先级为:项目级的.npmrc文件 > 用户级的.npmrc文件> 全局级的.npmrc文件 > npm 内置的.npmrc文件 -
检查项目中有无
lock文件。 -
无
lock文件则重新构建依赖树:-
从
npm远程仓库获取包信息 -
根据
package.json构建依赖树,构建过程:- 当`npm`安装依赖时,不管是自身依赖还是三方库的依赖,都优先安装在顶层`node_modoules`文件夹中。 - 当遇到重复安装的模块时,如果版本范围兼容,则直接略过。 - 当遇到重复安装的模块时,如果版本范围不兼容,则在当前模块的`node_modules` 下安装。注意这一步只是确定逻辑上的依赖树,并非真正的安装,后面会根据这个依赖结构去下载或拿到缓存中的依赖包
-
在缓存中依次查找依赖树中的每个包
-
不存在缓存:
-
从
npm远程仓库下载包 -
校验包的完整性
-
校验不通过:
- 重新下载
-
校验通过:
- 将下载的包复制到
npm缓存目录 - 将下载的包按照依赖结构解压到
node_modules
- 将下载的包复制到
-
-
存在缓存:将缓存按照依赖结构解压到
node_modules
-
-
将包解压到
node_modules -
生成
lock文件
-
有 lock 文件:
- 检查
package.json中的依赖版本是否和package-lock.json中的依赖有冲突。 - 如果没有冲突,直接跳过获取包信息、构建依赖树过程,开始在缓存中查找包信息,后续过程相同
流程图如下:
yarn
yarn 是在 2016 年发布的,那时 npm 还处于 V3 时期,还没有 package-lock.json 文件,就像上面我们提到的:不稳定性、安装速度慢等缺点经常会受到广大开发者吐槽。
当时 yarn 的推出,基于以上的优点,快速受到大家欢迎。后来npm多次迭代升级,借鉴了很多yarn的优秀设计,基本上从使用上来说,两者没有太大区别。
yarn安装后也会生成lock文件yarn.lock
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@ampproject/remapping@^2.1.0":
version "2.2.0"
resolved "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==
dependencies:
"@jridgewell/gen-mapping" "^0.1.0"
"@jridgewell/trace-mapping" "^0.3.9"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.44", "@babel/code-frame@^7.14.5", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.8.3":
version "7.18.6"
resolved "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
dependencies:
"@babel/highlight" "^7.18.6"
"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8":
version "7.18.8"
resolved "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d"
integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==
"@babel/core@>=7.2.2", "@babel/core@^7.14.0", "@babel/core@^7.14.5", "@babel/core@^7.17.9", "@babel/core@^7.8.7":
version "7.18.9"
resolved "https://registry.npmmirror.com/@babel/core/-/core-7.18.9.tgz#805461f967c77ff46c74ca0460ccf4fe933ddd59"
integrity sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g==
dependencies:
"@ampproject/remapping" "^2.1.0"
"@babel/code-frame" "^7.18.6"
"@babel/generator" "^7.18.9"
"@babel/helper-compilation-targets" "^7.18.9"
"@babel/helper-module-transforms" "^7.18.9"
"@babel/helpers" "^7.18.9"
"@babel/parser" "^7.18.9"
"@babel/template" "^7.18.6"
"@babel/traverse" "^7.18.9"
"@babel/types" "^7.18.9"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.1"
semver "^6.3.0"
整体上和 package-lock.json 非常类似,区别在于:
package-lock.json使用的是json格式,yarn.lock使用的是一种自定义格式yarn.lock子依赖的版本号不是固定的,意味着单独又一个yarn.lock确定不了node_modules目录结构,还需要和package.json文件进行配合。而package-lock.json只需要一个文件即可确定。
yarn缓存比较直观,每个缓存的模块被存放在独立的文件夹,文件夹名称包含了模块名称、版本号等信息。使用命令 yarn cache dir 可以查看缓存数据的目录:
yarn 默认使用 prefer-online 模式,即优先使用网络数据,如果网络数据请求失败,再去请求缓存数据。
lock文件更新策略
版本冲突引起更新
-
npm 5.0.x 版本:不管 package.json 中依赖是否有更新,npm i 都会根据 package-lock.json 下载。针对这种安装策略,有人提出了这个 issue - #16866 ,然后就演变成了 5.1.0 版本后的规则。
-
5.1.0 版本后:当 package.json 中的依赖项有新版本时,npm install 会无视 package-lock.json 去下载新版本的依赖项并且更新 package-lock.json。针对这种安装策略,又有人提出了一个 issue - #17979 ,参考 npm 贡献者 iarna 的评论,得出 5.4.2 版本后的规则。
-
5.4.2 版本后:
- 如果只有一个 package.json 文件,运行
npm i会根据它生成一个 package-lock.json 文件。 - 如果 package.json 的 semver-range version 和 package-lock.json 中版本兼容,即使此时 package.json 中有新的版本,执行
npm i也还是会根据 package-lock.json 下载。 - 如果手动修改了 package.json 的 version ranges,且和 package-lock.json 中版本不兼容,那么执行
npm i时 package-lock.json 将会更新到兼容 package.json 的版本。
- 如果只有一个 package.json 文件,运行
resolved字段引起更新
{
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true,
"engines": {
"node": ">=6"
}
}
resolved是记录包引用地址的字段,这本来没有什么问题,但由于国内网络环境,开发中实际使用的registry地址,一般是使用淘宝代理的npm源地址或者公司私有的源地址,这会造成一些问题。
- 如果项目组每个人的npm源地址设置不同,生成的lock文件也会是五花八门的源地址。
- 当项目组成员使用另外的地址安装npm包后,所有重新安装的包会更新
resolved字段,造成代码冲突。
所以推荐项目里面添加.npmrc文件,统一npm相关配置。
registry=https://registry.npm.taobao.org/ // 统一使用淘宝源地址
@private:registry = https://npm.xx.com/ // @private开头的公司私有库使用私有地址
提升依赖安装速度
不得不说每次重新安装依赖都是一个痛苦的过程,我只能盯着屏幕无助的等待。(当然这也是一段愉快的摸鱼时间,每次老板看到我耍手机,我都说在安装依赖或等编译。😂 )那么有哪些手段能提升我们的安装速度呢?
使用私有镜像仓库
在大型的项目中,一次安装过程往往有几千次的网路请求,包括远程仓库版本查询,tar包下载等。那么使用国内的公有镜像库是一个不错的选择,如淘宝镜像,它每隔十分钟与官方镜像仓库同步一次,而它位于国内,网络不至于太慢。
另外一种选择是企业内搭建一套私有仓库,定时去同步官方镜像。这样本地局域网就闭环了所有操作,速度起飞。
充分利用缓存
虽然我们每次安装依赖时,包管理器都会把文件写一份到缓存中,但再次安装时,缓存优先级并不是想象中那么高。比如yarn 默认使用 prefer-online 模式,即优先使用网络数据,如果网络数据请求失败,再去请求缓存数据。这时候本地缓存仅是作为错误兜底。
为了只痛苦一次,我们可以使用--prefer-offline 来指定缓存优先策略,充分利用缓存。
使用 npm ci 替代 npm i
npm ci 是为了适用于 CI 环境而推出的命令,它做了一系列优化,如去除掉一些面向用户的特性来加强速度。除了性能,它也有一些在 CI 上基于完整性与安全性的检查,如 package.json 与 package-lock.json 版本不一致的问题。
为了更好地提高速度,npm ci 基于一个独立的库 libcipm (opens new window)安装依赖,而它拥有和 npm install 兼容的 API。并且当它安装依赖时,默认是缓存优先的,它会充分利用缓存,从而加速装包。
经实验,npm ci 可以减少将近一半的的依赖安装时间。
上图是我找到的一张优化效果对比图,大家可以感受下。
pnpm
目前来说,yarn和npm区别不大,实际工作中用哪个都可以,只要团队内统一就行。那目前市面上有没有更好用的包管理器呢? 接下来让我们一起了解一下pnpm。
pnpm代表performant npm(高性能的npm),官网上介绍为快速的,节省磁盘空间的包管理工具。
使用上和npm语法相似,比如常用的pnpm init、pnpm install xxx、pnpm run xxx等,很快能上手。另外pnpm还可以管理node环境。(可实现nvm、n等node版本管理工具,安装并切换node.js版本的功能。)
- 本地安装并使用:
pnpm env use <node版本号> - 全局安装并使用:
pnpm env use --global <node版本号>
速度提升
基于pnpm优秀的设计,它在安装速度上有明显的改善,大多数情况下pnpm 比其他包管理器快 2 倍。
在pnpm官网上,提供了一个benchmarks基准测试图表,它展示了npm、pnpm、Yarn、Yarn pnp在install、update等场景下的耗时:
节约磁盘空间
当使用 npm 或 Yarn 时,如果你有 100 个项目使用了某个依赖(dependency),就会有 100 份该依赖的副本保存在硬盘上。 而在使用 pnpm 时,依赖会被存储在内容可寻址的存储中,所以:
- 如果你用到了某依赖项的不同版本,只会将不同版本间有差异的文件添加到仓库。 例如,如果某个包有100个文件,而它的新版本只改变了其中1个文件。那么
pnpm update时只会向存储中心额外添加1个新文件,而不会因为仅仅一个文件的改变复制整新版本包的内容。 - 所有文件都会存储在硬盘上的某一位置。 当软件包被被安装时,包里的文件会硬链接到这一位置,而不会占用额外的磁盘空间。 这允许你跨项目地共享同一版本的依赖。
因此,您在磁盘上节省了大量空间,这与项目和依赖项的数量成正比,并且安装速度要快得多!
稳定
之前我们提到在npm3之后,嵌套结构优化成了扁平结构。这会造成两个问题:
- 由于所有包都被提升到模块目录的根目录,项目可以访问到未被添加进当前项目的依赖而不会报错。
- 扁平结构是不稳定的,会根据安装顺序不同,实际依赖结构不同,需要借助
lock文件规避这个问题。
默认情况下,pnpm 使用软链的方式将项目的直接依赖添加进模块文件夹的根目录,项目中node_modules文件夹下只会有直接依赖的软连接,直接依赖的次级依赖放在自本身的node_modules文件夹下,所以pnpm生成的是稳定的嵌套结构。
对比npm和pnpm安装的node_modules:
| npm | pnpm |
|---|---|
所有依赖包平铺在node_modules目录,包括直接依赖包以及其他次级依赖包 | node_modules目录下只有.pnpm和直接依赖包(vue、vite、...),没有其他次级依赖包 |
| 没有符号链接 | 直接依赖包的后面有符号链接的标识 |
npm或Yarn 转 pnpm
pnpm有这些优点,如果我想从npm或yarn转到pnpm,应该怎么做影响最小呢?
- 全局安装
pnpm
npm install -g pnpm
- 删除
npm或yarn生成的node_modules
# 项目目录下运行或手动物理删除
rm -rf node_modules
pnpm import从其他软件包管理器的lock文件生成pnpm-lock.yaml,再执行pnpm install --frozen-lockfile(相当于npm ci)生成依赖,防止没有lock文件意外升级依赖包,导致项目出错
# 生成`pnpm-lock.yaml`
pnpm import
# 安装依赖
pnpm install --frozen-lockfile
- 删除
npm或yarn生成的lock文件
# 删除package-lock.json
rm -rf package-lock.json
# 删除yarn.lock
rm -rf yarn.lock
- 项目中的
npm命令等修改为pnpm,包括README文档、运行命令等
整体来看,pnpm是值得我们去尝试、去学习的提升效能的工具,但是工作项目中是否使用或者切换,应该根据实际情况来判断,毕竟项目稳定后,安装依赖的频次是很低的。
引用致谢: