likes
comments
collection
share

你知道 npm、yarn、pnpm 下载依赖时遇到版本冲突是怎么解决的吗?

作者站长头像
站长
· 阅读数 3

1. 了解 package.json 依赖字段含义

首先,npm、yarn、pnpm 下载依赖都离不开 package.json 文件,我们先了解一下 package.json 的依赖字段以及版本限制。

1.1 依赖的分类

  • dependencies - 业务依赖
  • devDependencies - 开发依赖
  • peerDependencies - 同伴依赖
  • bundledDependencies - 打包依赖
  • optionalDependencies - 可选依赖

1.2 依赖包版本号

npm 采用了 semver 规范作为依赖版本管理方案;一个 npm 依赖包的版本格式一般为:主版本号.次版本号.修订号(x.y.z)

{
  // 常用的版本控制写法
    "antd-mobile": "5.32.4", // 精确匹配版本号
    "core-js": "^3.31.0", // >= 3.31.0 < 4.0.0
    "md5": "~2.3.0", // >=2.3.0 < 2.4.0
  // 其他写法
    "react": ">18.2.0", // 大于
    "react-dom": ">=18.2.0", // 大于等于
    "prettier": "<2.8.8", // 小于
    "stylelint": "<=15.10.3", // 小于等于
    "webpack": "=5.87.0", // 等于 5.87.0 版本,没写运算符默认就是 =
    "ahooks": ">=3.7.8 <3.9.0", // 大于等于 3.7.8 且小于 3.9.0
    "classnames": "<2.3.2 || >3.0.0", // 小于 2.3.2 或 > 3.0.0
}

还有一些其他写法,具体可以在 npm官网 查看。

2. npm 解决依赖冲突

2.1 检查项目中是否有package-lock.json文件

从npm 5.x开始,执行npm install时会自动生成一个 package-lock.json 文件。

package-lock.json 文件精确描述了node_modules 目录下所有的包的树状依赖结构,每个包的版本号都是完全精确的。

2.2 package-lock.json文件存在

检查package-lock.json和package.json中声明的依赖是否一致?一致则直接使用 package-lock.json 里声明的依赖树去缓存或者网络上下载。

2.2 package-lock.json文件不存在

根据package.json递归构建依赖树,然后根据依赖树下载完整的依赖资源,在下载时会检查是否有相关的资源缓存,有的话就直接使用缓存的包解压到 node_modules,没有的话就从远程下载到缓存目录中并解压到 node_modules 目录。

2.3 下载依赖时遇到相同依赖的包时

从npm 3.x开始就采用了 扁平化 的方式来安装模块,优先将模块安装在一级node_modules中。当安装到相同模块时,判断已安装的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的node_modules下安装该模块。

3. yarn 解决依赖冲突

yarn是由Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具 ,正如官方文档中写的,Yarn 是为了弥补 npm 的一些缺陷而出现的。但是随着 npm 的迭代,npm 也对其性能进行了优化,特别是 npm 5 之后,性能上的差异有所缩小。

yarn 处理依赖冲突的方式其实和 npm 5 的逻辑类似,也是扁平化方式安装,只不过记录依赖关系链的 lock 文件是 yarn.lock。

4. pnpm 解决依赖冲突

pnpm 使用一种独特的策略来管理和解决版本冲突,这依赖于它的两个核心特性:

  1. 硬链接和符号链接:pnpm 创建一个称为 store 的中央存储库,用于存放所有下载的包的唯一副本。当你在项目中安装一个包时,pnpm 不会将这个包的副本放到你的 node_modules 文件夹中,而是创建指向存储库中该包版本的硬链接。如果需要(例如在不同的包需要不同版本时),它还可以创建指向硬链接的符号链接(symlinks)。
  2. 扁平的 node_modules 结构:尽管 pnpm 保持 node_modules 的扁平结构,但是它不像 npm 那样简单地将所有包提升到顶层。相反,pnpm 会为每个包创建一个符号链接,这些链接指向存储库中的确切版本。但是,如果不同的包需要不同版本的依赖,pnpm 将会在该包的 node_modules 目录下创建一个子目录,其中包含指向所需版本依赖项的符号链接。

