npm install

npm install 原理分析

開門見山,npm install?大概會經過上面的幾個流程,本篇文章來講一講各個流程的實現(xiàn)細節(jié)、發(fā)展以及為何要這樣實現(xiàn)。

嵌套結構

我們都知道,執(zhí)行?npm install?后,依賴包被安裝到了?node_modules?,下面我們來具體了解下,npm?將依賴包安裝到?node_modules?的具體機制是什么。

在?npm?的早期版本,?npm?處理依賴的方式簡單粗暴,以遞歸的形式,嚴格按照?package.json?結構以及子依賴包的?package.json?結構將依賴安裝到他們各自的?node_modules?中。直到有子依賴包不在依賴其他模塊。

舉個例子,我們的模塊?my-app?現(xiàn)在依賴了兩個模塊:buffer、ignore:

{

? "name": "my-app",

? "dependencies": {

? ? "buffer": "^5.4.3",

? ? "ignore": "^5.1.4",

? }

}

ignore是一個純?JS?模塊,不依賴任何其他模塊,而?buffer?又依賴了下面兩個模塊:base64-js?、?ieee754。

{

? "name": "buffer",

? "dependencies": {

? ? "base64-js": "^1.0.2",

? ? "ieee754": "^1.1.4"

? }

}

那么,執(zhí)行?npm install?后,得到的?node_modules?中模塊目錄結構就是下面這樣的:

這樣的方式優(yōu)點很明顯,?node_modules?的結構和?package.json?結構一一對應,層級結構明顯,并且保證了每次安裝目錄結構都是相同的。

但是,試想一下,如果你依賴的模塊非常之多,你的?node_modules?將非常龐大,嵌套層級非常之深:

在不同層級的依賴中,可能引用了同一個模塊,導致大量冗余。

在?Windows?系統(tǒng)中,文件路徑最大長度為260個字符,嵌套層級過深可能導致不可預知的問題。

扁平結構

為了解決以上問題,NPM?在?3.x?版本做了一次較大更新。其將早期的嵌套結構改為扁平結構:

安裝模塊時,不管其是直接依賴還是子依賴的依賴,優(yōu)先將其安裝在?node_modules?根目錄。

還是上面的依賴結構,我們在執(zhí)行?npm install?后將得到下面的目錄結構:

此時我們若在模塊中又依賴了?base64-js@1.0.1?版本:

{

? "name": "my-app",

? "dependencies": {

? ? "buffer": "^5.4.3",

? ? "ignore": "^5.1.4",

? ? "base64-js": "1.0.1",

? }

}

當安裝到相同模塊時,判斷已安裝的模塊版本是否符合新模塊的版本范圍,如果符合則跳過,不符合則在當前模塊的?node_modules?下安裝該模塊。

此時,我們在執(zhí)行?npm install?后將得到下面的目錄結構:

對應的,如果我們在項目代碼中引用了一個模塊,模塊查找流程如下:

在當前模塊路徑下搜索

在當前模塊?node_modules?路徑下搜素

在上級模塊的?node_modules?路徑下搜索

...

直到搜索到全局路徑中的?node_modules

假設我們又依賴了一個包?buffer2@^5.4.3,而它依賴了包?base64-js@1.0.3,則此時的安裝結構是下面這樣的:

所以?npm 3.x?版本并未完全解決老版本的模塊冗余問題,甚至還會帶來新的問題。

試想一下,你的APP假設沒有依賴?base64-js@1.0.1?版本,而你同時依賴了依賴不同?base64-js?版本的?buffer?和?buffer2。由于在執(zhí)行?npm install?的時候,按照?package.json?里依賴的順序依次解析,則?buffer?和?buffer2?在 ?package.json?的放置順序則決定了?node_modules?的依賴結構:

先依賴buffer2:

先依賴buffer:

另外,為了讓開發(fā)者在安全的前提下使用最新的依賴包,我們在?package.json?通常只會鎖定大版本,這意味著在某些依賴包小版本更新后,同樣可能造成依賴結構的改動,依賴結構的不確定性可能會給程序帶來不可預知的問題。

Lock文件

為了解決?npm install?的不確定性問題,在?npm 5.x?版本新增了?package-lock.json?文件,而安裝方式還沿用了?npm 3.x?的扁平化的方式。

package-lock.json?的作用是鎖定依賴結構,即只要你目錄下有?package-lock.json?文件,那么你每次執(zhí)行?npm install?后生成的?node_modules?目錄結構一定是完全相同的。

