乐趣区

关于javascript:npmyarn-lock真香

前言

看完本文,你将从整体理解依赖版本锁定原理,package-lock.jsonyarn.lock 的重要性。首先要从最近接连呈现两起无关 npm 装置 package.json 中依赖包,因为依赖包版本更新 bug 造成我的项目出错问题说起。

事件一:新版本依赖包自身 bug

我的项目本地打包失常,然而线上应用 Jenkins 实现 DevOps 交付流水线打包出错问题。报出如下谬误:

**17:15:32**  ERROR in ./node_modules/clipboard/dist/clipboard.js
**17:15:32**  Module build failed (from ./node_modules/babel-loader/lib/index.js):
**17:15:32**  Error: Couldn't find preset"@babel/env"relative to directory"/app/workspace/SIT/node_modules/clipboard"

显示谬误起因是 clipboard 插件没有装置 @babel/env 预设(preset)。显著这个是插件问题了,去官网库 clipboard 查看源码发现该库依赖包很少,大部分是原生实现。再看 issue 他人有没有呈现同样的问题,目前来看还没有人提出。以此推断可能是插件自身的 “ 问题 ” 了。

然而我本地我的项目打包失常,线上的出错,可能因为本地版本和线上版本不统一导致(某个小版本呈现的 bug)的。通过查看 package.json 配置的 clipboard: "^2.0.4",线上理论装置版本是 2.0.7,而我本地理论装置版本是 2.0.6
因而定位到 2.0.7 呈现的“问题”。

因为是插件自身“问题”,我的长期解决办法是锁定到 2.0.4 版本,也就是 clipboard: "2.0.4",前面加上 package-lock.json

打破沙锅问到底
既然“问题”曾经定位到了 2.0.7 版本,进一步通过比照此次版本提交文件内容差别,发现 .babelrc 文件用到的 presetenv

2.0.7 版本用的是 @bable/env,将 babel 更新到了 7!

问题根本定位到了,这里就顺便给作者提了一个 issues

事件二:依赖包的新版插件 bug

始终失常应用的 braft-editor 优良的富文本编辑器插件,最近在其余小伙伴电脑或者在我本地电脑重新部署我的项目,启动后发现 toHtml() 办法获取富文本 html 内容总是空!

历史版本是失常的,猜想可能又是版本更新造成。同样的,去官网库 braft-editor 看看 issues 他人有没有遇到同样的问题。果然这次有,起因是它的依赖包 draft-js 更新后的问题,具体的看这个 issues

这个是因为插件的依赖包更新呈现的问题,间接去锁定以后插件没有作用,不会对它的依赖包产生束缚(依赖包还是会去下载最新版本的包)。我的长期解决办法是尝试将版本回退到后一个版本并锁定。这样做的起因是回退版本的依赖包版本必定会低于当初的,之前的版本是失常的。

经验教训

其实这两起事件是同一个诱因导致的:没有锁定以后我的项目依赖树模块的版本。上面就来探索一下依赖包的版本治理。

语义化版本(semver)

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 应该兼容旧版,然而这在开源的世界里很难管制,尤其在简单我的项目的泛滥依赖包中难免会引入一些意想不到的 bug

npm-shrinkwrap && package-lock

npm-shrinkwrap

正是存在这每次重新安装,依赖树模块版本存在的不确定性,才有了相应的锁定版本机制。

npm5 之前能够通过 npmshrinkwrap 实现。通过运行 npm shrinkwrap,会在当前目录下生成一个 npm-shrinkwrap.json 文件,它是 package.json 中列出的每个依赖项的大型列表,应装置的特定版本,模块的地位(URI),验证模块完整性的哈希,它须要的包列表,以及依赖项列表。运行 npm install 的时候会优先应用 npm-shrinkwrap.json 进行装置,没有则应用 package.json 进行装置。

package-lock

npm5 版本后,当咱们运行 npm intall 发现会生成一个新文件 package-lock.json,内容跟下面提到的 npm-shrinkwrap.json 根本一样。

