目前前端常用的包管理工具有如下几个,对于常用包管理工具,其管理的包都需要遵循semver规范
npm
cnpm
yarn
pnpm
Semver规范
举例而言
1.5.4
是版本号,其中
- 1 是主版本号,代表的是不兼容的 API 修改
- 5 是次版本号,代表的是向下兼容的功能性新增
- 4 是修订号,代表的是向下兼容的问题修正
package.json
中的更新规范
- 如果写入的是 〜0.13.0,则只更新补丁版本:即 0.13.1 可以,但 0.14.0 不可以。
- 如果写入的是 ^0.13.0,则要更新补丁版本和次版本:即 0.13.1、0.14.0、依此类推。
- 如果写入的是 0.13.0,则始终使用确切的版本。
npm
通过 npm install
进行安装,一般的npm
包都遵循Semver
规范
- 将依赖包的版本解析为某个具体的版本号
- 下载对应版本依赖的
tar
包到本地离线镜像 - 将依赖从离线镜像解压到本地缓存
- 将依赖从缓存拷贝到当前目录的
node_nodules
目录
目前存在的问题
对于 npm@3
之后,避免了嵌套而使用扁平化的包管理方式,但同时存在以下问题
-
依赖结构的不确定性和二重身(doppelgangers)。
一个包的不同版本还是会重复安装(不能打平同一个包的不同版本),能会造成同一个包重复安装,性能还是会损失
-
项目中仍然可以**非法访问依赖 (phatom)**没有声明过依赖的包
package.json
中只声明了 A, 但A
的depdencies
有B
, 这样安装在A
时B
也会被安装,项目中还是可以require
到B
,这是正是由于扁平化引起的 -
扁平化算法本身的复杂性很高,耗时较长。
对于依赖结构不确定的情况,比如项目中存在如下两个包 foo
和 bar
,依赖关系如下
foo
依赖base64-js@1.0.1
bar
依赖base64-js@1.0.2
则扁平化后可能是下面两种中的一种(依赖于foo
和 bar
在package.json
中的位置
一部分为了解决如上的问题,在npm@5
版本后,推出了package-lock.json
,其会固化当前每个软件包的版本,而在下次的npm install
中,会使用这些确切的版本,同时其也会锁定其依赖的依赖。
如果想要更新 package-lock.json
中的版本,可以执行 npm update
,此命令会根据 Semver
规范更新依赖包,但是出于谨慎,最好还是单次升级某个特定的包:npm update xxpackage
。
其他常用命令
npm list --depth=0
查看整个项目的当前安装的依赖包版本。
npm outdated
查询已安装的包离最新版本的距离,Latest
是这个包的最新大版本,Wanted
是这个包当前大版本下的最新小版本。
yarn
npm@5
之前因为没有锁定版本的功能,所以有人使用了 yarn
和 yarn.lock
来补足,npm@5
以后在这点上差异不大,同时, 因为yarn
是并行安装(PnP),所以安装速度上会更快一些,其他地方差异不大。
这里注意下,对于yarn
,和npm
不同的是,需要同时拥有 yarn.lock
和 package.json
才能保证其依赖包的确定性。
但对于扁平化算法复杂
和package非法访问
的问题依然存在
cnpm
淘宝产出的国内的包管理器,由于其镜像在淘宝机房,因此国内的安装速度比较好,但是有部分包内依赖包还是调用了npm i
,所以还是可能会慢。
缺点
**cnpm
没有package-lock.json
**
这个问题不用说了,可能导致多个环境对应的依赖版本不一致,上线可能导致各种各样的问题
cnpm使用软链接
在cnpm
进行一次安装后,是软链接(符号链接)到了对应的npm
包上(类似一个有垃圾回收机制的指针),而如果此时又进行了npm install
的操作,则新的npm
包可能更新,这会使得cnpm
对应的包实际也变成了新的,那么在对应的cnpm
再次引入的时候就可能导致错误
可以通过如下命令来使用原有的npm
的包管理方式而不适用软链接,但是会损失速度
cnpm i --by=npm
cnpm i --by=npm react-native
pnpm
pnpm
的安装速度较快,同时其基于内容寻址的文件系统来存储文件,他会在全局的 store
目录下存储 node_modules
文件的 hardlink
,这样,用户可以通过不同路径去寻找某个文件。
-
对于同一个包,不会重复安装,而是只安装一份,之后的使用都是直接通过硬链接
-
对于同一个包的不同版本,会尽可能复用之前版本的代码,比如更新后多了一个文件,则是生成之前文件的硬链接,同时写入那一个新文件
-
什么是
hardlink
和symlink(symbolic link)
hardlink
:有垃圾回收的指针如果A是B的硬链接,则
A
的indexNode
与B
的indexNode
指向同一个,删除其中任何一个都不会影响另外一个,只有当所有指向同一文件的硬链接删除后,该文件才真正删除。symlink
: 没有垃圾回收的指针也可类比为桌面的快捷方式,比如A是B的软链接,则
A
存放着B
的路径,访问A时会自动找到B,如果B删除,而A
存在,则A
指向了一个无效链接
一般pnpm
将node_modules
文件的hardlink
会放到全局的store
目录下(nvm
下的安装除外)
${os.homedir}/.pnpm-store/v3/files
store
目录也会随着安装包的数量增大,使用 pnpm store prune
可以删除不再被引用的包
pnpm的树形 + 扁平结构
我们安装一个swiper
的包,此时会自动生成一个pnpm-lock.yaml
文件,用来记录当前安装的这个包和这个包依赖的所有包
安装任意一个包后,在项目的根 node_modules
目录下会存在两个目录,一个是.pnpm
虚拟磁盘目录,用户不能直接从这个目录中require
另一个是真正这个包的目录,也就是 js
中 require
的路径,这个目录当前只是一个软链接,软链接到.pnpm
中的对应包的目录,这个目录就是一个硬链接了,而 .pnpm
中的这个硬链接,会真实链接到全局的 store
中
phatom (非法访问依赖)的解决
由于依赖打平是在 .pnpm
目录中的,而不是在 node_modules
中, 无法直接被项目require
,也就不存在非法访问了
doppelgangers (二重身)的解决
因为pnpm
的依赖始终是安装在全局的store
下的,所以不存在安装重复的包的情况,一个包的不同版本始终只会被安装一次
独有特性
可以和.npmrc
文件联动,从.npmrc
读取指定的node
版本,并以该版本执行 pnpm run
等命令
对于使用了nvm
来管理node
版本的情况(nvm
也是npm
官方所推荐的node
版本管理工具)
支持monorepo
通过全局配置 pnpm-workspace.yaml
packages:
# all packages in subdirs of packages/ and components/
- 'packages/**'
或者通过 pnpm 命令安装全局公共包
pnpm install react react-dom -w // 根目录
pnpm i dayjs -r --filter packageName // 安装在指定的 packages下