例如,我們有如下的依賴結構:

{

? "name": "my-app",

? "dependencies": {

? ? "buffer": "^5.4.3",

? ? "ignore": "^5.1.4",

? ? "base64-js": "1.0.1",

? }

}

在執(zhí)行?npm install?后生成的?package-lock.json?如下:

{

? "name": "my-app",

? "version": "1.0.0",

? "dependencies": {

? ? "base64-js": {

? ? ? "version": "1.0.1",

? ? ? "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz",

? ? ? "integrity": "sha1-aSbRsZT7xze47tUTdW3i/Np+pAg="

? ? },

? ? "buffer": {

? ? ? "version": "5.4.3",

? ? ? "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz",

? ? ? "integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==",

? ? ? "requires": {

? ? ? ? "base64-js": "^1.0.2",

? ? ? ? "ieee754": "^1.1.4"

? ? ? },

? ? ? "dependencies": {

? ? ? ? "base64-js": {

? ? ? ? ? "version": "1.3.1",

? ? ? ? ? "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",

? ? ? ? ? "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="

? ? ? ? }

? ? ? }

? ? },

? ? "ieee754": {

? ? ? "version": "1.1.13",

? ? ? "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",

? ? ? "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="

? ? },

? ? "ignore": {

? ? ? "version": "5.1.4",

? ? ? "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz",

? ? ? "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A=="

? ? }

? }

}

我們來具體看看上面的結構:

最外面的兩個屬性?name?、version?同?package.json?中的?name?和?version?,用于描述當前包名稱和版本。

dependencies?是一個對象,對象和?node_modules?中的包結構一一對應,對象的?key?為包名稱,值為包的一些描述信息:

version:包版本 —— 這個包當前安裝在?node_modules?中的版本

resolved:包具體的安裝來源

integrity:包?hash?值,基于?Subresource Integrity?來驗證已安裝的軟件包是否被改動過、是否已失效

requires:對應子依賴的依賴,與子依賴的?package.json?中?dependencies的依賴項相同。

dependencies:結構和外層的?dependencies?結構相同,存儲安裝在子依賴?node_modules?中的依賴包。

這里注意,并不是所有的子依賴都有?dependencies?屬性,只有子依賴的依賴和當前已安裝在根目錄的 ?node_modules?中的依賴沖突之后,才會有這個屬性。

例如,回顧下上面的依賴關系:

我們在?my-app?中依賴的?base64-js@1.0.1?版本與?buffer?中依賴的?base64-js@^1.0.2?發(fā)生沖突,所以 ?base64-js@1.0.1??需要安裝在?buffer?包的?node_modules?中,對應了?package-lock.json?中?buffer?的?dependencies?屬性。這也對應了?npm?對依賴的扁平化處理方式。

所以,根據(jù)上面的分析,?package-lock.json?文件 和?node_modules?目錄結構是一一對應的,即項目目錄下存在 ?package-lock.json?可以讓每次安裝生成的依賴目錄結構保持相同。

另外,項目中使用了?package-lock.json?可以顯著加速依賴安裝時間。

我們使用?npm i --timing=true --loglevel=verbose?命令可以看到?npm install?的完整過程,下面我們來對比下使用?lock?文件和不使用?lock?文件的差別。在對比前先清理下npm?緩存。

不使用?lock?文件:

使用?lock?文件:

可見,?package-lock.json?中已經緩存了每個包的具體版本和下載鏈接,不需要再去遠程倉庫進行查詢,然后直接進入文件完整性校驗環(huán)節(jié),減少了大量網絡請求。

使用建議

開發(fā)系統(tǒng)應用時,建議把?package-lock.json?文件提交到代碼版本倉庫,從而保證所有團隊開發(fā)者以及?CI?環(huán)節(jié)可以在執(zhí)行?npm install?時安裝的依賴版本都是一致的。

在開發(fā)一個?npm包 時,你的?npm包 是需要被其他倉庫依賴的,由于上面我們講到的扁平安裝機制,如果你鎖定了依賴包版本,你的依賴包就不能和其他依賴包共享同一?semver?范圍內的依賴包,這樣會造成不必要的冗余。所以我們不應該把package-lock.json?文件發(fā)布出去(?npm?默認也不會把?package-lock.json?文件發(fā)布出去)。

緩存

在執(zhí)行?npm install?或?npm update命令下載依賴后,除了將依賴包安裝在node_modules?目錄下外,還會在本地的緩存目錄緩存一份。

