相關參考,推薦閱讀
https://classic.yarnpkg.com/en/docs/
https://www.sitepoint.com/yarn-vs-npm/
https://mp.weixin.qq.com/s/Yr2_Jr0wv3yFDEQuGVqdyw
https://docs.npmjs.com/cli/v7/commands/npm-install
https://classic.yarnpkg.com/en/docs/cli/install
https://classic.yarnpkg.com/blog/2016/11/24/lockfiles-for-all/
背景
做為一個Web前端,項目開發時經常會用到 npm
和 yarn
。平常只是用於安裝項目依賴並沒有過多研究內部更新機制及理念,本文在這里為大家介紹一下。
在開始之前我們可以先帶着問題去閱讀方便我們更深入理解,使用 npm
或 yarn
管理項目依賴時,常遇到的問題:
- 項目依賴出現問題怎么辦?常見操作刪除
node_modules
,重新install
,是否有風險? - 把所有依賴都安裝到 dependencies 中,不區分 devDependencies 會有問題嗎?
- 我們的應用依賴了 pkg-a 和 pkg-b,同時 pkg-a 也依賴了 pkg-b,那么 pkg-b 會被多次安裝或重復打包嗎?
- 項目開發時我使用 npm 同事使用 yarn,這會引發什么問題?
- 是否要提交 lockfile(package-lock.json/yarn.lock) 到項目倉庫呢?
- lockfile 在 git 操作時,時常會出現大量的沖突,你是怎么解決的呢?
npm 內部機制和背后的思考
先來看下第一個問題, “刪除 node_modules,重新 install” 這樣解決依賴安裝問題百試不爽,其中的原理是什么?這樣做是否存在風險?下面我們一起探究一下。
npm 會優先將依賴包安裝到項目目錄。 這樣做的好處是使不同項目的依賴各成體系,同時還減輕了包作者的 API 壓力;缺點也比較明顯,如果我們的 repo_a 和 repo_b 都有一個相同的依賴 pkg_c,那么這個公共依賴將在兩個項目中各被安裝一次。也就是說,同一個依賴可能在我們的電腦上多次安裝。
npm install
上圖是 npm 安裝依賴大致的過程,其中這樣幾個步驟需要關注:
1、檢查配置。包括項目級、用戶級、全局級、內置的 .npmrc 文件。
2、確定依賴版本,構建依賴樹。確定項目依賴版本有兩個來源,一是 package.json 文件,一是 lockfile 文件,兩個確認版本、構建依賴樹的來源,互不可少、相輔相成。如果 package-lock.json 文件存在且符合 package.json 聲明的的情況下,直接讀取;否則重新確認依賴的版本。
3、下載包資源。下載前先確認本地是否存在匹配的緩存版本,如果有就直接使用緩存文件,如果沒有就下載並添加到緩存,然后將包按依賴樹解壓到 node_modules 目錄。
4、生成 lockfile 文件。
可以確認這樣幾個邏輯:
1、構建依賴樹的過程中,版本確認需要結合 package.json 和 package-lock.json 兩個文件。先確認 package-lock.json 安裝版本,符合規則就以此為准,否則由 package.json 聲明的版本范圍重新確認。特別地,若是在開發中手動更改包信息,會導致lockfile 版本信息異常,也可能由 package.json 確認。確認好的依賴樹會存到 package-lock.json 文件中,這里跟 yarn.lock 存在差異。
2、同一個依賴,更高版本的包會安裝到頂層目錄,即 node_modules 目錄;否則會分散在某些依賴的 node_modules 目錄,如:node_modules/expect-jsx/node_modules/react 目錄。
3、如果依賴升級,造成版本不兼容,需要多版本共存,那么仍然是將高版本安裝到頂層,低版本分散到各級目錄。
4、lockfile 的存在,保證了項目依賴結構的確定性,保障了項目在多環境運行的穩定性。
yarn 安裝理念以及破解依賴管理困境
yarn 作為區別於 npm 的依賴管理工具,誕生之初就是為了解決歷史上 npm 的某些不足,比如 npm 缺乏對於依賴的完整性和一致性保障,以及 npm 安裝速度過慢的問題等,盡管 npm 發展至今,已經在很多方面向 yarn 看齊,但 yarn 的安裝理念仍然需要我們關注。yarn 提出的安裝理念很好的解決了當時 npm 的依賴管理問題:
1、確定性。通過 yarn.lock 等機制,保證了確定性,這里的確定性包括但不限於明確的依賴版本、明確的依賴安裝結構等。即在任何機器和環境下,都可以以相同的方式被安裝。
2、模塊扁平化安裝。將依賴包的不同版本,按照一定策略,歸結為單個版本,以避免創建多個副本造成冗余。(npm 也有相同的優化)
3、更好的網絡性能。Yarn 采用了請求排隊的理念,類似並發連接池,能夠更好地利用網絡資源;同時引入了更好的安裝失敗時的重試機制。(npm 較早的版本是順序下載,當第一個包完全下載完成后,才會將下載控制權交給下一個包)
4、引入緩存機制,實現離線策略。(npm 也有類似的優化)
yarn.lock 文件結構
以 react 等依賴為例,先大致了解一下 yarn.lock 文件的結構以及確定依賴版本的方式:
# 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"
從上面依賴版本描述的信息中,可以確定以下幾點:
1、所有依賴,不管是項目聲明的依賴,還是依賴的依賴,都是扁平化管理。
2、依賴的版本是由所有依賴的版本聲明范圍確定的,具備相同版本聲明范圍的依賴歸結為一類,確定3、一個該范圍下的依賴版本。如果同一個依賴多個版本共存,那么會並列歸類。
4、每個依賴確定的版本中,是由以下幾項構成:
- 多個依賴的聲明版本,且符合 semver 規范;
- 確定的版本號 version 字段;
- 版本的完整性驗證字段
- 依賴列表
5、相比 npm,Yarn 一個顯著區別是 yarn.lock 中子依賴的版本號不是固定版本。 也就是說單獨一個 yarn.lock 確定不了 node_modules 目錄結構,還需要和 package.json 文件進行配合。
yarn install
以下是在 yarn 安裝依賴時的步驟:
1、檢查(checking) 主要是檢查項目中是否存在一些 npm 相關的配置文件,如 package-lock.json 等。如果存在,可能會警告提示,因為它們可能會存在沖突。在這一階段,也會檢查系統 OS、CPU 等信息。
2、解析包(resolving packages) 這一步主要是解析依賴樹,確定版本信息等。首先獲取項目 package.json 中聲明的首層依賴,包括 dependencies, devDependencies, optionalDependencies 聲明的依賴。接着采用遍歷首層依賴的方式獲取依賴包的版本信息,以及遞歸查找每個依賴下嵌套依賴的版本信息,並將解析過和正在解析的包用一個 Set 數據結構來存儲,這樣就能保證同一個版本范圍內的包不會被重復解析。
- 對於沒有解析過的包,首次嘗試從 yarn.lock 中獲取到版本信息,並標記為已解析;
- 如果在 yarn.lock 中沒有找到包,則向 Registry 發起請求獲取滿足版本范圍的已知最高版本的包信息,獲取后將當前包標記為已解析。
總之,在經過復雜的解析算法后,我們就確定了所有依賴的具體版本信息以及下載地址。
3、獲取包(fetching packages) 這一步主要是利用系統緩存,到緩存中找到具體的包資源。首先會嘗試在緩存中查找依賴包,如果沒有命中緩存,則將依賴包下載到緩存中。對於沒有命中緩存的包,Yarn 會維護一個 fetch 隊列,按照規則進行網絡請求。這里也是 yarn 誕生之初解決 npm v3 安裝緩慢問題的優化點,支持並行下載。
如何判斷有沒有命中緩存?
判斷系統中存在符合 "cachefolder+slug+node_modules+pkg.name" 規則的路徑,如果存在則判斷為命中緩存,否則就會重新下載。值得注意的是,不同版本的包在緩存中是扁平化管理。以下是緩存中 webpack 的依賴緩存,可以通過 yarn cache dir 查看。
4、鏈接包(linking dependencies) 這一步主要是將緩存中的依賴,復制到項目目錄下,同時遵循扁平化原則。前面說到,npm 優先將依賴安裝到項目目錄,因此需要將全局緩存中的依賴復制到項目。在復制依賴前,Yarn 會先解析 peerDependencies,如果找不到符合 peerDependencies 聲明的依賴版本,則進行 warning 提示(這並不會影響命令執行),並最終拷貝依賴到項目中。
5、構建包(building fresh package) 如果依賴包中存在二進制包需要進行編譯,會在這一步進行。
如果破解依賴管理困境
在 npm v2 時期,安裝的依賴會存在於引用依賴的 node_modules 目錄,如果依賴過多,會形成一顆巨大的依賴樹。這種結構雖然簡單明了,但是對於大型項目十分不友好。依賴層級深對開發排查不利,並且依賴的復用也是問題。在 npm v3 中引入扁平化的概念。看幾個場景的例子🌰:
場景一:不同 npm 版本安裝依賴的結構
pkg-a@1.0.0 依賴 pkg-b@1.0.0,npm v3 是扁平化管理依賴。
場景二:不同 npm 版本處理依賴多版本共存問題
在場景一的基礎上,安裝 pkg-c@1.0.0,而它依賴另一個版本的 pgk-b@2.0.0。由於根目錄下已存在 pkg-b@1.0.0 的依賴,npm v3 會把 pkg-b@2.0.0 安裝到 pkg-c@1.0.0 依賴的 node_modules 目錄。
靚仔疑惑:為什么 pkg-b@1.0.0 在頂級,而 pkg-b@2.0.0 在子級呢?
場景三:依賴的多版本的數量與依賴版本分布關系
在場景二的基礎上,安裝 pkg-d@1.0.0,而它也依賴 pkg-b@2.0.0。同樣的,由於根目錄下已存在 pkg-b@1.0.0 的依賴,npm v3 會把 pkg-b@2.0.0 安裝到 pkg-d@1.0.0 依賴的 node_modules 目錄。
靚仔疑惑:你可能會疑問,此時存在2個 pkg-b@2.0.0 和1個 pkg-b@1.0.0,出現在頂級安裝目錄的不應該是 v2 版本而非 v1 版本嘛?
其實這是由依賴的安裝順序決定的,真就是依賴的某個版本如果出現在合適的時間,那么它就會被安裝到頂級 node_modules 目錄。不同版本的出場順序導致依賴結構的差異,npm v3 注定不是穩定的包管理工具。跟生活一樣,人物的出場順序很重要,它決定了你在哪里做什么事。
場景四:依賴版本存在重復和可用
在場景三的基礎上,安裝 pkg-e@1.0.0,它依賴 pkg-b@1.0.0。由於頂級目錄已存在目標版本,因此 npm v3 會跳過該依賴的安裝。
場景五:版本升級囧境在
場景三的基礎上,如果更新了 pkg-a@2.0.0,同時它的依賴是 pkg-b@2.0.0。那么 npm v3 的執行順序是,刪除 pkg-a@1.0.0,安裝 pkg-a@2.0.0,安裝 pkg-b@2.0.0,留下了 pkg-b@1.0.0 在頂層目錄,因此 pkg-b@2.0.0 會安裝到其父依賴的 node_modules 目錄。
場景六:依賴版本多目錄存在且符合復用條件
在場景五的基礎上,更新 pkg-e@2.0.0,它依賴了 pkg-b@2.0.0。那么 npm v3 的執行順序是,刪除 pkg-a@1.0.0,安裝 pkg-e@2.0.0,刪除 pkg-b@1.0.0,安裝 pkg-b@2.0.0,於是出現以下結構。
此時你會發現,存在多個 pkg-b@2.0.0 分布在不同的 node_modules 目錄,他們是不是只要在頂級目錄存在一份即可?沒錯,我們刪除 node_modules 目錄重裝,得到的就是你想的清晰的結構。
實際上,更優雅的方式是使用 npm dedupe 命令達到上述結構。而 yarn 在安裝依賴時會自動執行 dedupe 命令。
正是由於上述一些 npm 歷史的坑,所以更建議使用 yarn 作為項目協作的包管理工具。當然 npm 發展至今,很多問題已經優化掉,現在 yarn 和 npm 是兩款互相看齊、互相獲取靈感的依賴管理工具。
npm vs yarn
這里簡單對比 npm v6 和 yarn v1. 這是我們生產開發常用的版本。
npm 和 yarn 作為兩款相似的包管理工具,在一些功能實現上它們互相獲取靈感。
相同點
- package.json 作為項目依賴描述文件。
- node_modules 作為依賴存儲目錄,yarn v2 不再是這樣。
- lockfile 鎖定版本依賴,在 yarn 中叫 yarn.lock,在 npm 中叫 package-lock.json,在 npm v7 也支持了 yarn.lock。它確保在不同機器或不同環境中,能夠得到穩定的 node_modules 目錄結構。
差異
- 依賴管理策略。
- lockfile。package-lock.json 自帶版本鎖定+依賴結構,你想改動一些依賴,可能影響的范圍要比表面看起來的復雜的多;而 yarn.lock 自帶版本鎖定,並沒有確定的依賴結構,使用 yarn 管理項目依賴,需要 package.json + yarn.lock 共同確定依賴的結構。
- 性能。(對比 npm v6 和 yarn v1)目前 npm v7 優化了緩存和下載網絡策略,性能的差異在縮小。
No cache | cached | Reinstall | |
---|---|---|---|
Npm 6.13.4 | 67 seconds | 61 seconds | 28 seconds |
Yarn 1.21.1 | 57 seconds | 29 seconds | 1.2 seconds |
【拓展】npm 企業級部署私服原理
npm 中的源(registry),其實就是一個查詢服務。 以 npmjs.org 為例,它的查詢服務網址是 https://registry.npmjs.org/ ,在這個網址后加上依賴的名字,就會得到一個 JSON 對象,里面包含了依賴所有的信息。例如:
我們可以通過 npm config set registry 命令來設置安裝源。你知道我們公司為什么要部署私有的 npm 鏡像嗎?雖然 npm 並沒有被屏蔽,但是下載第三方依賴包的速度依然較緩慢,這嚴重影響 CI/CD 流程或本地開發效率。通常我們認為部署 npm 私服具備以下優點:
- 確保高速、穩定的 npm 服務
- 確保發布私有模塊的安全性
- 審核機制可以保障私服上 npm 模塊質量和安全
部署企業級私服,能夠獲得安全、穩定、高速的保障。
管理項目依賴的小技巧(集思廣益...)
- 推薦使用 yarn 作為團隊包管理工具,而不是 npm。盡管在 npm v6 之后的版本趨向穩定和安全,但由於歷史原因和團隊管理兼容性,仍然是推薦使用 yarn 作為團隊統一的包管理工具。
- 項目中一定要存在 lockfile 文件,且禁止手動修改,因為這是項目穩定性運行的保障。
- 如果 yarn.lock 在代碼合並的過程中出現了問題,可以嘗試使用 yarn install 解決問題。
作者:雨中愚
鏈接:https://juejin.cn/post/7008168128739803150
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。