"vue-loader": {
  "version": "14.2.4",
  "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-14.2.4.tgz",
  "integrity": "sha512-bub2/rcTMJ3etEbbeehdH2Em3G2F5vZIjMK7ZUePj5UtgmZSTtOX1xVVawDpDsy021s3vQpO6VpWJ3z3nO8dDw==",
  "dev": true,
  "requires": {
    "consolidate": "^0.14.0",
    "hash-sum": "^1.0.2",
    "loader-utils": "^1.1.0",
    "lru-cache": "^4.1.1",
    "postcss": "^6.0.8",
    "postcss-load-config": "^1.1.0",
    "postcss-selector-parser": "^2.0.0",
    "prettier": "^1.16.0",
    "resolve": "^1.4.0",
    "source-map": "^0.6.1",
    "vue-hot-reload-api": "^2.2.0",
    "vue-style-loader": "^4.0.1",
    "vue-template-es2015-compiler": "^1.6.0"
  },
  "dependencies": {
    "postcss-load-config": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz",
      "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=",
      "dev": true,
      "requires": {
        "cosmiconfig": "^2.1.0",
        "object-assign": "^4.1.0",
        "postcss-load-options": "^1.2.0",
        "postcss-load-plugins": "^2.3.0"
      }
    },
  }
},

当我的项目中已有 package-lock.json 文件,在装置我的项目依赖时,将以该文件为主进行解析装置指定版本依赖包,而不是应用 package.json 来解析和装置模块。因为 package-lock 为每个模块及其每个依赖项指定了版本,地位和完整性哈希,所以它每次创立的装置都是雷同的。无论你应用什么设施,或者未来装置它都无关紧要,每次都应该给你雷同的后果。

npm5 版本下 install 规定

npm 并不是一开始就是依照现有这种规定制订的。

5.0.x 版本

不论 package.json 中依赖是否有更新,npm install 都会依据 package-lock.json 下载。针对这种装置策略,有人提出了这个 issue,而后就演变成了 5.1.0 版本后的规定。

5.1.0 版本后

package.json 中的依赖项有新版本时,npm install 会忽视 package-lock.json 去下载新版本的依赖项并且更新 package-lock.json。针对这种装置策略,又有人提出了一个 issue 参考 npm 贡献者 iarna 的评论,得出 5.4.2 版本后的规定。

5.4.2 版本后

如果只有一个 package.json 文件,运行 npm install 会依据它生成一个 package-lock.json 文件,这个文件相当于本次 install 的一个快照,它不仅记录了 package.json 指明的间接依赖的版本,也记录了间接依赖的版本。

如果 package.jsonsemver-range versionpackage-lock.json 中版本兼容(package-lock.json 版本在 package.json 指定的版本范畴内),即便此时 package.json 中有新的版本,执行 npm install 也还是会依据 package-lock.json 下载。

如果手动批改了 package.jsonversion ranges,且和 package-lock.json 中版本不兼容,那么执行 npm installpackage-lock.json 将会更新到兼容 package.json 的版本。

yarn

yarn 的呈现次要指标是解决下面形容的因为语义版本控制而导致的 npm 装置的不确定性问题。尽管能够应用 npm shrinkwrap 来实现可预测的依赖关系树,但它并不是默认选项,而是取决于所有的开发人员晓得并且启用这个选项。
yarn 采取了不同的做法。每个 yarn 装置都会生成一个相似于npm-shrinkwrap.jsonyarn.lock 文件,而且它是默认创立的。除了惯例信息之外,yarn.lock 文件还蕴含要装置的内容的校验和,以确保应用的库的版本雷同。