通過?npm config get cache?命令可以查詢到:在?Linux?或?Mac?默認是用戶主目錄下的?.npm/_cacache?目錄。

在這個目錄下又存在兩個目錄:content-v2、index-v5,content-v2?目錄用于存儲?tar包的緩存,而index-v5目錄用于存儲tar包的?hash。

npm 在執(zhí)行安裝時,可以根據(jù)?package-lock.json?中存儲的?integrity、version、name?生成一個唯一的?key?對應到?index-v5?目錄下的緩存記錄,從而找到?tar包的?hash,然后根據(jù)?hash?再去找緩存的?tar包直接使用。

我們可以找一個包在緩存目錄下搜索測試一下,在?index-v5?搜索一下包路徑:

grep "https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz" -r index-v5

然后我們將json格式化:

{

? "key": "pacote:version-manifest:https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz:sha1-aSbRsZT7xze47tUTdW3i/Np+pAg=",

? "integrity": "sha512-C2EkHXwXvLsbrucJTRS3xFHv7Mf/y9klmKDxPTE8yevCoH5h8Ae69Y+/lP+ahpW91crnzgO78elOk2E6APJfIQ==",

? "time": 1575554308857,

? "size": 1,

? "metadata": {

? ? "id": "base64-js@1.0.1",

? ? "manifest": {

? ? ? "name": "base64-js",

? ? ? "version": "1.0.1",

? ? ? "engines": {

? ? ? ? "node": ">= 0.4"

? ? ? },

? ? ? "dependencies": {},

? ? ? "optionalDependencies": {},

? ? ? "devDependencies": {

? ? ? ? "standard": "^5.2.2",

? ? ? ? "tape": "4.x"

? ? ? },

? ? ? "bundleDependencies": false,

? ? ? "peerDependencies": {},

? ? ? "deprecated": false,

? ? ? "_resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz",

? ? ? "_integrity": "sha1-aSbRsZT7xze47tUTdW3i/Np+pAg=",

? ? ? "_shasum": "6926d1b194fbc737b8eed513756de2fcda7ea408",

? ? ? "_shrinkwrap": null,

? ? ? "bin": null,

? ? ? "_id": "base64-js@1.0.1"

? ? },

? ? "type": "finalized-manifest"

? }

}

上面的?_shasum?屬性?6926d1b194fbc737b8eed513756de2fcda7ea408?即為?tar?包的?hash,?hash的前幾位?6926?即為緩存的前兩層目錄,我們進去這個目錄果然找到的壓縮后的依賴包:

以上的緩存策略是從 npm v5 版本開始的,在 npm v5 版本之前,每個緩存的模塊在 ~/.npm 文件夾中以模塊名的形式直接存儲,儲存結構是{cache}/{name}/{version}。

npm?提供了幾個命令來管理緩存數(shù)據(jù):

npm cache add:官方解釋說這個命令主要是?npm?內部使用,但是也可以用來手動給一個指定的 package 添加緩存。

npm cache clean:刪除緩存目錄下的所有數(shù)據(jù),為了保證緩存數(shù)據(jù)的完整性,需要加上?--force?參數(shù)。

npm cache verify:驗證緩存數(shù)據(jù)的有效性和完整性,清理垃圾數(shù)據(jù)。

基于緩存數(shù)據(jù),npm 提供了離線安裝模式,分別有以下幾種:

--prefer-offline:優(yōu)先使用緩存數(shù)據(jù),如果沒有匹配的緩存數(shù)據(jù),則從遠程倉庫下載。

--prefer-online:優(yōu)先使用網絡數(shù)據(jù),如果網絡數(shù)據(jù)請求失敗,再去請求緩存數(shù)據(jù),這種模式可以及時獲取最新的模塊。

--offline:不請求網絡,直接使用緩存數(shù)據(jù),一旦緩存數(shù)據(jù)不存在,則安裝失敗。

文件完整性

上面我們多次提到了文件完整性,那么什么是文件完整性校驗呢?

在下載依賴包之前,我們一般就能拿到?npm?對該依賴包計算的?hash?值,例如我們執(zhí)行?npm info?命令,緊跟?tarball(下載鏈接) 的就是?shasum(hash) :

用戶下載依賴包到本地后,需要確定在下載過程中沒有出現(xiàn)錯誤,所以在下載完成之后需要在本地在計算一次文件的?hash?值,如果兩個?hash?值是相同的,則確保下載的依賴是完整的,如果不同,則進行重新下載。

整體流程

好了,我們再來整體總結下上面的流程:

