來(lái)了新同事,拉同一個(gè)項(xiàng)目到本地安裝依賴之后跑不起來(lái),但是其他三臺(tái)電腦運(yùn)行著都沒(méi)問(wèn)題。接下來(lái)就是逐步定位問(wèn)題,首先排除了代碼問(wèn)題,因?yàn)樽钚麓a在其他同事不同系統(tǒng)的電腦上都沒(méi)正常運(yùn)行,進(jìn)過(guò)百度/谷歌/github issue搜索報(bào)錯(cuò)、反復(fù)重新拉項(xiàng)目、重啟電腦、重裝環(huán)境、重裝系統(tǒng)等一天的的操作之后,終于定位到大概是依賴包版本更新的問(wèn)題。。
項(xiàng)目里一共有60+依賴,主要的幾個(gè)依賴都是手動(dòng)鎖死版本的,但有個(gè)lozad沒(méi)有鎖死,而且前段時(shí)間應(yīng)該是發(fā)布了次版本更新,跟nuxt的兼容有問(wèn)題所以報(bào)了錯(cuò)。跟運(yùn)行正常項(xiàng)目的node_moduels中l(wèi)ozad版本對(duì)比,改了之后果然項(xiàng)目就跑起來(lái)了。
npm包管理原理
在思考解決方案前,首先了解下npm包管理及依賴版本管理的原理。這些都是通過(guò)package.json文件實(shí)現(xiàn)的
當(dāng)你使用npm安裝一個(gè)包(并保存它)或者更新一個(gè)包的時(shí)候,package.json里就自動(dòng)添加了一條信息,包括包名和其版本。npm默認(rèn)安裝最新版本,然后在其版本號(hào)之前添加一個(gè)符號(hào)。比如1.2.12,它表明最低應(yīng)使用1.2.12版本。并且在這之上,擁有相同大版本號(hào)的任何版本都是OK的。畢竟小版本和bugfix版本不會(huì)對(duì)使用造成任何影響,所以用任何相同大版本的更高級(jí)版本都很安全。
- 符號(hào)
^:表示主版本固定的情況下,可更新最新版。例如:vuex: "^3.1.3",3.1.3及其以上的3.x.x都是滿足的。 - 符號(hào)
~:表示次版本固定的情況下,可更新最新版。如:vuex: "~3.1.3",3.1.3及其以上的3.1.x都是滿足的。 - 無(wú)符號(hào):無(wú)符號(hào)表示固定版本號(hào),例如:vuex: "3.1.3",此時(shí)一定是安裝3.1.3版本。
舉例:
"^1.2.3": 大于等于 1.2.3 且小于 2.0.0版本
"^0.3.4": 大于等于 0.3.4 且小于 0.4.0版本
"^0.0.6": 大于等于 0.0.6 且小于 0.0.7版本
版本依賴為什么需要鎖定
沒(méi)有版本鎖定的情況下,在執(zhí)行每次npm i的時(shí)候,對(duì)應(yīng)的版本前都有個(gè) ^ 符號(hào)。也就是未固定版本的依賴如果有了次版本更新或者修訂版本更新,會(huì)自動(dòng)安裝對(duì)應(yīng)的最新版。
在這種情況下,你再次install時(shí)安裝的包的版本可能與前次不一樣,具體的,你可以到package-lock.json中查看實(shí)際的包版本。
例如:A新建了一個(gè)項(xiàng)目,生成了上面這份package.json文件,但A安裝依賴的時(shí)間比較早,此時(shí)packageA的最新版本是2.1.0,該版本與代碼兼容,沒(méi)有出現(xiàn)bug。后來(lái)B克隆了A的項(xiàng)目,在安裝依賴時(shí)packageA的最新版本是2.2.0,那么根據(jù)語(yǔ)義npm會(huì)去安裝2.2.0的版本,但2.2.0版本的API可能發(fā)生了改動(dòng),導(dǎo)致代碼出現(xiàn)bug。
這就是package.json會(huì)帶來(lái)的問(wèn)題,同一份package.json在不同的時(shí)間和環(huán)境下安裝會(huì)產(chǎn)生不同的結(jié)果。
理論上這個(gè)問(wèn)題是不應(yīng)該出現(xiàn)的,因?yàn)閚pm作為開(kāi)源世界的一部分,也遵循一個(gè)發(fā)布原則:相同大版本號(hào)下的新版本應(yīng)該兼容舊版本。即2.1.0升級(jí)到2.2.0時(shí)API不應(yīng)該發(fā)生變化。但很多開(kāi)源庫(kù)的開(kāi)發(fā)者并沒(méi)有嚴(yán)格遵守這個(gè)發(fā)布原則,導(dǎo)致了上面的這個(gè)問(wèn)題。
為了在不同的環(huán)境下生成相同的node_modules,引入版本依賴鎖定就尤為必要了。
npm5.0之前可以通過(guò)npmshrinkwrap實(shí)現(xiàn)。通過(guò)運(yùn)行 npm shrinkwrap,會(huì)在當(dāng)前目錄下生成一個(gè) npm-shrinkwrap.json文件,里面包含了通過(guò)當(dāng)前 node_modules 計(jì)算出的模塊的依賴樹及版本。只要目錄下有 npm-shrinkwrap.json ,則運(yùn)行 npm install 的時(shí)候會(huì)優(yōu)先使用 npm-shrinkwrap.json 進(jìn)行安裝,沒(méi)有則使用 package.json 進(jìn)行安裝。
在npm5.0之后,npm自帶了package-lock.json文件,通過(guò)npm安裝依賴,每當(dāng)node_modules目錄或者package.json發(fā)生變化時(shí)就會(huì)生成或者更新這個(gè)文件。不同版本有有些不同:
-
npm 5.0.x版本:不管package.json中依賴是否有更新,npm i都會(huì)根據(jù)package-lock.json下載。針對(duì)這種安裝策略,有人提出了這個(gè)issue - #16866 ,然后就演變成了5.1.0版本后的規(guī)則。 -
5.1.0版本后:當(dāng)package.json中的依賴項(xiàng)有新版本時(shí),npm install會(huì)無(wú)視package-lock.json去下載新版本的依賴項(xiàng)并且更新package-lock.json。針對(duì)這種安裝策略,又有人提出了一個(gè)issue - #17979,參考 npm 貢獻(xiàn)者 iarna 的評(píng)論,得出5.4.2版本后的規(guī)則。 -
5.4.2版本后:
如果只有一個(gè)package.json文件,運(yùn)行npm i會(huì)根據(jù)它生成一個(gè)package-lock.json文件,這個(gè)文件相當(dāng)于本次install的一個(gè)快照,它不僅記錄了package.json指明的直接依賴的版本,也記錄了間接依賴的版本。
如果package.json的semver-range version和package-lock.json中版本兼容(package-lock.json版本在package.json指定的版本范圍內(nèi)),即使此時(shí)package.json中有新的版本,執(zhí)行npm i也還是會(huì)根據(jù)package-lock.json下載 - 實(shí)踐場(chǎng)景1。
如果手動(dòng)修改了package.json的version ranges,且和package-lock.json中版本不兼容,那么執(zhí)行npm i時(shí)package-lock.json將會(huì)更新到兼容package.json的版本 - 實(shí)踐場(chǎng)景2。
如果需要更新依賴依賴包版本,需要手動(dòng)修改package.json中對(duì)應(yīng)的版本或者指定依賴的版本號(hào)安裝:npm i xxx@x.x.x。
更換/管理npm源
首先要說(shuō)的是,很多同學(xué)可能習(xí)慣使用cnpm,因?yàn)榘惭b速度確實(shí)比npm快不少,但在版本依賴鎖定方案中,最基礎(chǔ)的一條就是:不要使用cnpm,因?yàn)?strong>cnpm,是不支持依賴版本鎖定的。也即是說(shuō),無(wú)論你的項(xiàng)目中有package-lock.json、npm-shrinkwrap.json還是yarn-lock.json文件,執(zhí)行cnpm i安裝依賴的時(shí)候他們都只是擺設(shè),都只會(huì)根據(jù)package.json文件進(jìn)行安裝。所以通過(guò)cnpm安裝依賴是不能避免上面問(wèn)題的。而且有很多網(wǎng)友反饋cnpm會(huì)有依賴包丟失的問(wèn)題。
但是使用npm避不開(kāi)的一個(gè)問(wèn)題就是安裝速度,實(shí)在太慢了。這里我們可以通過(guò)手動(dòng)更換npm源和nrm的方式實(shí)現(xiàn)使用npm命令的同時(shí),依然享受cnpm的安裝速度。
手動(dòng)更換npm源
設(shè)置npm源: npm config set registry [url]
查看確認(rèn): npm config get registry

