npm & yarn 包管理機制
大廠技術(shù) 堅持周更 精選好文
背景
使用 npm 或 yarn 管理項目依賴時,可能會產(chǎn)生以下疑問:
項目依賴出現(xiàn)問題怎么辦?刪了重裝,即先刪除 node_modules 再重新 install,那這樣的操作會不會存在風險? 把所有依賴都安裝到 dependencies 中,不區(qū)分 devDependencies 會有問題嗎? 我們的應(yīng)用依賴了 pkg-a 和 pkg-b,同時 pkg-a 也依賴了 pkg-b,那么 pkg-b 會被多次安裝或重復(fù)打包嗎? 一個項目中,我使用 npm 別人使用 yarn,這會引發(fā)什么問題? 我們是否要提交 lockfile(package-lock.json/yarn.lock) 到項目倉庫呢? lockfile 在 git 操作時,時常會出現(xiàn)大量的沖突,你是怎么解決的呢?
npm 內(nèi)部機制和背后的思考
先來看下第一個問題,“刪除 node_modules,重新 npm install” 這樣解決依賴安裝問題百試不爽,其中的原理是什么?這樣做存在怎樣的風險?下面我們一起探究一下。
npm 的安裝機制非常值得探究。pip 是全局安裝,但 npm 的安裝機制秉承了不同的設(shè)計哲學(xué)。
npm 會優(yōu)先將依賴包安裝到項目目錄。 這樣做的好處是使不同項目的依賴各成體系,同時還減輕了包作者的 API 壓力;缺點也比較明顯,如果我們的 repo_a 和 repo_b 都有一個相同的依賴 pkg_c,那么這個公共依賴將在兩個項目中各被安裝一次。也就是說,同一個依賴可能在我們的電腦上多次安裝。
npm install
上圖是 npm 安裝依賴大致的過程,其中這樣幾個步驟需要關(guān)注:
檢查配置。包括項目級、用戶級、全局級、內(nèi)置的 .npmrc 文件。 確定依賴版本,構(gòu)建依賴樹。確定項目依賴版本有兩個來源,一是 package.json 文件,一是 lockfile 文件,兩個確認版本、構(gòu)建依賴樹的來源,互不可少、相輔相成。如果 package-lock.json 文件存在且符合 package.json 聲明的的情況下,直接讀取;否則重新確認依賴的版本。 下載包資源。下載前先確認本地是否存在匹配的緩存版本,如果有就直接使用緩存文件,如果沒有就下載并添加到緩存,然后將包按依賴樹解壓到 node_modules 目錄。 生成 lockfile 文件。
可以確認這樣幾個邏輯:
構(gòu)建依賴樹的過程中,版本確認需要結(jié)合 package.json 和 package-lock.json 兩個文件。先確認 package-lock.json 安裝版本,符合規(guī)則就以此為準,否則由 package.json 聲明的版本范圍重新確認。特別地,若是在開發(fā)中手動更改包信息,會導(dǎo)致lockfile 版本信息異常,也可能由 package.json 確認。確認好的依賴樹會存到 package-lock.json 文件中,這里跟 yarn.lock 存在差異。 同一個依賴,更高版本的包會安裝到頂層目錄,即 node_modules 目錄;否則會分散在某些依賴的 node_modules 目錄,如:node_modules/expect-jsx/node_modules/react 目錄。 如果依賴升級,造成版本不兼容,需要多版本共存,那么仍然是將高版本安裝到頂層,低版本分散到各級目錄。 lockfile 的存在,保證了項目依賴結(jié)構(gòu)的確定性,保障了項目在多環(huán)境運行的穩(wěn)定性。 ...
yarn 安裝理念以及破解依賴管理困境
yarn 作為區(qū)別于 npm 的依賴管理工具,誕生之初就是為了解決歷史上 npm 的某些不足,比如 npm 缺乏對于依賴的完整性和一致性保障,以及 npm 安裝速度過慢的問題等,盡管 npm 發(fā)展至今,已經(jīng)在很多方面向 yarn 看齊,但 yarn 的安裝理念仍然需要我們關(guān)注。yarn 提出的安裝理念很好的解決了當時 npm 的依賴管理問題:
確定性。通過 yarn.lock 等機制,保證了確定性,這里的確定性包括但不限于明確的依賴版本、明確的依賴安裝結(jié)構(gòu)等。即在任何機器和環(huán)境下,都可以以相同的方式被安裝。 模塊扁平化安裝。將依賴包的不同版本,按照一定策略,歸結(jié)為單個版本,以避免創(chuàng)建多個副本造成冗余。(npm 也有相同的優(yōu)化) 更好的網(wǎng)絡(luò)性能。Yarn 采用了請求排隊的理念,類似并發(fā)連接池,能夠更好地利用網(wǎng)絡(luò)資源;同時引入了更好的安裝失敗時的重試機制。(npm 較早的版本是順序下載,當?shù)谝粋€包完全下載完成后,才會將下載控制權(quán)交給下一個包) 引入緩存機制,實現(xiàn)離線策略。(npm 也有類似的優(yōu)化)
yarn.lock 文件結(jié)構(gòu)
以 react 等依賴為例,先大致了解一下 yarn.lock 文件的結(jié)構(gòu)以及確定依賴版本的方式:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
expect-jsx@^5.0.0:
version "5.0.0"
resolved "[http://registry.npmjs.org/expect-jsx/-/expect-jsx-5.0.0.tgz#61761b43365f285a80eb280c785e0783bbe362c7](http://registry.npmjs.org/expect-jsx/-/expect-jsx-5.0.0.tgz#61761b43365f285a80eb280c785e0783bbe362c7 "http://registry.npmjs.org/expect-jsx/-/expect-jsx-5.0.0.tgz#61761b43365f285a80eb280c785e0783bbe362c7")"
integrity sha1-YXYbQzZfKFqA6ygMeF4Hg7vjYsc=
dependencies:
collapse-white-space "^1.0.0"
react "^16.0.0"
react-element-to-jsx-string "^13.0.0"
react-rater@^6.0.0:
version "6.0.0"
resolved "[http://registry.npmjs.org/react-rater/-/react-rater-6.0.0.tgz#2e666b6e5e5c33b622541df6a7124f6c99606927](http://registry.npmjs.org/react-rater/-/react-rater-6.0.0.tgz#2e666b6e5e5c33b622541df6a7124f6c99606927 "http://registry.npmjs.org/react-rater/-/react-rater-6.0.0.tgz#2e666b6e5e5c33b622541df6a7124f6c99606927")"
integrity sha512-NP1+rEeL3LyJqA5xF7U2fSHpISMcVeMgbQ0u/P1WmayiHccI7Ixx5GohygmJY82g7SxdJnIun2OOB6z8WTExmg==
dependencies:
prop-types "^15.7.2"
react "^16.8.0"
react-dom "^16.8.0"
//一或多個具有相同版本范圍的依賴聲明,確定一個可用的版本。這就是 lockfile 的確定性。
react@^16.0.0, react@^16.8.0:
version "16.14.0"
resolved "[http://registry.npmjs.org/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d](http://registry.npmjs.org/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d "http://registry.npmjs.org/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d")"
integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
//如果同一個依賴存在多個版本,那么最高版本安裝在頂層目錄,即 node_modules 目錄。
react@^17.0.1:
version "17.0.2"
resolved "[http://registry.npmjs.org/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037](http://registry.npmjs.org/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037 "http://registry.npmjs.org/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037")"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
從上面依賴版本描述的信息中,可以確定以下幾點:
所有依賴,不管是項目聲明的依賴,還是依賴的依賴,都是扁平化管理。 依賴的版本是由所有依賴的版本聲明范圍確定的,具備相同版本聲明范圍的依賴歸結(jié)為一類,確定一個該范圍下的依賴版本。如果同一個依賴多個版本共存,那么會并列歸類。 每個依賴確定的版本中,是由以下幾項構(gòu)成: 多個依賴的聲明版本,且符合 semver 規(guī)范; 確定的版本號 version 字段; 版本的完整性驗證字段 依賴列表 相比 npm,Yarn 一個顯著區(qū)別是 yarn.lock 中子依賴的版本號不是固定版本。 也就是說單獨一個 yarn.lock 確定不了 node_modules 目錄結(jié)構(gòu),還需要和 package.json 文件進行配合。
yarn install
以下是在 yarn 安裝依賴時的步驟:
1、檢查(checking)主要是檢查項目中是否存在一些 npm 相關(guān)的配置文件,如 package-lock.json 等。如果存在,可能會警告提示,因為它們可能會存在沖突。在這一階段,也會檢查系統(tǒng) OS、CPU 等信息。
2、解析包(resolving packages)這一步主要是解析依賴樹,確定版本信息等。首先獲取項目 package.json 中聲明的首層依賴,包括 dependencies, devDependencies, optionalDependencies 聲明的依賴。接著采用遍歷首層依賴的方式獲取依賴包的版本信息,以及遞歸查找每個依賴下嵌套依賴的版本信息,并將解析過和正在解析的包用一個 Set 數(shù)據(jù)結(jié)構(gòu)來存儲,這樣就能保證同一個版本范圍內(nèi)的包不會被重復(fù)解析。
對于沒有解析過的包,首次嘗試從 yarn.lock 中獲取到版本信息,并標記為已解析; 如果在 yarn.lock 中沒有找到包,則向 Registry 發(fā)起請求獲取滿足版本范圍的已知最高版本的包信息,獲取后將當前包標記為已解析。
總之,在經(jīng)過復(fù)雜的解析算法后,我們就確定了所有依賴的具體版本信息以及下載地址。
3、獲取包(fetching packages)這一步主要是利用系統(tǒng)緩存,到緩存中找到具體的包資源。首先會嘗試在緩存中查找依賴包,如果沒有命中緩存,則將依賴包下載到緩存中。對于沒有命中緩存的包,Yarn 會維護一個 fetch 隊列,按照規(guī)則進行網(wǎng)絡(luò)請求。這里也是 yarn 誕生之初解決 npm v3 安裝緩慢問題的優(yōu)化點,支持并行下載。
如何判斷有沒有命中緩存?
判斷系統(tǒng)中存在符合 "cachefolder+slug+node_modules+pkg.name" 規(guī)則的路徑,如果存在則判斷為命中緩存,否則就會重新下載。值得注意的是,不同版本的包在緩存中是扁平化管理。以下是緩存中 webpack 的依賴緩存,可以通過 yarn cache dir 查看。
4、鏈接包(linking dependencies)這一步主要是將緩存中的依賴,復(fù)制到項目目錄下,同時遵循扁平化原則。前面說到,npm 優(yōu)先將依賴安裝到項目目錄,因此需要將全局緩存中的依賴復(fù)制到項目。在復(fù)制依賴前,Yarn 會先解析 peerDependencies,如果找不到符合 peerDependencies 聲明的依賴版本,則進行 warning 提示(這并不會影響命令執(zhí)行),并最終拷貝依賴到項目中。
5、構(gòu)建包(building fresh package)如果依賴包中存在二進制包需要進行編譯,會在這一步進行。
如果破解依賴管理困境
在 npm v2 時期,安裝的依賴會存在于引用依賴的 node_modules 目錄,如果依賴過多,會形成一顆巨大的依賴樹。這種結(jié)構(gòu)雖然簡單明了,但是對于大型項目十分不友好。依賴層級深對開發(fā)排查不利,并且依賴的復(fù)用也是問題。在 npm v3 中引入扁平化的概念。看幾個場景的例子??:
場景一:不同 npm 版本安裝依賴的結(jié)構(gòu)
[email protected] 依賴 [email protected],npm v3 是扁平化管理依賴。
場景二:不同 npm 版本處理依賴多版本共存問題
在場景一的基礎(chǔ)上,安裝 [email protected],而它依賴另一個版本的 [email protected]。由于根目錄下已存在 [email protected] 的依賴,npm v3 會把 [email protected] 安裝到 [email protected] 依賴的 node_modules 目錄。
靚仔疑惑:為什么 [email protected] 在頂級,而 [email protected] 在子級呢?
場景三:依賴的多版本的數(shù)量與依賴版本分布關(guān)系
在場景二的基礎(chǔ)上,安裝 [email protected],而它也依賴 [email protected]。同樣的,由于根目錄下已存在 [email protected] 的依賴,npm v3 會把 [email protected] 安裝到 [email protected] 依賴的 node_modules 目錄。
靚仔疑惑:你可能會疑問,此時存在2個 [email protected] 和1個 [email protected],出現(xiàn)在頂級安裝目錄的不應(yīng)該是 v2 版本而非 v1 版本嘛?
其實這是由依賴的安裝順序決定的,真就是依賴的某個版本如果出現(xiàn)在合適的時間,那么它就會被安裝到頂級 node_modules 目錄。不同版本的出場順序?qū)е乱蕾嚱Y(jié)構(gòu)的差異,npm v3 注定不是穩(wěn)定的包管理工具。跟生活一樣,人物的出場順序很重要,它決定了你在哪里做什么事。
場景四:依賴版本存在重復(fù)和可用
在場景三的基礎(chǔ)上,安裝 [email protected],它依賴 [email protected]。由于頂級目錄已存在目標版本,因此 npm v3 會跳過該依賴的安裝。
場景五:版本升級囧境在
場景三的基礎(chǔ)上,如果更新了 [email protected],同時它的依賴是 [email protected]。那么 npm v3 的執(zhí)行順序是,刪除 [email protected],安裝 [email protected],安裝 [email protected],留下了 [email protected] 在頂層目錄,因此 [email protected] 會安裝到其父依賴的 node_modules 目錄。
場景六:依賴版本多目錄存在且符合復(fù)用條件
在場景五的基礎(chǔ)上,更新 [email protected],它依賴了 [email protected]。那么 npm v3 的執(zhí)行順序是,刪除 [email protected],安裝 [email protected],刪除 [email protected],安裝 [email protected],于是出現(xiàn)以下結(jié)構(gòu)。
此時你會發(fā)現(xiàn),存在多個 [email protected] 分布在不同的 node_modules 目錄,他們是不是只要在頂級目錄存在一份即可?沒錯,我們刪除 node_modules 目錄重裝,得到的就是你想的清晰的結(jié)構(gòu)。
實際上,更優(yōu)雅的方式是使用 npm dedupe 命令達到上述結(jié)構(gòu)。而 yarn 在安裝依賴時會自動執(zhí)行 dedupe 命令。
正是由于上述一些 npm 歷史的坑,所以更建議使用 yarn 作為項目協(xié)作的包管理工具。當然 npm 發(fā)展至今,很多問題已經(jīng)優(yōu)化掉,現(xiàn)在 yarn 和 npm 是兩款互相看齊、互相獲取靈感的依賴管理工具。
npm vs. yarn ??
這里簡單對比 npm v6 和 yarn v1. 這是我們生產(chǎn)開發(fā)常用的版本。
npm 和 yarn 作為兩款相似的包管理工具,在一些功能實現(xiàn)上它們互相獲取靈感。
相同點:
package.json 作為項目依賴描述文件。 node_modules 作為依賴存儲目錄,yarn v2 不再是這樣。 lockfile 鎖定版本依賴,在 yarn 中叫 yarn.lock,在 npm 中叫 package-lock.json,在 npm v7 也支持了 yarn.lock。它確保在不同機器或不同環(huán)境中,能夠得到穩(wěn)定的 node_modules 目錄結(jié)構(gòu)。
差異:
依賴管理策略。 lockfile。package-lock.json 自帶版本鎖定+依賴結(jié)構(gòu),你想改動一些依賴,可能影響的范圍要比表面看起來的復(fù)雜的多;而 yarn.lock 自帶版本鎖定,并沒有確定的依賴結(jié)構(gòu),使用 yarn 管理項目依賴,需要 package.json + yarn.lock 共同確定依賴的結(jié)構(gòu)。 性能。(對比 npm v6 和 yarn v1)目前 npm v7 優(yōu)化了緩存和下載網(wǎng)絡(luò)策略,性能的差異在縮小。