yarn 的次要优化

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
    以后 npm 工程如果定义了 preinstall 钩子此时会被执行。
  2. 确定首层依赖。
    模块首先须要做的是确定工程中的首层依赖,也就是 dependenciesdevDependencies 属性中间接指定的模块(假如此时没有增加 npm install 参数)。工程自身是整棵依赖树的根节点,每个首层依赖模块都是根节点上面的一棵子树,npm 会开启多过程从每个首层依赖模块开始逐渐寻找更深层级的节点。
  3. 获取模块。
    获取模块是一个递归的过程,分为以下几步:

    • 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic versionsemver,语义化版本)。此时如果版本形容文件(npm-shrinkwrap.jsonpackage-lock.json)中有该模块信息间接拿即可,如果没有则从仓库获取。如 package.json 中某个包的版本是 ^1.1.0npm 就会去仓库中获取合乎 1.x.x 模式的最新版本。
    • 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址查看本地缓存,缓存中有就间接拿,如果没有则从仓库下载。
    • 查找该模块依赖,如果有依赖则回到第 1 步,如果没有则进行。
  4. 模块扁平化(dedupe)。
    上一步获取到的是一棵残缺的依赖树,其中可能蕴含大量反复模块。比方 A 模块依赖于 loadshB 模块同样依赖于 lodash。在 npm3 以前会严格依照依赖树的构造进行装置,因而会造成模块冗余。yarn 和从 npm5 开始默认退出了一个 dedupe 的过程。它会遍历所有节点,一一将模块放在根节点上面,也就是 node-modules 的第一层。当发现有反复模块时,则将其抛弃。这里须要对反复模块进行一个定义,它指的是模块名雷同且 semver 兼容。每个 semver 都对应一段版本容许范畴,如果两个模块的版本容许范畴存在交加,那么就能够失去一个兼容版本,而不用版本号完全一致,这能够使更多冗余模块在 dedupe 过程中被去掉。
  5. 装置模块。
    这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(依照 preinstallinstallpostinstall 的程序)。
  6. 执行工程本身生命周期。
    以后 npm 工程如果定义了钩子此时会被执行(依照 installpostinstallprepublishprepare 的程序)。

举例说明

插件 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 目录构造:

能够发现:

  1. dom-serializer@^0.2.2 的依赖包 entities@^2.0.0 和咱们本人装置的 entities@^2.1.0 被理论装置成 entities@^2.2.0,并放在 node_modules 的第一层。因为这两个版本的semver 范畴雷同,又先被遍历,所有会被合并装置在第一层;
  2. htmlparser2@^3.10.1 的依赖包 entities@^1.1.1 被理论安放在 dom-serializer 包的 node_modules 中,并且和 package-lock.json 形容构造保持一致。

通过 yarn 装置后,生成的 yarn.lock 文件内容和它的 node_modules 目录构造:

能够发现与 npm install 不同的是:

  1. yarn.lock 中所有依赖形容都是扁平化的,即没有依赖形容的嵌套关系;
  2. yarn.lock 中,雷同名称版本号不同的依赖包,如果 semver 范畴雷同会被合并,否则,会存在多个版本形容。

留神 cnpm 不反对 package-lock

应用 cnpm install 时候,并不会生成 package-lock.json 文件。cnpm install 的时候,就算你我的项目中有 package-lock.json 文件,cnpm 也不会辨认,仍会依据 package.json 来装置。所以这就是为什么之前你用 npm 装置产生了 package-lock.json,前面的人用 cnpm 来装置,可能会跟你装置的依赖包不统一。

因而,尽量不要间接应用 cnpm install 装置我的项目依赖包。然而为了解决间接应用 npm install 速度慢的问题,能够设置 npm 代理解决。

// 设置淘宝镜像代理
npm config set registry https://registry.npm.taobao.org

// 查看已设置代理
npm config get registry

当然,也能够通过 nrm 工具,快捷操作设置代理。

全局装置

$ npm install -g nrm

查看已装置代理列表

$ nrm ls

* npm -----  https://registry.npmjs.org/
  yarn ----- https://registry.yarnpkg.com
  cnpm ----  http://r.cnpmjs.org/
  taobao --  https://registry.npm.taobao.org/
  nj ------  https://registry.nodejitsu.com/
  skimdb -- https://skimdb.npmjs.com/registry

切换代理

$ nrm use cnpm  //switch registry to cnpm

* Registry has been set to: http://r.cnpmjs.org/

测速

nrm test cnpm

* cnpm --- 618ms

总结

我的项目在当前从新构建,因为依赖树中有版本更新,造成意外事故是不可避免的,究其原因是整个依赖树版本没有锁死。解决方案分为如下四种:

  • package.json 中固定版本。留神:仅能锁定以后依赖包版本,不能管制整棵依赖树版本。
  • npm+npm-shrinkwrap.json
  • npm+package-lock.json
  • yarn+yarn-lock.json

依据本身状况抉择。
见识无限,欢送斧正,谢谢点赞,完~

退出移动版