檢查?.npmrc?文件:優(yōu)先級為:項目級的?.npmrc?文件 > 用戶級的?.npmrc?文件> 全局級的?.npmrc?文件 > npm 內置的?.npmrc?文件

檢查項目中有無?lock?文件。

無?lock?文件:

從?npm?遠程倉庫獲取包信息

根據(jù)?package.json?構建依賴樹,構建過程:

構建依賴樹時,不管其是直接依賴還是子依賴的依賴,優(yōu)先將其放置在?node_modules?根目錄。

當遇到相同模塊時,判斷已放置在依賴樹的模塊版本是否符合新模塊的版本范圍,如果符合則跳過,不符合則在當前模塊的?node_modules?下放置該模塊。

注意這一步只是確定邏輯上的依賴樹,并非真正的安裝,后面會根據(jù)這個依賴結構去下載或拿到緩存中的依賴包

在緩存中依次查找依賴樹中的每個包? ? ?

不存在緩存:

從?npm?遠程倉庫下載包

校驗包的完整性

校驗不通過:

重新下載????

校驗通過:

將下載的包復制到?npm?緩存目錄

將下載的包按照依賴結構解壓到?node_modules

存在緩存:將緩存按照依賴結構解壓到?node_modules

將包解壓到?node_modules

生成?lock?文件

有?lock?文件:

檢查?package.json?中的依賴版本是否和?package-lock.json?中的依賴有沖突。

如果沒有沖突,直接跳過獲取包信息、構建依賴樹過程,開始在緩存中查找包信息,后續(xù)過程相同

上面的過程簡要描述了?npm install?的大概過程,這個過程還包含了一些其他的操作,例如執(zhí)行你定義的一些生命周期函數(shù),你可以執(zhí)行?npm install package --timing=true --loglevel=verbose?來查看某個包具體的安裝流程和細節(jié)。

yarn

yarn?是在?2016?年發(fā)布的,那時?npm?還處于?V3?時期,那時候還沒有?package-lock.json?文件,就像上面我們提到的:不穩(wěn)定性、安裝速度慢等缺點經常會受到廣大開發(fā)者吐槽。此時,yarn?誕生:

上面是官網提到的?yarn?的優(yōu)點,在那個時候還是非常吸引人的。當然,后來?npm?也意識到了自己的問題,進行了很多次優(yōu)化,在后面的優(yōu)化(lock文件、緩存、默認-s...)中,我們多多少少能看到?yarn?的影子,可見?yarn?的設計還是非常優(yōu)秀的。

yarn?也是采用的是?npm v3?的扁平結構來管理依賴,安裝依賴后默認會生成一個?yarn.lock?文件,還是上面的依賴關系,我們看看?yarn.lock?的結構:

# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.

# yarn lockfile v1

base64-js@1.0.1:

? version "1.0.1"

? resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.0.1.tgz#6926d1b194fbc737b8eed513756de2fcda7ea408"

? integrity sha1-aSbRsZT7xze47tUTdW3i/Np+pAg=

base64-js@^1.0.2:

? version "1.3.1"

? resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"

? integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==

buffer@^5.4.3:

? version "5.4.3"

? resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115"

? integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==

? dependencies:

? ? base64-js "^1.0.2"

? ? ieee754 "^1.1.4"

ieee754@^1.1.4:

? version "1.1.13"

? resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"

? integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==

ignore@^5.1.4:

? version "5.1.4"

? resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf"

? integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==

可見其和?package-lock.json?文件還是比較類似的,還有一些區(qū)別就是:

package-lock.json?使用的是?json?格式,yarn.lock?使用的是一種自定義格式

yarn.lock?中子依賴的版本號不是固定的,意味著單獨又一個?yarn.lock?確定不了?node_modules?目錄結構,還需要和?package.json?文件進行配合。而?package-lock.json?只需要一個文件即可確定。

yarn?的緩策略看起來和?npm v5?之前的很像,每個緩存的模塊被存放在獨立的文件夾,文件夾名稱包含了模塊名稱、版本號等信息。使用命令?yarn cache dir?可以查看緩存數(shù)據(jù)的目錄:

yarn?默認使用?prefer-online?模式,即優(yōu)先使用網絡數(shù)據(jù),如果網絡數(shù)據(jù)請求失敗,再去請求緩存數(shù)據(jù)。

參考

https://juejin.im/post/5a6008c2f265da3e5033cd93

https://www.zhihu.com/question/305539244/answer/551386426

https://zhuanlan.zhihu.com/p/37285173

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容