原文地址:https://tech.meituan.com/npm-shrinkwrap.html
在一次項(xiàng)目開發(fā)中遇到了這個(gè)情況:我從倉庫把項(xiàng)目拉到本地,結(jié)果構(gòu)建出來發(fā)現(xiàn)有問題,但是在同事那里就運(yùn)行正常。無疑是因?yàn)橐蕾嚢陌姹静煌瑢?dǎo)致的,在Google時(shí)發(fā)現(xiàn)了這篇文章,下邊是原文。
使用 npm shrinkwrap 來管理項(xiàng)目依賴
敬威 ·2015-10-23 17:30
管理依賴是一個(gè)復(fù)雜軟件開發(fā)過程中必定會(huì)遇到的問題。
在Node.js項(xiàng)目開發(fā)的時(shí)候,我們也經(jīng)常需要安裝和升級(jí)對(duì)應(yīng)的依賴。雖然 npm 以及語意化的版本號(hào) (semantic versioning, semver) 讓開發(fā)過程中依賴的獲取和升級(jí)變得非常容易, 但不嚴(yán)格的版本號(hào)限制,也帶來了版本號(hào)的不確定性。主要的問題可能有三個(gè):
npm 建議使用 semver 的應(yīng)用程序版本,但這也完全依賴第三方包遵守這一規(guī)則。如果你依賴于的包不遵循 semver ,或者依賴的包的新版本有重大更改(而你使用了 ^
的寬泛版本安裝),這潛在可能是會(huì)導(dǎo)致問題的。
另一個(gè)問題的出現(xiàn)是由于 npm 安裝依賴的機(jī)制。npm 的安裝包是有層次結(jié)構(gòu)的,手動(dòng)控制要安裝的軟件包的版本號(hào)可以實(shí)現(xiàn),但是你只能在 package.json 使用精確的版本號(hào)控制你的直接依賴包,但那些多層以上的依賴就沒辦法控制了;一個(gè)第三方包不嚴(yán)謹(jǐn)?shù)陌姹疽蕾嚿赡芷茐哪愕囊蕾嚬芾怼?/p>
在開發(fā)階段執(zhí)行得到的版本,和后續(xù)部署時(shí)得到的可能是不一致的,更不可控的是,你依賴的第三方包也有這樣的情況會(huì)導(dǎo)致潛在的上線風(fēng)險(xiǎn)。
如果要控制上線的風(fēng)險(xiǎn),我們就必需要解決這個(gè)問題,這時(shí)候,就需要使用 npm shrinkwrap
這個(gè)命令來解決問題。
介紹 shrinkwrap
npm shrinkwrap
可以按照當(dāng)前項(xiàng)目 node_modules 目錄內(nèi)的安裝包情況生成穩(wěn)定的版本號(hào)描述。
比方說,有一個(gè)包 A
{ "name": "A", "version": "0.1.0", "dependencies": { "B": "<0.1.0" } }
還有一個(gè)包 B
{ "name": "B", "version": "0.0.1", "dependencies": { "C": "<0.1.0" } }
以及包 C
{ "name": "C", "version": "0.0.1" }
你的項(xiàng)目只依賴于 A,于是 npm install
會(huì)得到這樣的目錄結(jié)構(gòu)
A@0.1.0 -- B@0.0.1-- C@0.0.1
這時(shí)候,B@0.0.2 發(fā)布了,這時(shí)候在一個(gè)新的環(huán)境下執(zhí)行 npm install
將得到
A@0.1.0 -- B@0.0.2-- C@0.0.1
這時(shí)候兩次安裝得到的版本號(hào)就不一致了。而通過 shrinkwrap 命令,我們可以保證在所有環(huán)境下安裝得到穩(wěn)定的結(jié)果。
在項(xiàng)目引入新包的時(shí)候,或者 A 的開發(fā)者執(zhí)行一下 npm shrinkwrap
,可以在項(xiàng)目根目錄得到一個(gè) npm-shrinkwrap.json 文件。
這個(gè)文件內(nèi)容如下
{ "name": "A", "version": "0.1.0", "dependencies": { "B": { "version": "0.0.1", "dependencies": { "C": { "version": "0.0.1" } } } } }
shrinkwrap 命令根據(jù)目前安裝在node_modules的文件情況鎖定依賴版本。在項(xiàng)目中執(zhí)行 npm install
的時(shí)候,npm 會(huì)檢查在根目錄下有沒有 npm-shrinkwrap.json 文件,如果 shrinkwrap 文件存在的話,npm 會(huì)使用它(而不是 package.json)來確定安裝的各個(gè)包的版本號(hào)信息。
這樣一來,在安裝時(shí)候確定的所有版本信息會(huì)穩(wěn)定的固化在 shrinkwrap 里。無論是A,B 和 C中的版本如何變化,或者它們的 package.json 文件如何修改,你始終能保證,在你項(xiàng)目中執(zhí)行 npm install
的到的版本號(hào)時(shí)穩(wěn)定的。
在開發(fā)中使用 shrinkwrap
在開發(fā)過程中,引入一個(gè)新包的流程如下
npm install PACKAGE_NAME@VERSION --save
獲取特定版本的包
測(cè)試功能
測(cè)試功能正常后,執(zhí)行 npm shrinkwrap
把依賴寫入 shrinkwrap 文件
在代碼倉庫中提交 shrinkwrap / package.json 描述
升級(jí)一個(gè)包的流程應(yīng)該是這樣
npm outdated
獲取項(xiàng)目所有依賴的更新信息
npm install PACKAGE_NAME@VERSION --save
獲取特定版本的包
測(cè)試功能
測(cè)試功能正常后,執(zhí)行 npm shrinkwrap
把依賴寫入 shrinkwrap 文件
在代碼倉庫中提交 shrinkwrap / package.json 描述
刪除一個(gè)包的流程如下
npm uninstall PACKAGE_NAME --save
刪除這個(gè)包
測(cè)試功能
測(cè)試功能正常后,執(zhí)行 npm shrinkwrap
把更新的依賴寫入 shrinkwrap 文件
在代碼倉庫中提交 shrinkwrap / package.json 描述
比一般的安裝多了一步手工生成 shrinkwrap 文件。在實(shí)際工作中,有時(shí)候我們會(huì)忘記這一步,導(dǎo)致上線時(shí)候沒有獲取到依賴包的特定版本。
介紹 npm-shrinkwrap-install
去年我引入 shrinkwrap 工作流的時(shí)候,npm 官方的 shrinkwrap 命令還有很多問題,比如
在生成版本描述的時(shí)候不會(huì)忽略 devDependencies 和 optionalDependencies
不會(huì)檢查 package.json 和 shrinkwrap 文件的差異
不會(huì)刪除 from 字段(這個(gè)字段沒有用),導(dǎo)致在 diff 時(shí)候會(huì)出現(xiàn)多余的信息
所以我寫了一個(gè) bin/shrinkwrap 腳本。這個(gè)腳本會(huì)自動(dòng)對(duì)比 package.json 和 npm-shrinkwrap.json 文件的區(qū)別,獲取需要更新的版本,然后對(duì)相關(guān)信息進(jìn)行更新。(更新:現(xiàn)在的 npm shrinkwrap 已經(jīng)修復(fù)了很多的問題,但 from 字段有時(shí)候仍然有些小問題。)
當(dāng)時(shí)為了忽略 devDependencies 和 optionalDependencies,我會(huì)執(zhí)行 npm prune
刪除額外的包之后才生成版本描述,然后再把其它的包裝回來,導(dǎo)致腳本執(zhí)行時(shí)間有點(diǎn)長。
后面 uber 發(fā)布了 npm-shrinkwrap 工具,可以更高效的生成版本描述??上н@個(gè)包不支持 scoped package。我花了一點(diǎn)時(shí)間 patch 了這個(gè)工具,因?yàn)樗鼈兊陌l(fā)版太慢,所以我發(fā)布了一份 @th507/npm-shrinkwrap,可以支持 scoped package。
在上面這個(gè)包的基礎(chǔ)上,我還寫了另外一個(gè)小工具,叫做 npm-shrinkwrap-install,它可以無縫替換 npm install
的執(zhí)行過程,讓 shrinkwrap 文件的生成變得更自動(dòng)。
安裝
$ npm install npm-shrinkwrap-install
安裝完成之后,有如下命令可以使用
安裝依賴的命令npm-installnpm-i
刪除依賴的命令npm-unisntallnpm-unnpm-removenpm-rmnpm-r
手工生成 shrinkwrap 描述npm-shrinkwrap
在開發(fā)中使用 npm-install
引入新依賴包
npm-install PACKAGE_NAME@VERSION --save
獲取特定版本的包
測(cè)試功能
在代碼倉庫中提交 shrinkwrap / package.json 描述
npm-install 在運(yùn)行時(shí)會(huì)對(duì) package.json 中的依賴做校驗(yàn),如果你直接修改 package.json 文件,或者是指定了一個(gè)非嚴(yán)格的版本號(hào),在運(yùn)行的時(shí)候都會(huì)做更新檢查,防止遺漏。
值得注意的是,因?yàn)?npm-install 會(huì)進(jìn)行依賴對(duì)比和校驗(yàn),在安裝新包的時(shí)候需要帶上 --save 參數(shù)。否則,在自動(dòng)更新 shrinkwrap 描述之前,腳本會(huì)自動(dòng)移除多余的依賴包,導(dǎo)致你新安裝的包被刪除。
升級(jí)依賴包
npm outdated
獲取項(xiàng)目所有依賴的更新信息
npm-install PACKAGE_NAME@VERSION --save
獲取特定版本的包
測(cè)試功能
在代碼倉庫中提交 shrinkwrap / package.json 描述
package.json 文件中指定了一個(gè)非嚴(yán)格的版本號(hào)的依賴在運(yùn)行 npm-install 的時(shí)候會(huì)做自動(dòng)更新檢查,無需指定版本號(hào),如果你不希望進(jìn)行自動(dòng)更新,請(qǐng)?jiān)?package.json 中使用嚴(yán)格版本號(hào)。
刪除依賴包
npm-uninstall PACKAGE_NAME --save
刪除這個(gè)包
測(cè)試功能
在代碼倉庫中提交 shrinkwrap / package.json 描述
可以看到,在實(shí)際使用中沒有引入額外的流程,對(duì)開發(fā)者基本沒有學(xué)習(xí)的負(fù)擔(dān)。但仍然注意,不建議開發(fā)者執(zhí)行 npm update
命令更新所有的依賴。
小結(jié)
通過引入 shrinkwrap 文件,我們可以較好的管理項(xiàng)目的依賴關(guān)系,讓上線變得更輕松。需要注意的是,盡管相關(guān)工具可以幫助你減化工作流程、可靠的分發(fā)依賴描述,但工具不能取代功能測(cè)試;每次升級(jí)依賴版本之后,我們?nèi)匀粦?yīng)該進(jìn)行相關(guān)測(cè)試來確保項(xiàng)目能可靠的運(yùn)行在該環(huán)境中。
如果使用 @th507/npm-shrinkwrap 或者 npm-shrinkwrap-install 有任何問題歡迎給我提 issue。
本文沒有涉及如何優(yōu)化上線前的安裝過程,縮短依賴構(gòu)建時(shí)間,這個(gè)問題留給專門的文介紹。