以下是 pnpm 解决依赖冲突的方式:

  1. 创建全局存储 pnpm 将所有下载的包版本保存在一个全局存储位置。每个包版本都存储在其自己的目录中,与其他版本完全隔离。
  2. 解析依赖版本 当 pnpm install 命令运行时,pnpm 首先解析 package.json 文件中指定的依赖,并尝试找出每个依赖包的最合适版本。在这个过程中,pnpm 会尊重 package.json 和 pnpm-lock.yaml 文件中指定的版本范围和锁定的版本。
  3. 遇到版本冲突 如果不同的包依赖同一个包的不同版本(即发生了版本冲突),pnpm 会为每个需要特定版本的依赖分别安装和链接这个包的不同版本。这意味着在 node_modules 目录中,每个包都会得到满足其版本要求的依赖副本。
  4. 使用符号链接 pnpm 通过在项目的 node_modules 目录中创建符号链接(对于 Windows 系统是联接点)来引用全局存储中的包。这允许 pnpm 保持 node_modules 中的物理文件数量最小,同时处理版本冲突。
  5. 扁平化 node_modules 目录 尽管 pnpm 支持嵌套依赖,但它仍然尽可能地将包扁平化,只有在有必要解决版本冲突时才创建嵌套结构。一个包的多个依赖如果都依赖同一版本的子包,那么这个子包将被提升到一个共享的位置,以避免重复。
  6. 复用依赖 由于全局存储的内容寻址性质,即使不同的项目使用了相同的包版本,pnpm 也只需要保存一份该版本的副本。这样,即使处理了依赖冲突,pnpm 仍旧能够节省磁盘空间。

总的来说,pnpm 通过独特的全局存储和符号链接的方式,能够有效地处理依赖冲突,同时避免不同项目或不同依赖间的冗余存储。这一点对于保持 node_modules 的清晰和维护磁盘空间的高效利用是非常有帮助的。

5. npm 和 yarn 的扁平安装

  1. 在下载依赖时,首先将 package.json 里的依赖按照首字母(@排最前)进行排序,然后将排序后的依赖包按照广度优先遍历的算法进行安装,最先被安装到的模块将会被优先安装在一级 node_modules 目录下。
  2. 遇到相同依赖不同版本的时候,如果一级 node_modules 目录下已经存在这个依赖且不能和兼容,那么会将这个版本的依赖安装在当前模块的 node_modules 目录下;如果不存在版本冲突,则会忽略安装。

例如:

假设项目App中有如下三个依赖:

"dependencies": {
    A: "1.0.0",
    B: "1.0.0",
    C: "1.0.0"
}

A、B、C三个模块又有如下依赖:

A@1.0.0 -> D@1.0.0

B@1.0.0 -> D@2.0.0

C@1.0.0 -> D@2.0.0

D@2.0.0 模块和 D@1.0.0 模块都有可能优先被安装在一级 node_modules 目录下,谁先安装谁就在一级node_modules 目录下;所以执行 install 后,node_modules 会变成如下目录结构:

├── node_modules
│   ├── A@1.0.0
│   │   └── node_modules
│   │   │   └── D@1.0.0
│   ├── B@1.0.0
│   ├── C@1.0.0
│   └── D@2.0.0

也可能变成如下目录结构:

├── node_modules
│   ├── A@1.0.0
│   ├── B@1.0.0
│   │   └── node_modules
│   │   │   └── D@2.0.0
│   ├── C@1.0.0
│   │   └── node_modules
│   │   │   └── D@2.0.0
│   └── D@1.0.0

当然,如果之前的 package.lock.json 或 yarn.lock 文件已经指定了下载依赖关系,那么就一 lock 文件为主。

参考: