作為前端工程師你真的知道 npm install 原理麼?

作為前端工程師你真的知道 npm install 原理麼?

做前端工程師得你,一定經常會使用到npm install,那你真的瞭解npm install 執行之後的流程細節麼?

嵌套結構

我們都知道,執行 npm install 後,依賴包被安裝到了 node_modules ,下面我們來具體瞭解下,npm 將依賴包安裝到 node_modules 的具體機制是什麼。

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

舉個例子,我們的模塊 my-app 現在依賴了兩個模塊:buffer、ignore:

作為前端工程師你真的知道 npm install 原理麼?

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

作為前端工程師你真的知道 npm install 原理麼?

那麼,執行 npm install 後,得到的 node_modules 中模塊目錄結構就是下面這樣的。

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

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

作為前端工程師你真的知道 npm install 原理麼?

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

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

扁平結構

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

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

還是上面的依賴結構,我們在執行 npm install 後將得到下面的目錄結構:

作為前端工程師你真的知道 npm install 原理麼?


作為前端工程師你真的知道 npm install 原理麼?

此時我們若在模塊中又依賴了 [email protected] 版本:

作為前端工程師你真的知道 npm install 原理麼?

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


此時,我們在執行 npm install 後將得到下面的目錄結構:

作為前端工程師你真的知道 npm install 原理麼?


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

在當前模塊路徑下搜索

在當前模塊 node_modules 路徑下搜素

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

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

假設我們又依賴了一個包 buffer2@^5.4.3,而它依賴了包 [email protected],則此時的安裝結構是下面這樣的:


作為前端工程師你真的知道 npm install 原理麼?


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

試想一下,你的APP假設沒有依賴 [email protected] 版本,而你同時依賴了依賴不同 base64-js 版本的 buffer 和 buffer2。由於在執行 npm install 的時候,按照 package.json 裡依賴的順序依次解析,則 buffer 和 buffer2 在 package.json 的放置順序則決定了 node_modules 的依賴結構:

先依賴buffer2:


先依賴buffer:


作為前端工程師你真的知道 npm install 原理麼?


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

Lock文件

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

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

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

作為前端工程師你真的知道 npm install 原理麼?

在執行 npm install 後生成的 package-lock.json 如下:

作為前端工程師你真的知道 npm install 原理麼?

作為前端工程師你真的知道 npm install 原理麼?

作為前端工程師你真的知道 npm install 原理麼?

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

最外面的兩個屬性 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 中的依賴衝突之後,才會有這個屬性。

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


作為前端工程師你真的知道 npm install 原理麼?


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

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

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

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

不使用 lock 文件:


作為前端工程師你真的知道 npm install 原理麼?

使用 lock 文件:


作為前端工程師你真的知道 npm install 原理麼?

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

使用建議:開發系統應用時,建議把 package-lock.json 文件提交到代碼版本倉庫,從而保證所有團隊開發者以及 CI 環節可以在執行 npm install 時安裝的依賴版本都是一致的。

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

緩存

在執行 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 在執行安裝時,可以根據 package-lock.json 中存儲的 integrity、version、name 生成一個唯一的 key 對應到 index-v5 目錄下的緩存記錄,從而找到 tar包的 hash,然後根據 hash 再去找緩存的 tar包直接使用。

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

作為前端工程師你真的知道 npm install 原理麼?

然後我們將json格式化:

作為前端工程師你真的知道 npm install 原理麼?

作為前端工程師你真的知道 npm install 原理麼?

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

作為前端工程師你真的知道 npm install 原理麼?

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

npm 提供了幾個命令來管理緩存數據:

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

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

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

基於緩存數據,npm 提供了離線安裝模式,分別有以下幾種:

--prefer-offline:優先使用緩存數據,如果沒有匹配的緩存數據,則從遠程倉庫下載。

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

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

文件完整性

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

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

作為前端工程師你真的知道 npm install 原理麼?

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

整體流程


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

檢查 .npmrc 文件:優先級為:項目級的 .npmrc 文件 > 用戶級的 .npmrc 文件> 全局級的 .npmrc 文件 > npm 內置的 .npmrc 文件

檢查項目中有無 lock 文件。

無 lock 文件:

從 npm 遠程倉庫獲取包信息

根據 package.json 構建依賴樹,構建過程:

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


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

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

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

不存在緩存:

從 npm 遠程倉庫下載包

校驗包的完整性

校驗不通過:

重新下載

校驗通過:

將下載的包複製到 npm 緩存目錄

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


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

將包解壓到 node_modules

生成 lock 文件

有 lock 文件:

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

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

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

yarn

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

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

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

作為前端工程師你真的知道 npm install 原理麼?
作為前端工程師你真的知道 npm install 原理麼?

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

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

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

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



yarn 默認使用 prefer-online 模式,即優先使用網絡數據,如果網絡數據請求失敗,再去請求緩存數據。

參考

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

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

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



分享到:


相關文章: