很多人上來就刪除的 package-lock-json,還有這麼多你不知道的
作者:wuwhs
原文:https://segmentfault.com/a/1190000039684460
- 前言
看完本文,你將從整體瞭解依賴版本鎖定原理,package-lock.json
或 yarn.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
[1] 查看源碼發現該庫依賴包很少,大部分是原生實現。再看 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
文件用到的 preset
是 env
。
2.0.7
版本用的是 @bable/env
,將 babel
更新到了 7!
問題基本定位到了,這裏就順便給作者提了一個 issues
[2]。
事件二:依賴包的新版插件 bug
一直正常使用的 braft-editor
優秀的富文本編輯器插件,最近在其他小夥伴電腦或者在我本地電腦重新部署項目,啓動後發現 toHtml()
方法獲取富文本 html
內容總是空!
歷史版本是正常的,猜測可能又是版本更新造成。同樣的,去官方庫 braft-editor[3] 看看 issues
別人有沒有遇到同樣的問題。果然這次有,原因是它的依賴包 draft-js
[4] 更新後的問題,具體的看這個 issues
[5]。
這個是由於插件的依賴包更新出現的問題,直接去鎖定當前插件沒有作用,不會對它的依賴包產生約束(依賴包還是會去下載最新版本的包)。我的臨時解決辦法是嘗試將版本回退到後一個版本並鎖定。這樣做的原因是回退版本的依賴包版本肯定會低於現在的,之前的版本是正常的。
經驗教訓
其實這兩起事件是同一個誘因導致的:沒有鎖定當前項目依賴樹模塊的版本。下面就來探究一下依賴包的版本管理。
- 語義化版本(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[6] ,然後就演變成了 5.1.0
版本後的規則。
5.1.0
版本後:
當 package.json
中的依賴項有新版本時,npm install
會無視 package-lock.json
去下載新版本的依賴項並且更新 package-lock.json
。針對這種安裝策略,又有人提出了一個 issue[7] 參考 npm
貢獻者 iarna
的評論,得出 5.4.2
版本後的規則。
5.4.2
版本後:
如果只有一個 package.json
文件,運行 npm install
會根據它生成一個 package-lock.json
文件,這個文件相當於本次 install
的一個快照,它不僅記錄了 package.json
指明的直接依賴的版本,也記錄了間接依賴的版本。
如果 package.json
的 semver-range version
和 package-lock.json
中版本兼容 (package-lock.json
版本在 package.json
指定的版本範圍內),即使此時 package.json
中有新的版本,執行 npm install
也還是會根據 package-lock.json
下載。
如果手動修改了 package.json
的 version ranges
,且和 package-lock.json
中版本不兼容,那麼執行 npm install
時 package-lock.json
將會更新到兼容 package.json
的版本。
- yarn
yarn
的出現主要目標是解決上面描述的由於語義版本控制而導致的 npm
安裝的不確定性問題。雖然可以使用 npm shrinkwrap
來實現可預測的依賴關係樹,但它並不是默認選項,而是取決於所有的開發人員知道並且啓用這個選項。yarn
採取了不同的做法。每個 yarn
安裝都會生成一個類似於npm-shrinkwrap.json
的 yarn.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
要更清晰。
- 安裝依賴樹流程
-
執行工程自身
preinstall
。當前npm
工程如果定義了preinstall
鉤子此時會被執行。 -
確定首層依賴。模塊首先需要做的是確定工程中的首層依賴,也就是
dependencies
和devDependencies
屬性中直接指定的模塊(假設此時沒有添加npm install
參數)。工程本身是整棵依賴樹的根節點,每個首層依賴模塊都是根節點下面的一棵子樹,npm
會開啓多進程從每個首層依賴模塊開始逐步尋找更深層級的節點。 -
獲取模塊。獲取模塊是一個遞歸的過程,分爲以下幾步:
-
獲取模塊信息。在下載一個模塊之前,首先要確定其版本,這是因爲
package.json
中往往是semantic version
(semver
,語義化版本)。此時如果版本描述文件(npm-shrinkwrap.json
或package-lock.json
)中有該模塊信息直接拿即可,如果沒有則從倉庫獲取。如package.json
中某個包的版本是^1.1.0
,npm
就會去倉庫中獲取符合1.x.x
形式的最新版本。 -
獲取模塊實體。上一步會獲取到模塊的壓縮包地址(
resolved
字段),npm
會用此地址檢查本地緩存,緩存中有就直接拿,如果沒有則從倉庫下載。 -
查找該模塊依賴,如果有依賴則回到第
1
步,如果沒有則停止。
-
模塊扁平化(
dedupe
)。上一步獲取到的是一棵完整的依賴樹,其中可能包含大量重複模塊。比如A
模塊依賴於loadsh
,B
模塊同樣依賴於lodash
。在npm3
以前會嚴格按照依賴樹的結構進行安裝,因此會造成模塊冗餘。yarn
和從npm5
開始默認加入了一個dedupe
的過程。它會遍歷所有節點,逐個將模塊放在根節點下面,也就是node-modules
的第一層。當發現有重複模塊時,則將其丟棄。這裏需要對重複模塊進行一個定義,它指的是模塊名相同且semver
兼容。每個semver
都對應一段版本允許範圍,如果兩個模塊的版本允許範圍存在交集,那麼就可以得到一個兼容版本,而不必版本號完全一致,這可以使更多冗餘模塊在dedupe
過程中被去掉。 -
安裝模塊。這一步將會更新工程中的
node_modules
,並執行模塊中的生命週期函數(按照preinstall
、install
、postinstall
的順序)。 -
執行工程自身生命週期。當前
npm
工程如果定義了鉤子此時會被執行(按照install
、postinstall
、prepublish
、prepare
的順序)。 -
舉例說明
插件 htmlparser2@^3.10.1
和 dom-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
目錄結構:
可以發現:
-
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
目錄結構:
可以發現與 npm install
不同的是:
-
yarn.lock
中所有依賴描述都是扁平化的,即沒有依賴描述的嵌套關係; -
在
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
[8] 工具,快捷操作設置代理。
全局安裝
$ 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
然而,設置這些全局代理可能還是不能滿足下載一些特定依賴包(在沒有 VPN
情況下),比如:node-sass
、puppeteer
、chromedriver
、electron
等。可以通過 .npmrc
文件設置具體依賴包的國內鏡像。該文件在項目 npm install
時會被加載讀取,優先級高於 npm
全局設置。
registry=https://registry.npm.taobao.org/
sass_binary_site=http://npm.taobao.org/mirrors/node-sass
chromedriver_cdnurl=http://npm.taobao.org/mirrors/chromedriver
electron_mirror=http://npm.taobao.org/mirrors/electron/ npm install -g electron
puppeteer_download_host=http://npm.taobao.org/mirrors/chromium-browser-snapshots/
- 總結
項目在以後重新構建,由於依賴樹中有版本更新,造成意外事故是不可避免的,究其原因是整個依賴樹版本沒有鎖死。解決方案分爲如下四種:
-
package.json
中固定版本。注意:僅能鎖定當前依賴包版本,不能控制整棵依賴樹版本。 -
npm+npm-shrinkwrap.json
。 -
npm+package-lock.json
。 -
yarn+yarn-lock.json
。
根據自身情況選擇~
參考資料
[1]
clipboard
: https://github.com/zenorocha/clipboard.js
[2]
issues
: https://github.com/zenorocha/clipboard.js/issues/745
[3]
braft-editor: https://github.com/margox/braft-editor
[4]
draft-js
: https://github.com/facebook/draft-js
[5]
issues
: https://github.com/margox/braft-editor/issues/847
[6]
issue: https://github.com/npm/npm/issues/16866
[7]
issue: https://github.com/npm/npm/issues/17979
[8]
nrm
: https://www.npmjs.com/package/nrm
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/heg7oZRjTHKilE0R0e-nvw