包管理工具
pnpm(高性能的npm)
npm和yarn的问题
- npm@3之前:嵌套安装依赖
- windows上目录路径过长
- 同个package被多次复制生成多份文件
- npm@3+和yarn:将依赖扁平化
- 幻影依赖:指在没有
package.json
的情况下引用其他包的依赖 - 依赖结构的不稳定性,不同安装顺序可能不同
- npm包分身,不同包下的同个依赖会被安装多次
- 占用空间
- 非单例,可以出现多个库实例
- 幻影依赖:指在没有
pnpm如何解决
前置知识
inode
:每个文件都有唯一的inode,包含了元信息,可以使用stat
查看。hard link
:一个相互的指针,创建hardlink 指向源文件的 inode,系统并不会重新分配inode。硬链接不管有多少个,都指向的是同一个 inode 节点,这意味着当你修改源文件或者链接文件的时候,都会做同步的修改。每新建一个 hardlink 会把节点连接数增加,只要节点的链接数非零,文件就一直存在,不管你删除的是源文件还是 hradlink。只要有一个存在,文件就存在。.pnpm
中的每个文件都是来自内容可寻址存储的硬链接
soft link
:软链接可以理解为是一个单向指针,是一个独立的文件且拥有独立的 inode,永远指向源文件。类比于 Windows 系统的快捷方式。删除源文件,软链接就会失效。
注意几个地方
- 根目录下的
node_modules
中- 只有直接依赖的包,且通过
软链接
链接到.pnpm 目录中
- 只有直接依赖的包,且通过
.pnpm
:虚拟存储目录,所有直接和间接的依赖都链接到这里,通过<package-name>@<version>
来实现相同模块不同版本之间隔离和复用。Store
:在全局通过Store来存储所有的 node_modules 依赖,并且在.pnpm
中存储项目的hard links
- 在使用 pnpm 对项目安装依赖的时候,如果某个依赖在 sotre 目录中存在了话,那么就会直接从 store 目录里面去 hard-link,避免了二次安装带来的时间消耗,如果依赖在 store 目录里面不存在的话,就会去下载一次。
**假如全局的包变得非常大怎么办?
**使用方法为
pnpm store prune
,它提供了一种用于删除一些不被全局项目所引用到的 packages 的功能例如有个包 axios@1.0.0 被一个项目所引用了,但是某次修改使得项目里这个包被更新到了 1.0.1 ,那么 store 里面的 1.0.0 的 axios 就就成了个不被引用的包,执行
pnpm store prune
就可以在 store 里面删掉它。
可视化分析
我们项目中有一个依赖 bar@1.0.0
。bar@1.0.0也有一个依赖 foo@1.0.0。
- node_modules 下面有 bar@1.0.0 和 .pnpm 目录,没有 foo@1.0.0
- bar@1.0.0 通过软链接指向
.pnpm/bar@1.0.0/node_modules/bar@1.0.0
。.pnpm/bar@1.0.0/node_modules/bar@1.0.0
又通过硬链接指向 Store - bar@1.0.0 依赖的foo@1.0.0 会安装在跟自己的同一级,这里的设计,我理解是根据 node 的 require 机制,bar 中 require('foo') 的时候,就会先找到 foo@1.0.0,而不会往上寻找,这样就避免依赖包版本不一致的问题。
.pnpm/bar@1.0.0/node_modules/foo@1.0.0
。并通过软链接指向 .pnpm 下一级的 foo@1.0.0 .pnpm/foo@1.0.0
一样通过硬链接指向 Store
npm
node
默认的,以JavaScript
开发的,基于NodeJs
的命令行工具
安装流程
- 运行
npm install
,会先去查找配置信息- 检查项目是否存在
.npmrc
文件,没有的话查找全局的,还是没有的话,使用内置的配置文件
- 检查项目是否存在
- 获取完配置后,根据
package
文件构建依赖树- 如果
package-lock.json
存在且与package.json
中的一致,使用lock
中的信息 - 反之使用
package.json
中的信息
- 如果
- 有了依赖树,根据依赖树下载完整的依赖资源
- 下载前会检查是否有缓存,存在则直接解压到
node_modules
- 不存在则会到远程仓库下载,下载完通过
完整性校验
后将其添加到缓存并且解压到node_modules
中(默认不会安装到全局,可以手动加-g
)
- 下载前会检查是否有缓存,存在则直接解压到
- 生成或更新
package-lock.json
文件,记录下依赖包的版本信息,可以保证在以后的任何时候,安装的都是同样的依赖,避免由于版本不一致导致bug的出现
不同版本生成依赖树的区别
npm 2.X
- 简单的循环遍历方式
- 缺点
- 存在大量冗余的依赖
node_modules
大小指数级上升- 由于层级过深,导致路径过长报错
npm 3.X
- 更改为扁平化的层级关系
- 缺点
- 生成的依赖树会因为依赖的顺序不同而不同
- 不同时机安装的依赖版本不同
npm 5.X
- 新增
package-lock.json
文件,存储确定的依赖信息
npm缓存机制
npm config get cache
可以获取缓存的位置
content-v2
存放缓存包的具体内容index-v5
存储依赖包的索引- 根据
index-v5
中的索引去content-v2
中查找具体的源文件
资料显示:npm 在安装依赖的过程中,会根据
lock
文件中具体的包信息,用pacote:range-manifest:{url}:{integrity}
规则生成出唯一的 key,然后用这个 key 通过 SHA256 加密算法得到一个 hash。这个 hash 对应的就是_cache/index-v5
中的文件路径,前 4 位 hash 用来区分路径,剩下的几位就是具体的文件名。文件的内容就是该缓存的具体信息了。其中
_shasum
是 SHA-1 加密返回的 40 位十六进制小写密文。这一步中_shasum
又充当了源文件包索引的作用,我们可以用这个_shasum
去_cache/content-v2
中找一下对应的文件。
我们试着找一下
执行npm install lodash
,在生成的lock
文件中,可以看到:
loadsh
包的integrity
和url
分别是
sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
和
https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz
通过https://emn178.github.io/online-tools/sha256.html
我们可以计算出对应的key值,通过规则,我们确实找到了缓存的文件。
我们找到对应的文件,拿到其中的_shasum
值为679591c564c3bffaae8454cf0b3df370c3d6911c
,通过规则访问对应的地址,可以得到是一个gzip
文件,解压后则是lodash
的源码
如何保证依赖包的完整性
资源完整性校验是npm安装机制的安全保证
在下载依赖之前,该资源包就有一个 hash 值,我们可以在 npm 提供的网站上看到所有这个包的版本信息:registry.npmjs.com/lodash 找一下我们下载的 4.17.21
这个版本。
"4.17.21":{
...
"dist":{
"integrity":"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"shasum":"679591c564c3bffaae8454cf0b3df370c3d6911c",
"tarball":"https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"fileCount":1054,
"unpackedSize":1412415,
}
}
可以看到一个熟悉的字段 shasum
。我们只需要在下载完成之后在本地用相同的算法再次计算得出一个 shasum
,对比两个 shasum
:如果相同,就代表下载的依赖是完整的;反之,则说明下载过程中出现了问题,会再次重新下载。
开发中的最佳实践
npm
使用5.7+
版本(package-lock.json
生成逻辑在5.6+
逐渐稳定)- 将
package.json
、package-lock.json
提交到远端仓库,同时 ignore掉node_modules
文件 - 更改依赖后,将更新的
package.json
文件和package-lock.json
文件一起提交到远端仓库,项目成员拉取新代码并更新依赖 - 如果
package-lock.json
文件冲突,应该先手动解决package.json
的冲突,然后执行npm install --package-lock-only
,让 npm 自动帮你解冲突。可以参考 官方推荐做法。