扣丁書屋

node_modules 困境

Ryan 對于 node.js 的十大遺憾之一就是支持了 node_modules,node_modules 的設計雖然能滿足大部分的場景,但是其仍然存在著種種缺陷,尤其在前端工程化領域,造成了不少的問題,本文總結下其存在的一些問題,和可能的改進方式。

術語

  • package:包含了 package.json, 使用 package.json 定義的一個 package,通常是對應一個 module,也可以不包含 module,比如 bin 里指明一個 shell 腳本,甚至是任意文件(將 registry 當做 http 服務器使用,或者利用 unpkg 當做 cdn 使用),一個 package 可以是一個 tar 包,也可以是本地 file 協議,甚至 git 倉庫地址。
  • module:能被 require 加載的就叫一個 module,如下都是 module,只有當 module 含有 package.json 的時候才能叫做 package。
  • 一個包含 package.json 且含有 main 字段的文件夾
  • 一個含有 index.js 的文件夾
  • 任意的 js 文件

綜合:module 不一定是 package,package 不一定是 module。

Dependency Hell

現在項目里有兩個依賴 A 和 C,A 和 C 分別依賴 B 的不同版本,如何處理

這里存在兩個問題1. 首先是 B 本身支持多版本共存,只要 B 本身沒有副作用,這是很自然的,但是對于很多庫如 core-js 會污染全局環境,本身就不支持多版本共存,因此我們需要盡早的進行報錯提示(conflict 的 warning 和運行時的 conflict 的 check)。 2. 如果 B 本身支持多版本共存,那么需要保證 A 正確的加載到 B v1.0 和 C 正確的加載到 B v2.0。

我們重點考慮第二個問題。

npm解決方式

node 的解決方式是依賴的 node 加載模塊的路徑查找算法和 node_modules 的目錄結構來配合解決的。 如何從 node_modules 加載 package? 核心是遞歸向上查找 node_modules 里的 package,如果在 '/home/ry/projects/foo.js' 文件里調用了 require('bar.js'),則 Node.js 會按以下順序查找:

  • /home/ry/projects/node_modules/bar.js
  • /home/ry/node_modules/bar.js
  • /home/node_modules/bar.js
  • /node_modules/bar.js

該算法有兩個核心

- 優先讀取最近的node_modules的依賴
- 遞歸向上查找node_modules依賴

該算法即簡化了 Dependency hell 的解決方式,也帶來了非常多的問題

node_modules的目錄結構

nest mode 利用 require 先在最近的 node_module 里查找依賴的特性,我們能想到一個很簡單的方式,直接在 node_module 維護原模塊的拓撲圖即可。

這樣根據 mod-a 就近的使用 mod-b 的 1.0 版本,而 mod-c 就近的使用了 mod-b 的 2.0 版本。 但是這樣帶來了另一個問題,如果我們此時再依賴一個 mod-d,該 mod-d 也同時依賴的 mod-b 的 2.0 版本,這時候 node_modules 就變成下面這樣:

我們發現這里存在個問題,雖然 mod-a 和 mod-d 依賴了同一個 mod-b 的版本,但是 mod-b 卻安裝了兩遍,如果你的應用了很多的第三方庫,同時第三方庫共同依賴了一些很基礎的第三方庫如 lodash,你會發現你的 node_modules 里充滿了各種重復版本的 lodash,造成了極大的空間浪費,也導致 npm install 很慢,這既是臭名昭著的 node_modules hell。

flat mode 我們還可以利用向上遞歸查找依賴的特性,將一些公共依賴放在公共的 node_module 里

根據 require 的查找算法- A 和 D 首先會去自己的 node_module 里去查找 B,發現不存在 B,然后遞歸的向上查找,此時查找到了 B 的 v1.0 版本,符合預期

  • C 會先查找到自己的 node_module 里查找到了 B v2.0,符合預期

這時我們發現了即解決了 depdency hell 也避免了 npm2 的 nest 模式導致的重復依賴問題。

doppelgangers

但是問題并沒有結束,如果此時引入的 D 依賴的是 B v2.0,而引入的 E 依賴的是 B v1.0,我們發現無論是把 B v2.0 還是 B v1.0 放在 top level,都會導致另一個版本任何會存在重復的問題,如這里的 B 的 v2.0 的重復問題

