npm install 原理分析
- 2019 年 12 月 18 日
- 筆記
這是ConardLi的第 71 篇原創,謝謝你的支援!

開門見山,npm install
大概會經過上面的幾個流程,本篇文章來講一講各個流程的實現細節、發展以及為何要這樣實現。
嵌套結構
我們都知道,執行 npm install
後,依賴包被安裝到了 node_modules
,下面我們來具體了解下,npm
將依賴包安裝到 node_modules
的具體機制是什麼。
在 npm
的早期版本, npm
處理依賴的方式簡單粗暴,以遞歸的形式,嚴格按照 package.json
結構以及子依賴包的 package.json
結構將依賴安裝到他們各自的 node_modules
中。直到有子依賴包不在依賴其他模組。
舉個例子,我們的模組 my-app
現在依賴了兩個模組: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" } }
那麼,執行 npm install
後,得到的 node_modules
中模組目錄結構就是下面這樣的:

這樣的方式優點很明顯, node_modules
的結構和 package.json
結構一一對應,層級結構明顯,並且保證了每次安裝目錄結構都是相同的。
但是,試想一下,如果你依賴的模組非常之多,你的 node_modules
將非常龐大,嵌套層級非常之深:

- 在不同層級的依賴中,可能引用了同一個模組,導致大量冗餘。
- 在
Windows
系統中,文件路徑最大長度為260個字元,嵌套層級過深可能導致不可預知的問題。
扁平結構
為了解決以上問題,NPM
在 3.x
版本做了一次較大更新。其將早期的嵌套結構改為扁平結構:
- 安裝模組時,不管其是直接依賴還是子依賴的依賴,優先將其安裝在
node_modules
根目錄。
還是上面的依賴結構,我們在執行 npm install
後將得到下面的目錄結構:


此時我們若在模組中又依賴了 [email protected]
版本:
{ "name": "my-app", "dependencies": { "buffer": "^5.4.3", "ignore": "^5.1.4", "base64-js": "1.0.1", } }
- 當安裝到相同模組時,判斷已安裝的模組版本是否符合新模組的版本範圍,如果符合則跳過,不符合則在當前模組的
node_modules
下安裝該模組。
此時,我們在執行 npm install
後將得到下面的目錄結構:


對應的,如果我們在項目程式碼中引用了一個模組,模組查找流程如下:
- 在當前模組路徑下搜索
- 在當前模組
node_modules
路徑下搜素 - 在上級模組的
node_modules
路徑下搜索 - …
- 直到搜索到全局路徑中的
node_modules
假設我們又依賴了一個包 buffer2@^5.4.3
,而它依賴了包 [email protected]
,則此時的安裝結構是下面這樣的:

所以 npm 3.x
版本並未完全解決老版本的模組冗餘問題,甚至還會帶來新的問題。
試想一下,你的APP假設沒有依賴 [email protected]
版本,而你同時依賴了依賴不同 base64-js
版本的 buffer
和 buffer2
。由於在執行 npm install
的時候,按照 package.json
里依賴的順序依次解析,則 buffer
和 buffer2
在 package.json
的放置順序則決定了 node_modules
的依賴結構:
先依賴buffer2
:

先依賴buffer
:

另外,為了讓開發者在安全的前提下使用最新的依賴包,我們在 package.json
通常只會鎖定大版本,這意味著在某些依賴包小版本更新後,同樣可能造成依賴結構的改動,依賴結構的不確定性可能會給程式帶來不可預知的問題。
Lock文件
為了解決 npm install
的不確定性問題,在 npm 5.x
版本新增了 package-lock.json
文件,而安裝方式還沿用了 npm 3.x
的扁平化的方式。
package-lock.json
的作用是鎖定依賴結構,即只要你目錄下有 package-lock.json
文件,那麼你每次執行 npm install
後生成的 node_modules
目錄結構一定是完全相同的。
例如,我們有如下的依賴結構:
{ "name": "my-app", "dependencies": { "buffer": "^5.4.3", "ignore": "^5.1.4", "base64-js": "1.0.1", } }
在執行 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
中依賴的 [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
文件:

使用 lock
文件:

可見, 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
搜索一下包路徑:
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": "[email protected]", "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": "[email protected]" }, "type": "finalized-manifest" } }
上面的 _shasum
屬性 6926d1b194fbc737b8eed513756de2fcda7ea408
即為 tar
包的 hash
, hash
的前幾位 6926
即為快取的前兩層目錄,我們進去這個目錄果然找到的壓縮後的依賴包:

以上的快取策略是從 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
) :

用戶下載依賴包到本地後,需要確定在下載過程中沒有出現錯誤,所以在下載完成之後需要在本地在計算一次文件的 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
的結構:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 [email protected]: 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
文件還是比較類似的,還有一些區別就是:
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
模式,即優先使用網路數據,如果網路數據請求失敗,再去請求快取數據。