npm install 原理分析
開門見山懊渡,npm install?大概會經(jīng)過上面的幾個流程,本篇文章來講一講各個流程的實現(xiàn)細(xì)節(jié)军拟、發(fā)展以及為何要這樣實現(xiàn)剃执。
嵌套結(jié)構(gòu)
我們都知道,執(zhí)行?npm install?后懈息,依賴包被安裝到了?node_modules?肾档,下面我們來具體了解下,npm?將依賴包安裝到?node_modules?的具體機(jī)制是什么辫继。
在?npm?的早期版本怒见,?npm?處理依賴的方式簡單粗暴,以遞歸的形式姑宽,嚴(yán)格按照?package.json?結(jié)構(gòu)以及子依賴包的?package.json?結(jié)構(gòu)將依賴安裝到他們各自的?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?中模塊目錄結(jié)構(gòu)就是下面這樣的:
這樣的方式優(yōu)點很明顯碘饼,?node_modules?的結(jié)構(gòu)和?package.json?結(jié)構(gòu)一一對應(yīng),層級結(jié)構(gòu)明顯悲伶,并且保證了每次安裝目錄結(jié)構(gòu)都是相同的艾恼。
但是,試想一下麸锉,如果你依賴的模塊非常之多蒂萎,你的?node_modules?將非常龐大,嵌套層級非常之深:
在不同層級的依賴中淮椰,可能引用了同一個模塊五慈,導(dǎo)致大量冗余。
在?Windows?系統(tǒng)中主穗,文件路徑最大長度為260個字符泻拦,嵌套層級過深可能導(dǎo)致不可預(yù)知的問題。
扁平結(jié)構(gòu)
為了解決以上問題忽媒,NPM?在?3.x?版本做了一次較大更新争拐。其將早期的嵌套結(jié)構(gòu)改為扁平結(jié)構(gòu):
安裝模塊時,不管其是直接依賴還是子依賴的依賴晦雨,優(yōu)先將其安裝在?node_modules?根目錄架曹。
還是上面的依賴結(jié)構(gòu),我們在執(zhí)行?npm install?后將得到下面的目錄結(jié)構(gòu):
此時我們?nèi)粼谀K中又依賴了?base64-js@1.0.1?版本:
{
? "name": "my-app",
? "dependencies": {
? ? "buffer": "^5.4.3",
? ? "ignore": "^5.1.4",
? ? "base64-js": "1.0.1",
? }
}
當(dāng)安裝到相同模塊時闹瞧,判斷已安裝的模塊版本是否符合新模塊的版本范圍绑雄,如果符合則跳過,不符合則在當(dāng)前模塊的?node_modules?下安裝該模塊奥邮。
此時万牺,我們在執(zhí)行?npm install?后將得到下面的目錄結(jié)構(gòu):
對應(yīng)的,如果我們在項目代碼中引用了一個模塊洽腺,模塊查找流程如下:
在當(dāng)前模塊路徑下搜索
在當(dāng)前模塊?node_modules?路徑下搜素
在上級模塊的?node_modules?路徑下搜索
...
直到搜索到全局路徑中的?node_modules
假設(shè)我們又依賴了一個包?buffer2@^5.4.3脚粟,而它依賴了包?base64-js@1.0.3,則此時的安裝結(jié)構(gòu)是下面這樣的:
所以?npm 3.x?版本并未完全解決老版本的模塊冗余問題蘸朋,甚至還會帶來新的問題核无。
試想一下,你的APP假設(shè)沒有依賴?base64-js@1.0.1?版本藕坯,而你同時依賴了依賴不同?base64-js?版本的?buffer?和?buffer2团南。由于在執(zhí)行?npm install?的時候噪沙,按照?package.json?里依賴的順序依次解析,則?buffer?和?buffer2?在 ?package.json?的放置順序則決定了?node_modules?的依賴結(jié)構(gòu):
先依賴buffer2:
先依賴buffer:
另外已慢,為了讓開發(fā)者在安全的前提下使用最新的依賴包曲聂,我們在?package.json?通常只會鎖定大版本霹购,這意味著在某些依賴包小版本更新后佑惠,同樣可能造成依賴結(jié)構(gòu)的改動,依賴結(jié)構(gòu)的不確定性可能會給程序帶來不可預(yù)知的問題齐疙。
Lock文件
為了解決?npm install?的不確定性問題膜楷,在?npm 5.x?版本新增了?package-lock.json?文件,而安裝方式還沿用了?npm 3.x?的扁平化的方式贞奋。
package-lock.json?的作用是鎖定依賴結(jié)構(gòu)赌厅,即只要你目錄下有?package-lock.json?文件,那么你每次執(zhí)行?npm install?后生成的?node_modules?目錄結(jié)構(gòu)一定是完全相同的轿塔。
例如特愿,我們有如下的依賴結(jié)構(gòu):
{
? "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=="
? ? }
? }
}
我們來具體看看上面的結(jié)構(gòu):
最外面的兩個屬性?name?、version?同?package.json?中的?name?和?version?勾缭,用于描述當(dāng)前包名稱和版本揍障。
dependencies?是一個對象,對象和?node_modules?中的包結(jié)構(gòu)一一對應(yīng)俩由,對象的?key?為包名稱毒嫡,值為包的一些描述信息:
version:包版本 —— 這個包當(dāng)前安裝在?node_modules?中的版本
resolved:包具體的安裝來源
integrity:包?hash?值,基于?Subresource Integrity?來驗證已安裝的軟件包是否被改動過幻梯、是否已失效
requires:對應(yīng)子依賴的依賴兜畸,與子依賴的?package.json?中?dependencies的依賴項相同。
dependencies:結(jié)構(gòu)和外層的?dependencies?結(jié)構(gòu)相同碘梢,存儲安裝在子依賴?node_modules?中的依賴包咬摇。
這里注意,并不是所有的子依賴都有?dependencies?屬性煞躬,只有子依賴的依賴和當(dāng)前已安裝在根目錄的 ?node_modules?中的依賴沖突之后菲嘴,才會有這個屬性。
例如汰翠,回顧下上面的依賴關(guān)系:
我們在?my-app?中依賴的?base64-js@1.0.1?版本與?buffer?中依賴的?base64-js@^1.0.2?發(fā)生沖突龄坪,所以 ?base64-js@1.0.1??需要安裝在?buffer?包的?node_modules?中,對應(yīng)了?package-lock.json?中?buffer?的?dependencies?屬性复唤。這也對應(yīng)了?npm?對依賴的扁平化處理方式健田。
所以,根據(jù)上面的分析佛纫,?package-lock.json?文件 和?node_modules?目錄結(jié)構(gòu)是一一對應(yīng)的妓局,即項目目錄下存在 ?package-lock.json?可以讓每次安裝生成的依賴目錄結(jié)構(gòu)保持相同总放。
另外,項目中使用了?package-lock.json?可以顯著加速依賴安裝時間好爬。
我們使用?npm i --timing=true --loglevel=verbose?命令可以看到?npm install?的完整過程局雄,下面我們來對比下使用?lock?文件和不使用?lock?文件的差別。在對比前先清理下npm?緩存存炮。
不使用?lock?文件:
使用?lock?文件:
可見炬搭,?package-lock.json?中已經(jīng)緩存了每個包的具體版本和下載鏈接,不需要再去遠(yuǎn)程倉庫進(jìn)行查詢穆桂,然后直接進(jìn)入文件完整性校驗環(huán)節(jié)宫盔,減少了大量網(wǎng)絡(luò)請求。
使用建議
開發(fā)系統(tǒng)應(yīng)用時享完,建議把?package-lock.json?文件提交到代碼版本倉庫灼芭,從而保證所有團(tuán)隊開發(fā)者以及?CI?環(huán)節(jié)可以在執(zhí)行?npm install?時安裝的依賴版本都是一致的。
在開發(fā)一個?npm包 時般又,你的?npm包 是需要被其他倉庫依賴的彼绷,由于上面我們講到的扁平安裝機(jī)制,如果你鎖定了依賴包版本茴迁,你的依賴包就不能和其他依賴包共享同一?semver?范圍內(nèi)的依賴包寄悯,這樣會造成不必要的冗余。所以我們不應(yīng)該把package-lock.json?文件發(fā)布出去(?npm?默認(rèn)也不會把?package-lock.json?文件發(fā)布出去)笋熬。
緩存
在執(zhí)行?npm install?或?npm update命令下載依賴后热某,除了將依賴包安裝在node_modules?目錄下外,還會在本地的緩存目錄緩存一份胳螟。
通過?npm config get cache?命令可以查詢到:在?Linux?或?Mac?默認(rèn)是用戶主目錄下的?.npm/_cacache?目錄昔馋。
在這個目錄下又存在兩個目錄:content-v2、index-v5糖耸,content-v2?目錄用于存儲?tar包的緩存秘遏,而index-v5目錄用于存儲tar包的?hash。
npm 在執(zhí)行安裝時嘉竟,可以根據(jù)?package-lock.json?中存儲的?integrity邦危、version、name?生成一個唯一的?key?對應(yīng)到?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?即為緩存的前兩層目錄,我們進(jìn)去這個目錄果然找到的壓縮后的依賴包:
以上的緩存策略是從 npm v5 版本開始的个束,在 npm v5 版本之前慕购,每個緩存的模塊在 ~/.npm 文件夾中以模塊名的形式直接存儲聊疲,儲存結(jié)構(gòu)是{cache}/{name}/{version}。
npm?提供了幾個命令來管理緩存數(shù)據(jù):
npm cache add:官方解釋說這個命令主要是?npm?內(nèi)部使用沪悲,但是也可以用來手動給一個指定的 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ù),則從遠(yuǎn)程倉庫下載蒜鸡。
--prefer-online:優(yōu)先使用網(wǎng)絡(luò)數(shù)據(jù)胯努,如果網(wǎng)絡(luò)數(shù)據(jù)請求失敗,再去請求緩存數(shù)據(jù)逢防,這種模式可以及時獲取最新的模塊叶沛。
--offline:不請求網(wǎng)絡(luò),直接使用緩存數(shù)據(jù)忘朝,一旦緩存數(shù)據(jù)不存在灰署,則安裝失敗。
文件完整性
上面我們多次提到了文件完整性局嘁,那么什么是文件完整性校驗?zāi)兀?/p>
在下載依賴包之前溉箕,我們一般就能拿到?npm?對該依賴包計算的?hash?值,例如我們執(zhí)行?npm info?命令悦昵,緊跟?tarball(下載鏈接) 的就是?shasum(hash) :
用戶下載依賴包到本地后肴茄,需要確定在下載過程中沒有出現(xiàn)錯誤,所以在下載完成之后需要在本地在計算一次文件的?hash?值但指,如果兩個?hash?值是相同的寡痰,則確保下載的依賴是完整的,如果不同棋凳,則進(jìn)行重新下載拦坠。
整體流程
好了,我們再來整體總結(jié)下上面的流程:
檢查?.npmrc?文件:優(yōu)先級為:項目級的?.npmrc?文件 > 用戶級的?.npmrc?文件> 全局級的?.npmrc?文件 > npm 內(nèi)置的?.npmrc?文件
檢查項目中有無?lock?文件剩岳。
無?lock?文件:
從?npm?遠(yuǎn)程倉庫獲取包信息
根據(jù)?package.json?構(gòu)建依賴樹贞滨,構(gòu)建過程:
構(gòu)建依賴樹時,不管其是直接依賴還是子依賴的依賴卢肃,優(yōu)先將其放置在?node_modules?根目錄疲迂。
當(dāng)遇到相同模塊時才顿,判斷已放置在依賴樹的模塊版本是否符合新模塊的版本范圍,如果符合則跳過尤蒿,不符合則在當(dāng)前模塊的?node_modules?下放置該模塊郑气。
注意這一步只是確定邏輯上的依賴樹,并非真正的安裝腰池,后面會根據(jù)這個依賴結(jié)構(gòu)去下載或拿到緩存中的依賴包
在緩存中依次查找依賴樹中的每個包? ? ?
不存在緩存:
從?npm?遠(yuǎn)程倉庫下載包
校驗包的完整性
校驗不通過:
重新下載????
校驗通過:
將下載的包復(fù)制到?npm?緩存目錄
將下載的包按照依賴結(jié)構(gòu)解壓到?node_modules
存在緩存:將緩存按照依賴結(jié)構(gòu)解壓到?node_modules
將包解壓到?node_modules
生成?lock?文件
有?lock?文件:
檢查?package.json?中的依賴版本是否和?package-lock.json?中的依賴有沖突尾组。
如果沒有沖突,直接跳過獲取包信息示弓、構(gòu)建依賴樹過程讳侨,開始在緩存中查找包信息,后續(xù)過程相同
上面的過程簡要描述了?npm install?的大概過程奏属,這個過程還包含了一些其他的操作跨跨,例如執(zhí)行你定義的一些生命周期函數(shù),你可以執(zhí)行?npm install package --timing=true --loglevel=verbose?來查看某個包具體的安裝流程和細(xì)節(jié)囱皿。
yarn
yarn?是在?2016?年發(fā)布的勇婴,那時?npm?還處于?V3?時期,那時候還沒有?package-lock.json?文件嘱腥,就像上面我們提到的:不穩(wěn)定性耕渴、安裝速度慢等缺點經(jīng)常會受到廣大開發(fā)者吐槽。此時齿兔,yarn?誕生:
上面是官網(wǎng)提到的?yarn?的優(yōu)點橱脸,在那個時候還是非常吸引人的。當(dāng)然分苇,后來?npm?也意識到了自己的問題添诉,進(jìn)行了很多次優(yōu)化,在后面的優(yōu)化(lock文件组砚、緩存吻商、默認(rèn)-s...)中,我們多多少少能看到?yarn?的影子糟红,可見?yarn?的設(shè)計還是非常優(yōu)秀的艾帐。
yarn?也是采用的是?npm v3?的扁平結(jié)構(gòu)來管理依賴,安裝依賴后默認(rèn)會生成一個?yarn.lock?文件盆偿,還是上面的依賴關(guān)系柒爸,我們看看?yarn.lock?的結(jié)構(gòu):
# 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?中子依賴的版本號不是固定的捎稚,意味著單獨(dú)又一個?yarn.lock?確定不了?node_modules?目錄結(jié)構(gòu),還需要和?package.json?文件進(jìn)行配合。而?package-lock.json?只需要一個文件即可確定今野。
yarn?的緩策略看起來和?npm v5?之前的很像葡公,每個緩存的模塊被存放在獨(dú)立的文件夾,文件夾名稱包含了模塊名稱条霜、版本號等信息催什。使用命令?yarn cache dir?可以查看緩存數(shù)據(jù)的目錄:
yarn?默認(rèn)使用?prefer-online?模式,即優(yōu)先使用網(wǎng)絡(luò)數(shù)據(jù)宰睡,如果網(wǎng)絡(luò)數(shù)據(jù)請求失敗蒲凶,再去請求緩存數(shù)據(jù)。
參考
https://juejin.im/post/5a6008c2f265da3e5033cd93
https://www.zhihu.com/question/305539244/answer/551386426
https://zhuanlan.zhihu.com/p/37285173