版本重復會有問題嗎?

你也許會說版本重復不就是浪費一點空間嗎,而且這種只有出現版本沖突的時候才會碰到,似乎問題不大,事實的確如此,然而某些情況下這仍然會造成問題

全局types沖突

雖然各個 package 之前的代碼不會相互污染,但是他們的 types 仍然可以相互影響,很多的第三方庫會修改全局的類型定義,典型的就是 @types/react,如下是一個常見的錯誤

其錯誤原因就在于全局的 types 形成了命名沖突,因此假如版本重復可能會導致全局的類型錯誤。 一般的解決方式就是自己控制包含哪些加載的 @types/xxx。

破壞單例模式

require 的緩存機制

node 會對加載的模塊進行緩存,第一次加載某個模塊后會將結果緩存下來,后續的 require 調用都返回同一結果,然而 node 的 require 的緩存并非是基于 module 名,而是基于 resolve 的文件路徑的,且是大小寫敏感的,這意味著即使你代碼里看起來加載的是同一模塊的同一版本,如果解析出來的路徑名不一致,那么會被視為不同的module,如果同時對該 module 同時進行副作用操作,就會產生問題。 以 react-loadable 為例,其同時在 browser 和 node 層使用 browser 里使用

node 層使用

然后將 browser 進行打包編譯為 bundle.js,并在 node 層加載編譯好的代碼 bundle.js 雖然 node 層和 browser 訪問的都是 'react-loadable',如果 webpack 編譯的時候涉及到路徑改寫,雖然 react-loadable 的版本一致,那么會導致 node 和 browser 加載的不是一份 react-loadble 的導出對象,不幸的是 react-loadable 強依賴 node 和 browser 導出的是同一個對象。因為 node 層會讀取 browser 設置的 READY_INITIALIZERS,如果 node 和 browser 導出的不是同一個對象,則導致讀取失敗

另一個容易出問題的地方就是使用 git submodule,git submodule很容易造成一個環境里多版本共存,比如同時存在多個react版本,更容易觸發問題。

Phantom dependency

我們發現 flat mode 相比 nest mode 節省了很多的空間,然而也帶來了一個問題即 phantom depdency,考察下如下的項目

我們編寫如下代碼

這里的 glob 和 brace-expansion 都不在我們的 depdencies 里,但是我們開發和運行時都可以正常工作(因為這個是 rimraf 的依賴),一旦將該庫發布,因為用戶安裝我們的庫的時候并不會安裝庫的 devDepdency,這導致在用戶的地方會運行報錯。 我們把一個庫使用了不屬于其 depdencies 里的 package 稱之為 phantom depdencies,phantom depdencies 不僅會存在庫里,當我們使用 monorepo 管理項目的情況下,問題更加嚴重,一個 package 不但可能引入 DevDependency 引入的 phantom 依賴,更很有可能引入其他 package 的依賴,當我們部署項目或者運行項目的時候就可能出問題。 在基于 yarn 或者 npm 的 node_modules 的結構下,doppelganger 和 phantom dependency 似乎并沒有太好的解決方式。其本質是因為 npm 和 yarn 通過 node resolve 算法配合 node_modules 的樹形結構對原本 depdency graph 的模擬,哪有沒有更好的模擬方式能夠避免上述問題呢。

Semver 當理想遇到現實

npm 對 package 版本號采用語義化版本,Semver 本身也是為了解決 Depdency Hell 而引入的解決方案,如果你的項目引入的第三方依賴越來越多,你將會面臨一個困境

  • 如果你為你的每一個版本都寫死依賴,那么如果某個底層的依賴需要修復或者升級,你難以評估這個升級會修復的影響范圍,這可能導致級聯反應,與其協作的任何包都可能會掛掉,導致整個系統都需要全量的測試回歸,最后的結果很大可能是整個應用徹底鎖死版本,再也不敢做任何升級改動

因此 semver 的提出主要是用于控制每個 package 的影響范圍,能夠實現系統的平滑升級和過渡,npm 每次安裝都會按照 semver 的限制,安裝最新的符合約束的依賴。