使用nrm
安裝nrm
npm i nrm -g
查看可選的源
nrm ls

其中,帶*的是當(dāng)前使用的源,上面的輸出表明當(dāng)前源是官方源。
切換到某個(gè)源:nrm use xx
例如切換到淘寶源:nrm use taobao
增加源(添加企業(yè)內(nèi)部的私有源或者其他源):nrm add [registryName] [url]
刪除源:nrm del <registryName>
測(cè)試某個(gè)源的相應(yīng)時(shí)間:nrm test taobao
依賴版本鎖定方案
大概有這么幾條方案:
-
package.json中固定版本 -
npm+package-lock.json -
npm+npm-shrinkwrap.json -
yarn+yarn-lock.json
package.json中固定版本
最直接的,可以在package.json中寫入固定版本號(hào),也就是去掉版本號(hào)前面的~或者^,或者安裝的時(shí)候加上--save-exact參數(shù)。但這樣只能鎖定最外一層的依賴,也就是這個(gè)依賴本身的其他依賴版本是不受控制的。所以不太推薦。
npm+package-lock.json
第一次npm i的時(shí)候會(huì)根據(jù)當(dāng)前node_modules目錄生成一個(gè)固定版本號(hào)的package-lock.json文件,后面如果安裝新增的依賴,會(huì)自動(dòng)更新這個(gè)文件。但如果需要更新當(dāng)前某個(gè)依賴的版本號(hào)并鎖定到package-lock.josn中,需要手動(dòng)修改package.json中對(duì)應(yīng)的版本或者指定依賴的版本號(hào)安裝:npm i xxx@x.x.x。
npm+npm-shrinkwrap.json
這種方式鎖定版本,每次依賴有新增或者版本更新之后,要手動(dòng)智行npm shrinkwrap來(lái)生成或者更新版本鎖定文件。
yarn+yarn-lock.json
yarn-lock.json與package-lock.josn原理類似,習(xí)慣用yarn命令的可以采取這種方式。
注:
如果項(xiàng)目中同時(shí)存在package-lock.json和npm-shrinkwrap.json,npm5 只會(huì)更新它,而不會(huì)生成 package-lock.json。
yarn 的鎖定版本文件叫 yarn.lock,目前發(fā)布平臺(tái)是支持的,不過(guò)最好保證項(xiàng)目中只有一個(gè)版本鎖定文件,package-lock.json、npm-shrinkwrap.json 或者 yarn.lock 二選一,防止出現(xiàn)安裝結(jié)果和預(yù)想不一致的情況。
npm和cnpm的區(qū)別
-
cnpm i不受package-lock.json影響,只會(huì)根據(jù)package.json進(jìn)行下載。 -
cnpm i xxx@xxx不會(huì)更新到package-lock.json中去。 -
npm i xxx@xxx會(huì)跟新到package-lock.json中去
限時(shí)秒殺阿里云服務(wù)器ECS、云數(shù)據(jù)庫(kù)MySQL、對(duì)象存儲(chǔ)OSS等多種代金券
參考
npm5.0新增package-lock.json文件挖的坑
前端核心工具:yarn、npm、cnpm三者如何優(yōu)雅的在一起使用 ?
npm的package.json和package-lock.json更新策略