[拓展] npm 企業(yè)級部署私服原理
npm 中的源(registry),其實就是一個查詢服務(wù)。以 npmjs.org 為例,它的查詢服務(wù)網(wǎng)址是 https://registry.npmjs.org/ ,在這個網(wǎng)址后加上依賴的名字,就會得到一個 JSON 對象,里面包含了依賴所有的信息。例如:
https://registry.npmjs.org/react https://registry.npm.taobao.org/react
我們可以通過 npm config set registry 命令來設(shè)置安裝源。你知道我們公司為什么要部署私有的 npm 鏡像嗎?雖然 npm 并沒有被屏蔽,但是下載第三方依賴包的速度依然較緩慢,這嚴重影響 CI/CD 流程或本地開發(fā)效率。通常我們認為部署 npm 私服具備以下優(yōu)點:
確保高速、穩(wěn)定的 npm 服務(wù) 確保發(fā)布私有模塊的安全性 審核機制可以保障私服上 npm 模塊質(zhì)量和安全
部署企業(yè)級私服,能夠獲得安全、穩(wěn)定、高速的保障。
管理項目依賴的小技巧(集思廣益...)
推薦使用 yarn 作為團隊包管理工具,而不是 npm。盡管在 npm v6 之后的版本趨向穩(wěn)定和安全,但由于歷史原因和團隊管理兼容性,仍然是推薦使用 yarn 作為團隊統(tǒng)一的包管理工具。 項目中一定要存在 lockfile 文件,且禁止手動修改,因為這是項目穩(wěn)定性運行的保障。 如果 yarn.lock 在代碼合并的過程中出現(xiàn)了問題,可以嘗試使用 yarn install 解決問題。 ...
參考資料
npm install[1] yarn install[2] yarn.lock[3] NPM vs. Yarn: Which Package Manager Should You Choose?[4] Lockfiles should be committed on all projects[5]
參考資料
npm install: https://docs.npmjs.com/cli/v7/commands/npm-install
[2]yarn install: https://classic.yarnpkg.com/en/docs/cli/install
[3]yarn.lock: https://classic.yarnpkg.com/en/docs/yarn-lock
[4]NPM vs. Yarn: Which Package Manager Should You Choose?: https://www.whitesourcesoftware.com/free-developer-tools/blog/npm-vs-yarn-which-should-you-choose/
[5]Lockfiles should be committed on all projects: https://classic.yarnpkg.com/blog/2016/11/24/lockfiles-for-all/
?? 謝謝支持
以上便是本次分享的全部內(nèi)容,希望對你有所幫助^_^
喜歡的話別忘了 分享、點贊、收藏 三連哦~。
歡迎關(guān)注公眾號 前端Sharing 收貨大廠一手好文章~
