npm 与 yarn 安装包不一致

问题
run 一个 CRA 项目,使用 npm 与 yarn 安装包,发现 npm 安装的包有 @babel/plugin-proposal-optional-chaining, 而 yarn 安装的没有 @babel/plugin-proposal-optional-chaining。本地 npm 安装包后启动正常,而生产环境使用的 yarn, 造成构建失败。
原因
yarn install 安装生成的 yarn.lock 文件 与 npm install 生成的 package-lock.json 文件时间相差较远,造成了 yarn.lock 的包版本低于 package-lock.json 的包版本。因为以 ?x.x.x 形式定义的包版本在不同时期安装包版本不一致。
yarn 安装 @babel/preset-env 版本有: "7.5.5", "^7.4.5",实际安装的 version 是 "7.5.5"。
npm 与 yarn 安装包不一致
文章图片

npm 安装 @babel/preset-env 版本有: "7.9.0", "^7.4.5",实际安装的 version 是 "7.9.0" 和 "7.11.5"。而在 “7.8.3”版本里首次依赖 @babel/plugin-proposal-optional-chaining
npm 与 yarn 安装包不一致
文章图片

解决:如果执行 yarn upgeade 就会更新 yark.lock 文件, 获取最新的包版本。
拓展 package.json
package.json 主要用来记录依赖包名称、版本、运行指令等信息字段。
其中,dependencies 字段指定了项目运行所依赖的模块,devDependencies 指定项目开发所需要的模块。它们都指向一个对象,该对象的各个成员,分别由模块名和对应的版本组成,表示依赖的模块及其版本范围。
版本限定方式【语义化版本】,主要有以下几种:

  • 指定版本:比如 1.2.2 ,遵循“大版本.次要版本.小版本”的格式规定,安装时只安装指定版本。
  • 波浪号(tilde)+指定版本:比如 ~1.2.2 ,表示安装 1.2.x 的最新版本(不低于1.2.2),但是不安装 1.3.x,也就是说安装时不改变大版本号和次要版本号。
  • 插入号(caret)+指定版本:比如 ?1.2.2,表示安装 1.x.x 的最新版本(不低于 1.2.2),但是不安装 2.x.x,也就是说安装时不改变大版本号。需要注意的是,如果大版本号为 0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。
  • latest:安装最新版本。
当我们使用比如 npm install package -save 安装一个依赖包时,版本是插入号形式。这样每次重新安装依赖包 npm install 时”次要版本“和“小版本”是会拉取最新的。一般的,主版本不变的情况下,不会带来核心功能变动,API 应该兼容旧版。
package-lock.json
在 npm5 版本后,运行 npm intall 会生成一个新文件 package-lock.json。
当项目中已有 package-lock.json 文件,在安装项目依赖时,将以该文件为主进行解析安装指定版本依赖包,而不是使用 package.json 来解析和安装模块。因为 package-lock 为每个模块及其每个依赖项指定了版本,位置和完整性哈希,所以它每次创建的安装都是相同的。 无论你使用什么设备,或者将来安装它都无关紧要,每次都应该给你相同的结果。
yarn
yarn 的出现主要目标是解决由于语义版本控制而导致的 npm 安装的不确定性问题。
每个 yarn 安装都会生成一个类似于 npm-shrinkwrap.json 的 yarn.lock 文件,而且它是默认创建的。除了常规信息之外,yarn.lock 文件还包含要安装的内容的校验和,以确保使用的库的版本相同。
yarn 的主要优化
  • 并行安装:无论 npm 还是 yarn 在执行包的安装时,都会执行一系列任务。npm 是按照队列执行每个 package,也就是说必须要等到当前 package 安装完成之后,才能继续后面的安装。而 yarn 是同步执行所有任务,提高了性能。
  • 离线模式:如果之前已经安装过一个软件包,用 yarn 再次安装时之间从缓存中获取,就不用像 npm 那样再从网络下载了。
  • 安装版本统一:为了防止拉取到不同的版本,yarn 有一个锁定文件 (lock file) 记录了被安装上的模块的版本号。每次只要新增了一个模块,yarn 就会创建(或更新)yarn.lock 这个文件。这么做就保证了,每一次拉取同一个项目依赖时,使用的都是一样的模块版本。
  • 更好的语义化: yarn 改变了一些 npm 命令的名称,比如 yarn add/remove,比 npm 原本的 install/uninstall 要更清晰。
安装依赖树流程
  1. 执行工程自身 preinstall
    如果定义了 preinstall 钩子此时会被执行。
  2. 确定首层依赖
    首先需要做的是确定项目中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。
    项目本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。
  3. 下载模块
    下载模块是一个递归的过程,分为以下几步:
    • 获取模块版本信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是语义化版本。
      如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接用即可,如果没有则从仓库获取。如 package.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
    • 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
    • 查找该模块依赖,如果有依赖则回到第 1 步,如果没有则停止。
  4. 模块扁平化(dedupe)。
    上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。yarn 和从 npm5 开始默认加入了一个 dedupe 的过程,它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。
重复模块
它指的是模块名相同且语义化兼容。每个语义化版本都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。
  1. 安装模块
    这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。
  2. 执行项目自身生命周期
    当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)
安装依赖实例
插件 htmlparser2@^3.10.1dom-serializer@^0.2.2 都有使用了 entities 依赖包,不过使用的版本不同,同时我们自己安装一个版本的 entities 包。具体如下:
--htmlparser2@^3.10.1 |--entities@^1.1.1--dom-serializer@^0.2.2 |--entities@^2.0.0--entities@^2.1.0

通过 npm install 安装后,生成的 package-lock.json 文件内容和它的 node_modules 目录结构:
npm-version
可以发现:
dom-serializer@^0.2.2 的依赖包 entities@^2.0.0 和我们自己安装的 entities@^2.1.0 被实际安装成 entities@^2.2.0,并放在 node_modules 的第一层。因为这两个版本的semver 范围相同,又先被遍历,所有会被合并安装在第一层;
htmlparser2@^3.10.1 的依赖包 entities@^1.1.1 被实际安放在 dom-serializer 包的 node_modules 中,并且和 package-lock.json 描述结构保持一致。
通过 yarn 安装后,生成的 yarn.lock 文件内容和它的 node_modules 目录结构:
yarn-version
可以发现与 npm install 不同的是:
yarn.lock 中所有依赖描述都是扁平化的,即没有依赖描述的嵌套关系;
在 yarn.lock 中, 相同名称版本号不同的依赖包,如果 semver 范围相同会被合并,否则,会存在多个版本描述。
【npm 与 yarn 安装包不一致】参考: npm/yarn lock真香

    推荐阅读