這樣每次 npm install 都會安裝符合"^4.0.0"約束的最新依賴,可能是 4.42.0 的版本。 如果所有的庫都能完美的遵守語義化版本,那么世界和平,然而現實是很多庫因為種種原因并未遵守 semver,原因包括- 不可預知的 bug,本來以為某個版本只是 bugfix,發布了 patch版本,但是該 patch卻引入了未預料的 breaking change 導致 semver 被破壞。

  • semver 的設計過于理想,實際上即使是最小的 bugfix,如果業務方無意中依賴了這個 bug,仍然會導致 breaking change,bug 和 breaking change 的界限是模糊的。
  • 認為 semver 沒有太大意義,例如 Typescript 官方就承認從未遵循semver語義,實際上typescript經常在minor版本引入各種breaking change。httpt/issues/1

lock 非靈藥

那么在現實世界該如何處理這種問題,你肯定不希望自己的代碼在本地是正常運行的,但是當你上線的時候就掛了吧。 在你的測試完成和業務上線前的 gap 期間,如果你的某個依賴不遵循 semver,產生了 breaking change ,那么你可能得半夜上線查 bug 了。我們發現問題的根源在于如何保證測試時候的代碼和上線的代碼是完全一致的。

直接寫死版本

一個很自然的想法就是,我直接把我的第三方依賴版本都寫死不就行了

然而問題并沒這么簡單,雖然你鎖定了 webpack 的版本,但是 webpack 的依賴卻沒法鎖定,如果 webpack 的某個依賴法生產不遵循 semver 的 breaking change,我們的應用還是會受到影響,除非我們保證所有的第三方以及第三方的依賴都是寫死版本,這即意味著整個社區放棄 semver,這顯然是不可能的。

yarn lock vs npm lock

一個更加靠譜的寫法是將項目里的依賴和第三方的依賴同時鎖定,yarn 的 lock 和 npm 的 lock都支持該功能,一個常見的 lock 文件如下 如我們的項目安裝了express 的依賴

其 lock 文件如下

我們發現 express 的所有依賴及其依賴的依賴的版本在 lock 文件里都鎖定了,這樣另一個用戶或者環境,能夠憑借 lock 文件復現 node_modules 里各個庫的版本。 然而還是有一些場景 lock 無法覆蓋,當我們第一次安裝創建項目時或者第一次安裝某個依賴的時候,此時即使第三方庫里含有 lock 文件,但是 npm install|(yarn install) 并不會去讀取第三方依賴的 lock,這導致第一次創建項目的時候,用戶還是會可能觸發 bug。這在全局安裝 cli 的場景下非常常見,經常會碰到上一次安裝全局 cli 的時候正常,但是重新安裝這個版本的 cli 卻掛了,這很有可能是該 cli 的版本的某個上游依賴發生了 breaking change,因為不存在全局環境的 lock,因此目前沒有較好的解決方式。

Resolutions 救火隊長

如果你某天安裝了一個新的 webpack-cli,卻發現這個 webpack-cli 并不能正常工作,經過一番定位發現,是該 cli 的一個上游依賴 portfinder 的最近一個版本有 bug,但是該 cli 的作者在休假,沒辦法及時修復這個cli,但項目趕著上線該怎么處理?yarn 提供了一個叫做https://classic.yarnpkg.com/en/docs/selective-version-resolutions/ 的機制,使得你可以忽略 dependency 的限制,強行將 portfinder 鎖定為某個沒有 bug 的版本,以解燃眉之急

npm 本身沒有提供 resolution 機制,但是可以通過 npm-froce-resolution這個庫實現類似機制

庫里應該提交lock文件嗎

前面提到 npm 和 yarn 在 install 的時候并不會讀取第三方庫里的 lock 文件,那么我們編寫庫的時候還有必要提供 lock 文件嗎。 不知道大家有沒有過這種經驗,某天發現了某個第三方庫存在某個 bug,摩拳擦掌的將該庫下載下來,準備修復下發個 mr,一頓npm install && npm build 操作猛如虎,然后就見到了一堆莫名其妙的編譯錯誤,這些錯誤很可能是編譯工具的某個上游依賴的 breaking change 所致,經過一番 google + stackoverflow 仍然沒有修復,這時候就基本上斷了提 mr 的沖動,如果庫的開發者將當前的編譯環境的 lock 提交上來,則很大程度上可以避免該問題。

determinism !!!

