npm/yarn lock真香

npm-yarn-lock

前言

看完本文,你將從總體瞭解依賴版本鎖定原理,package-lock.jsonyarn.lock 的重要性。首先要從最近接連出現兩起有關 npm 安裝 package.json 中依賴包,因爲依賴包版本更新 bug 形成項目出錯問題提及。css

事件一:新版本依賴包自己 bug

項目本地打包正常,可是線上使用 Jenkins 完成 DevOps 交付流水線打包出錯問題。報出以下錯誤:html

**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 出現的 「問題」。vue

因爲是插件自己「問題」,個人臨時解決辦法是鎖定到 2.0.4 版本,也就是 clipboard: "2.0.4",後面加上 package-lock.jsonnode

打破沙鍋問到底
既然「問題」已經定位到了 2.0.7 版本,進一步經過對比這次版本提交文件內容差別,發現 .babelrc 文件用到的 presetenvgit

clipboard2.0.6

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

clipboard2.0.7

問題基本定位到了,這裏就順便給做者提了一個 issuesnpm

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

一直正常使用的 braft-editor 優秀的富文本編輯器插件,最近在其餘小夥伴電腦或者在我本地電腦從新部署項目,啓動後發現 toHtml() 方法獲取富文本 html 內容老是空!json

歷史版本是正常的,猜想可能又是版本更新形成。一樣的,去官方庫 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 目錄結構:

npm-version

能夠發現:

  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 目錄結構:

yarn-version

能夠發現與 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

根據自身狀況選擇。見識有限,歡迎指正,謝謝點贊,完~

相關文章
相關標籤/搜索