determinism 指的是在給定 package.json 和 lock 文件下,每次重新 install 都會得到同樣的 node_modules 的拓撲結構。 事實上 yarn 僅保證了同一版本的確定性而無法保證不同版本的確定性,npm 則保證了不同版本的確定性。 版本確定性 !== 拓撲確定性 我們之前說到 yarn.lock 保證了所有第三方庫和其依賴的版本號是鎖定的,雖然保證了版本,但是實際上 yarn.lock里并沒有包含任何的 node_modules 拓撲信息

如上面的例子,該 lock 文件只保證了 has-flag 的版本和 suppors-colors 的版本,卻沒有保證 has-flag 是出現在 top level 還是出現在 supports-color 里,如下兩種拓撲結構都是合理的 第一種

第二種

與之相比 npm 的 lock 信息則包含了拓撲結構信息

上述結構表明 has-flag 和 supports-color 處于同一層級

而如上的 lock 文件我們可以看出,define-property 和 is-accessor-descritpor 等依賴是放在 base 里的 node_modules 的

拓撲結構 matters

大部分場景下鎖定版本號 + depdency 的拓撲結構一致基本上已經沒啥問題了,即使 node_modules 的拓撲結構不一致,也不會產生問題,然而在某些場景下仍然會有問題。 如下的代碼實際上是對 ndoe_modules 的拓撲結構有強假定,一旦 @types 的位置出現問題就可能存在問題。

這也要求我們讀取第三方的依賴的時候不要使用任何的相對路徑,而是應該通過 require.resolve 來讀取模塊的路徑,然后再基于此路徑去進行查找。 相對于哪個目錄 相對路徑的另一個問題,就是意義不明 以 babel 為例,當我們用 babel 去編譯代碼的時候,一般涉及到三個目錄

  • 當前的工作區:即 process.cwd() 的返回值這里是 my-project
  • 當前代碼的位置:即 my-project/build.js
  • 編譯工具的位置:即 xxx/node_modules/@babel/core

問題來了,這里的@babel/preset-env位置是相對于誰呢,這完全取決于 babel/core 里的內部實現。

monorepo:link is hard

如果說第三方庫里存在的依賴問題一定程度上還比較可控,那么當我們進入 monorepo 領域,問題就會被加倍放大。當我們用一個倉庫管理多個 package 的時候,有兩個比較嚴重的問題

  • 第三方依賴的重復安裝問題,如果 packageA 和 packageB 里都使用了 lodash 的同一版本,沒有優化的情況下,需要兩個 package 都重復安裝相同的 lodash 版本
  • link hell: 如果 A 依賴 B,B 依賴 C 和 D,我們每次開發,都需要執行將 C 和 Dlink 到 B 里,如果拓撲圖很復雜的話,手動做這些 link 操作是難以接受的

無論是 lerna 還是 yarn 工作機制核心都是

  • 將所有 package 的依賴都盡量以 flat 模式安裝到 root level 的 node_modules 里即 hoist,避免各個 package 重復安裝第三方依賴,將有沖突的依賴,安裝在自己 package 的 node_modules 里,解決依賴的版本沖突問題
  • 將各個 package 都軟鏈到 root level 的 node_modules 里,這樣各個 package 利用 node 的遞歸查找機制,可以導入其他 package,不需要自己進行手動的 link
  • 將各個 package 里 node_modules 的 bin 軟鏈到 root level 的 node_modules 里,保證每個 package 的 npm script 能正常運行。

這種方式盡管解決了依賴重復和 link hell 兩個核心問題,卻引入了其他問題

  • packageA 可以輕松的導入 packageB,即使沒有在 packageA 里聲明 packageB 為其依賴,甚者 packageA 可以輕松地導入 packageB 的第三方依賴,這實際上將 Phantom dependency 加劇放大了
  • packageA 里的依賴和 packageB 的第三方依賴的沖突可能性更大了,如 packageA 用了 webpack3 和 packageB 用了 webpack4,這就很容易產生沖突,實際上是加劇了 doppelgangers 問題

hoist并非安全,考察如下結構

實際上 html-webpack-plugin 運行時會依賴 webpack

在 hoist 前,react-scripts 會調用 html-webpack-plugin ,繼而調用 webpack,根據 node 的 resolve 算法,會優先使用最近的 node_modules 里的 webpack 版本即這里的 webpack@2 但在 hoist 后,按照鄰近原則則會使用 root-level 的 webpack 版本即 webpack@1 這樣就會造成運行時錯誤。 而對于 yarn 和 npm,其優先會使用 hoist,只有當本地版本和 root 的沖突的時候,才不進行 hoist 的操作(甚至你沒辦法判定當存在多個版本的時候哪個版本會被 hoist 到 root level)。 這個問題并不局限于 webpack,eslint、jest、babel 等只要涉及到 core 及其插件的都會受此影響。 因此 react 官方為了解決這個問題,特地搞了個 preflight 檢查(https://github.com/facebook/create-react-app/pull/3771),用于檢查當前用戶的 react-scripts 的 node_modules 及其祖先的 node_modules 里 babel 和 webpack 版本的一致性,一旦檢測出版本不一致則給出 warning 直接退出

由于 hoist 本身的一些缺陷,這也是導致 React 廢棄了 monorepo 支持的一大原因,該mr合并后被 revert。yarn 還有一種更為激進的模式,即 --flat 模式,該模式下 node_modules 里的各個 package 只允許才一個一個版本的存在,當出現版本沖突的時候,你需要自己選擇指定一個版本(即通過指定在 resolution 里,強控版本),這在大型項目中顯然行不通,因為第三方庫里存在大量的版本沖突問題(僅 webpack 內就存在 160+ 個版本沖突),這樣說明了 doopelganges 的嚴重性,強制指明所有版本不能解決問題。

PNPM: Explicit is better than implicit.

在不考慮循環依賴的情況下,我們實際的 depdency graph 實際上某種有向無環圖( DAG ),但是 npm 和 yarn 通過文件目錄和 node resolve 算法模擬的實際上是有向無環圖的一個超集(多出了很多錯誤祖先節點和兄弟節點之間的鏈接),這導致了很多的問題,所以我們需要個更加接近 DAG 的模擬。pnpm 正是采取了這種解決方式,通過更加精確的模擬 DAG 來解決 yarn 和 npm 代理的問題。

phantom dependency

相比于 yarn 盡可能的將 package 放到 root level,pnpm 則是只將顯式寫明的 dependency 的依賴寫入 root-level 的 node_modules,這避免了業務里錯誤的引入隱式依賴的問題,即解決了 phantom dependency 以如下例子為例

但在我們的代碼里卻可以使用 debug 模塊,因為這是 express 引入的模塊,雖然我們自己沒有顯式的引入

// src/index.js
const debug = require('debug')

如果有一天 express 決定將 debug 模塊換成了better-debug模塊,那么我們的代碼就會掛掉。 npm 的結構

pnpm 的結構

我們發現在頂層 node_modules 只有 express 模塊,沒有 debug 模塊,因此我們無法在業務代碼里錯誤的引入 debug,同時每一個第三方庫里都有自己的 node_modules 目錄,每一個 node_modules 目錄都包含了自己的 depdency 的軟鏈,這樣保證在 express 里可以正確的加載debug版本。

doppelgangers

pnpm在解決 phantom depdency 問題的同時,在此基礎上也解決了 doopelganger 問題。 考慮如下代碼

// package.json
{ 
  "dependencies": {
    "debug": "3",
    "express": "4.0.0",
    "koa": "^2.11.0"
  }
}

使用 pnpm 安裝相關依賴后,我們發現項目中存在 debug 的兩個版本

dependencies:
debug 3.1.0
express 4.0.0
├── debug 0.8.1
├─┬ send 0.2.0
│ └── debug 1.0.5
└─┬ serve-static 1.0.1
  └─┬ send 0.1.4
    └── debug 1.0.5
koa 2.11.0
└── debug 3.1.0%

查看 node_modules 里的版本,我們發現區別于 yarn,pnpm 是將不同版本放在同一層級里通過軟鏈選擇加載版本,而 yarn 則是放在不同層級,依賴遞歸查找算法來選擇版本

我們發現 pnpm 的 node_modules 里包含了三個版本,并且不同的模塊分別連接到了三個版本

這樣即使出現版本沖突,只需要將各個模塊進行鏈接即可,并不需要每個模塊再進行重復安裝模塊。 我們可以發現 pnpm 避免直接依賴 node_modules 的遞歸查找依賴的性質,而是直接通過軟鏈解決了 phantom dependency 和 doppelgangers 問題。因為徹底的避免了包的重復問題,其節省了大量的空間和加快了安裝速度 以一個 monorepo 項目為例

對比一下 pnpm: node_modules大小 359M,安裝耗時 20s yarn: node_modules大小 1.2G,安裝耗時 173s 差別非常顯著

global store

pnpm 不僅僅能保證一個項目里的所有 package 的每個版本是唯一的,甚至能保證你使得你不同的項目之間也可以公用唯一的版本(只需要公用 store 即可),這樣可以極大的節省了磁盤空間。核心就在于 pnpm 不再依賴于 node 的遞歸向上查找 node_modules 的算法,因為該算法強依賴于 node_modules 的物理拓撲結構,這也是導致不同項目的項目難以復用 node_modules 的根源。(還有一種干法,就是使用代碼的地方寫死依賴的版本號,這是 deno 的干法)

cargo: 全局store的包管理系統

實際上除了 node 的 npm,很少有其他的語言是需要每個項目都維護一個 node_modules 這種依賴(聽說過其他語言有 node_modules hell 的問題嗎),其他語言也很少有這種遞歸查找依賴的做法,所以其他語言很多都采用了全局store的管理系統。我們可以看一下 rust 是如何進行包管理的。 新建一個 rust 項目很簡單,只需要運行

$ rust new hello-cargo // 創建項目,包含可執行的binary
    $ rust new hello-lib --lib // 創建lib,

其生成目錄結構如下

.
    ├── Cargo.toml
    └── src
        └── main.rs

其中的 Cargo.toml 和 package.json 的功能幾乎一致(相比 json,tom 支持注釋),包括如下一些信息

// src/main.rs
fn main() {
    println!("Hello, world!");
}

其中的 dependencies 用于存放第三方依賴 cargo 的 src/main.rs 為項目的主入口,類似 index.js

// src/main.rs
fn main() {
    println!("Hello, world!");
}

cargo 內置了 rust 的編譯功能(相比于 js 生態里豐富的工具,cargo 內置 rustc 編譯的好處是很明顯,所有的第三方庫只需要提供源碼即可,cargo 自己完成第一方依賴的遞歸編譯操作)

$ cargo build // 編譯生成binary文件
$ cargo run // 執行binary文件

我們嘗試添加一個第三方依賴看看,與 npm 類似,cargo 的 dependencies 也支持 git 協議和 file 協議。

[dependencies]
time = "0.1.12"
rand = { git = "https://github.com/rust-lang-nursery/rand.git" } // 支持git協議

執行 build 安裝依賴,此時發現多了個 Cargo.lock,其類似于 yarn.lock 文件,里面包含了第三方庫的及其依賴的確定性版本

# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "hello_cargo"
version = "0.1.0"
dependencies = [
 "time",
]
[[package]]
name = "libc"
version = "0.2.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005"
[[package]]
name = "time"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
dependencies = [
 "libc",
 "winapi",
]
[[package]]
name = "winapi"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
dependencies = [
 "winapi-i686-pc-windows-gnu",
 "winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

此時的目錄結構如下

.
├── Cargo.lock
├── Cargo.toml
├── src
└── target // 編譯產物

我們發現項目里并沒有類似 node_modules 存放項目所有依賴的東西。 那么他的依賴都存放到哪里了? cargo home cargo 將所有的第三方依賴的代碼,都存放在了稱為 cargo home的目錄里,默認為~/.cargo,其包含三個主要目錄

/bin  // 存放executable的bin文件
/git // 存放從git協議拉取的第三方庫代碼
/registry // 存放從registry拉取的第三方庫代碼

monorepo支持

cargo本身也提供了對 monorepo的支持,和 yarn 類似,cargo 也是通過 workspace 的概念來支持 monorepo

// Cargo.tom
[workspace]
members = [
    "adder",
    "hardfist-add-one"
]

其幾乎等價于下面的 yarn.lock

// package.json
{
  "workspaces":["adder","hardfist-add-one"]
}

我們看下 workspace 的目錄結構

和 yarn 類似,其公用一個 Cargo.lock 文件 我們可以通過本地 file 將各個庫進行鏈接 假設 monorepo 里 adder 依賴 hardfist-add-one

// adder/Cargo.toml
[package]
name = "hardfist-adder"
version = "0.1.0"
authors = ["hardfist <1562502418@qq.com>"]
edition = "2018"
description = "test publish"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
hardfist-add-one = { path = "../hardfist-add-one", version="0.1.0" }
rand="0.7"

我們可以將 hardfist-add-one 通過 path 協議指向本地。 當我們需要將 adder 進行發布的時候,cargo 是不允許發布只包含 path 路徑的 dependency 的,因此我們需要同時給 hard-fist-one 指明 version 用于發布。

禁止隱式依賴

雖然在同一個 workspace 里,如果我們的 hardfist-add-one 依賴了 rand,同時 hardfist-adder 依賴了hardfist-add-one,如果 hardfist-adder 本身沒有將rand聲明為其依賴,cargo 則進行報錯處理

每次執行編譯的時候,cargo 都會自動的遞歸編譯其所有依賴,并不需要額外工具支持。 因為大部分的 cargo 應用都是打包成一個 binary,所以也不存在下面所述的 node 里的 monorepo 里的 vendor 的問題 cargo 同時也支持 offline 模式,支持離線安裝依賴

vendor: for serverless

monorepo 還存在的一個較大的問題就是如何分別部署每個 package,這在 serveless 場景下問題更為突出,因為一般 serveless 的環境對于用戶的資源大小都有較大的限制。 當我們使用 monorepo 管理應用時,部署存在兩個問題

  1. 第三方依賴都安裝到 root level 上,導致 package 內的 node_modules 并不包含所有的依賴信息,在 scm 等構件產物的地方,我們只能選擇將所有 package 的在 root-level 的 node_modules 一起打包
  2. 由于各個 package 是通過軟鏈來實現互相支持導入的,這導致即使我們打包了 node_modules,里面仍然只是包含依賴 package 的軟鏈,仍然會存在問題。

針對這個問題,基本上有兩種解決方式

  • 通過打包工具將代碼打包為一個 bundle,運行時不依賴 node_modules
  • 將 hoist 的第三方依賴和 link 的 package 都抽取出來放到 package 里的 node_modules 里

這兩種方案遇到的最大問題就是隱式依賴 bundle 對于前端應用 bundle 習以為常,但是對于服務端應用 bundle 卻并不常見,實際上很多的服務端語言都是采用 bundle 的方案,如 deno、rust、go 等,上線的都是一個 bundle 文件,這個 bundle 文件可能是binary也可能是其他格式。 實際上 node 生態里即使服務端也有一些比較成熟 bundle 方案,如 github.com/zeit/ncc, 其會智能的處理將 server 端的代碼 bundle 成一個 js 文件,更有甚者可以將 runtime 連同業務代碼打包為一個 binary 文件,如 github.com/zeit/pkg 的方案。 服務端 bundle 存在最大的問題就是文件讀寫和動態導入,因為編譯功能無法在編譯時獲取需要讀寫|導入文件的的信息,因此很難適用于一些約定大于配置的框架(如 egg 和 gulu),但如果是 express 和 koa 這種需要顯示的寫明依賴的框架,是沒有問題的。

最多閱讀

為Electron程序添加運行時日志 3年以前  |  14867次閱讀
Node.js下通過配置host訪問URL 3年以前  |  4765次閱讀
js動態創建類和實例化 3年以前  |  3902次閱讀
wordpress標簽頁的制作 3年以前  |  3739次閱讀
初探 React 組件 3年以前  |  3712次閱讀
用 esbuild 讓你的構建壓縮性能翻倍 2年以前  |  3699次閱讀
500行PHP代碼搞定富文本安全過濾 3年以前  |  3606次閱讀
10 種跨域解決方案(附終極方案) 2年以前  |  3508次閱讀
22個HTML5的初級技巧 3年以前  |  3480次閱讀
MBTI免費在線測試 3年以前  |  3428次閱讀
使用 SRI 增強 localStorage 代碼安全 3年以前  |  3357次閱讀
淺談瀏覽器的原生拖拽事件 3年以前  |  3352次閱讀
CSS清除浮動 3年以前  |  3348次閱讀
第三版主題上線 3年以前  |  3285次閱讀
利用服務器返回header來傳輸數據 3年以前  |  3256次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>