成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

聊一聊 NPM 依賴管理的復雜性

開發 前端
本文希望更聚焦討論 Node 場景下的依賴 —— 或者更直觀的說是 NPM Package 結構的不穩定性所帶來的被嚴重低估的質量風險,以及相應的應對策略。

這是一個很少被提及的話題 —— 「依賴管理」(Dependencies Management) 。

在開源文化盛行的現代,多數時候我們都不必從零開始搭建一套軟件系統,轉而可以借助諸多開放的代碼片段及其他資源更快速高效開發軟件應用,這算的上軟件工程發展史上一次巨大革命,因為它能大幅提升軟件工業的生產效率,我們不必再從底層開始編寫所有代碼,大部分問題與常見的編程模式都能在社區找到相應的解決方案,且這些被反復消費錘煉的軟件包通常有更高的穩定性與性能,你需要做的只是花一些時間了解學習這些開源資源,并在項目使用它們,“ 「依賴」 ”它們即可,這已經是一種被不斷實踐,不斷被驗證為行之有效的開發模式。

但現實比預想的復雜許多,如果你開發的只是規模較小,或生命周期非常短的項目時,依賴的狀態并不會造成多大問題,你只要確保當下執行 ok,功能符合預期即可。但,任何軟件項目一旦疊加“「規模」”與“「時間」”兩個變量之后,依賴網絡就很容易變得復雜混亂,如果不及時施加恰當的管理手段則遲早會引發諸多晦澀難懂的穩定性、性能、安全等諸多方面的問題。因此,我們需要學習了解“依賴管理”的基本含義與潛在風險,需要掌握一些軟件工程管理方面的方式方法,確保依賴網絡及其隨時間發生的變化都在可控范圍內,讓依賴網絡盡可能保持一個清晰有條理,且具備一定程度的健壯性。

在展開具體內容之前,我們先明確一下“依賴”這個概念,嚴格來說,你的代碼所需要消費的任何直接與間接資源都歸屬于“依賴”范疇,往小了說,包括系統時間、語言特性(如事件循環、閉包)、執行環境(如瀏覽器接口、node 接口)等;往大了說還包括:操作系統、網絡,甚至硬件設備(如 GPU),但這些并不在本文討論范圍內,誠然這些要素也可能帶來性能、安全性各方面的影響,但多數屬于基礎設施,穩定性還是比較有保障的。

本文希望更聚焦討論 Node 場景下的依賴 —— 或者更直觀的說是 NPM Package 結構的不穩定性所帶來的被嚴重低估的質量風險,以及相應的應對策略。

第三方依賴帶來的問題

09 年 NodeJS + NPM 的出現,不僅讓 JavaScript 擁有了脫離瀏覽器環境執行的能力,也帶來一套相對體系化的依賴管理方案,在此之前的依賴管理多數由“人”手工完成,需要用到什么就手動 copy 代碼進倉庫,或者 copy cdn 鏈接到 HTML 頁面。

而 NPM(Node Package Manager) 讓這件事情 「盡可能」 做到了自動化,我們只需要執行 npm install 命令即可自動完成下述工作:

  • 解析依賴樹:根據項目 package.json 文件中的依賴項列表,遞歸檢查每個依賴項及子依賴項的名稱和版本要求,構建出依賴樹并計算每一個依賴需要安裝的確切版本(這個并不容易做到,參考:Version SAT);
  • 參考:https://research.swtch.com/version-sat
  • 下載依賴項:構建出完整的依賴樹后,npm 會根據依賴項的名稱和版本,下載相應的依賴包,下載過程還會對依賴包做一系列安全檢查,防止被篡改;
  • 安裝依賴項:當依賴項下載完成后,npm 將它們安裝到項目的 node_modules 目錄中。它會在該目錄下創建一個與依賴項名稱相對應的文件夾,并將軟件包的文件和目錄解壓復制到相應的位置(不同包管理器最終產出的包結構不同);
  • 解決依賴沖突:在安裝依賴項的過程中,可能會出現依賴沖突,即不同依賴項對同一軟件包的版本有不同的要求。npm 會嘗試解決這些沖突,通常采用版本回退或更新來滿足所有依賴項的要求;
  • 更新 package-lock.json:在安裝完成后,npm 會更新項目目錄下的 package-lock.json 文件。該文件記錄了實際安裝的軟件包和版本信息,以及確切的依賴關系樹,可用于確保在后續安裝過程中保持一致的依賴項狀態(npm ci);

PS: 本文僅以 NPM 舉例,yarn、pnpm 的執行算法雖差異較大,但整體遵循上述過程,因此不再贅述。

相比于過往人工管理的各種低效且容易出錯的騷操作,NPM 這類包管理器能以極低的成本,更規范化、自動化完成依賴包的檢索、安裝、更新等管理動作,更容易搭建出一個相對穩定且安全可靠的工程環境,也更容易復用外部那些經過良好封裝、充分測試的代碼片段。

But,伴隨著工程能力的提升,依賴之間的復雜度也在急劇增長,當前我們正面臨著更多依賴管理相關的工程問題,例如:幽靈依賴、版本沖突、依賴地獄等等,這些問題很少被討論卻時時刻刻影響著工程項目的穩定性、開發效率、性能等要素,接下來我會盡可能完整討論依賴管理的方方面面,幫助大家更深入了解這些潛藏在日常開發之下很少被察覺的各類問題,并討論相關的應對方案。

依賴管理潛在的問題

1. semver 并不穩定

先從依賴管理中最淺顯直觀的視角講起,當我們決定使用某一個 NPM 包時,需要做的第一件事就是在項目 package.json 文件中定義 dependencies ,類似于:

{
  "name": "foo",
  "dependencies": {
    "lodash": "^1.0.0"
  }
}

這似乎已經是一種簡單而自然,不需要過多討論的常識,but,我們應該依賴于 Package 的那些版本呢?

答案取決于具體的功能需求、穩定性、性能等諸多因素,但一個大致通用的實踐是:「盡可能使用最新版本的范圍版本」,例如假定 React 最新版本為 18.2.0,在項目中可以聲明依賴為 "react": "^18.2.0",這種方法一方面能夠應用最新版本 —— 這可能意味著更多的功能,以及更好的性能等;另一方面,借助 ^ 聲明該依賴接受 >= 18.2.0 < 19 的版本范圍,在 React 下次發布 18.2.1 或更大版本時都能自動匹配應用,以此獲得一定范圍內動態更新依賴的能力。

PS:補充一個知識點,當前多數框架都遵循 semver 版本號(https://semver.org/)規則,即包含 Major.Minor.Patch 三段版本號,Major 代表較大范圍的功能迭代,通常意味著破壞性更新;Minor 代表小版本迭代,可能帶來若干新接口但“承諾”向后兼容;Patch 代表補丁版本,通常意味著沒有明顯的接口變化。

這看似很完美,但實踐卻漏洞百出。首先,部分 NPM 包作者并沒有嚴格遵守 semver 定義的規則迭代版本號,特別是許多公司內部依賴的版本管理更是混亂不堪,Patch 可能破壞原本的接口定義(多一個參數少一個參數),Minor 可能導致向后不兼容,等等,致使舊代碼無法正常執行。

其次,即使完全按照 semver 語義嚴格管理版本號,誰又能保證每次版本迭代都能完美符合用戶預期呢?例如按照 semver 語義,Patch 只用作 bug 修復,但難保會在一些邊界情況發生變化,比如:「日志」,講道理用戶不應該直接依賴代碼包的日志輸出,但可能有一些輸入輸出沒有覆蓋用戶需求,或者用戶沒有了解到正確的使用方式,致使消費者傾向于直接從運行日志解讀信息(只要用戶體量足夠大,總會出現一些意料之外的使用方法),若此時 Patch 版本更改了日志內容 —— 這看似很合理,卻可能導致日志解析失敗。從這個示例來說,日志算不算補丁更新呢?這種情況下,Patch 還是安全的嗎?

那么,能不能放棄范圍版本,寫死版本號呢?例如上例中只要把依賴關系寫死成 "react": "18.2.0" 似乎就能規避版本變化帶來的不確定性?某種程度上確實如此,但這又會帶來新的風險:版本累積可能帶來更大的破壞性更新!我們必須承認一個事實:無論你有多強的惰性,「軟件項目只要存活的時間足夠長,就總會有一天需要升級依賴」,升級的主因可能是:安全、合規、性能、架構調整等等,如果你從一開始就在使用某個固定版本,直到不得不更新的時刻到來時,新版本的使用方案、功能表現等可能都已經發生了劇變(例如,從 React 17 => 18),很可能會導致你原本運行良好的程序漏洞百出,質量風險、回歸成本都很高。

因此,「良好的依賴管理策略應該在保證穩定的前提下,定期跟進依賴包的更新」,小步快進將升級風險分攤到每一次小版本迭代中,為達成這一效果,一個比較 「常見」 的實踐是在開發環境中使用適當的范圍版本,在測試 & 生產環境使用固定版本,以 NPM 為例,可以繼續沿用 "react": "^18.2.0",在開發態中使用 npm install 安裝依賴,在測試 & 生產環境則使用 npm ci 命令,兩者區別在于 npm install 會嘗試更新依賴,觸發依賴結構樹變化并記錄到 package-lock.json 文件;而 npm ci 則嚴格按照 package-lock.json 內容準確安裝各個依賴版本,在 CI/CD 環境中能獲得更強的穩定性,確保代碼行為與開發環境盡可能一致。

2. 依賴類型

在確定依賴版本之后,接下來需要決定將依賴注冊到那個 dependencies 節點,按 package.json 規則,可選類型有:

  • dependencies:生產依賴,指在軟件包執行時必需的依賴項。這些依賴項是你的應用程序或模塊的核心組成部分,當你部署到生產或測試環境時,這些依賴項都需要被安裝消費;
  • devDependencies:開發依賴,僅在開發過程中需要使用的依賴項,通常包括測試框架、構建工具、代碼檢查器、TS 類型庫等。開發依賴項不需要在生產環境安裝;
  • peerDependencies:對等依賴,用于指定當前 package 希望宿主環境提供的依賴,這解釋有點繞,下面我們會展開解釋;
  • optionalDependencies:可選依賴,當滿足特定條件時可以選擇性安裝的依賴,且即使安裝失敗,安裝命令也不會中斷。可選依賴項通常用于提供額外的功能或優化,并不是必需的;
  • bundledDependencies:捆綁依賴,用于指定需要一同打包發布的依賴項,用的比較少。

根據我們正在開發的軟件包的用途及對依賴的使用方式,這里會有不同的決策邏輯。

假設正在開發的是“頂層”應用(Web APP、Service、CLI 等),那么多數依賴都可以注冊到 devDependencies 或 dependencies 節點,這也是我們日常應用比較多的依賴類型。兩者主要差異在于:dependencies 是生產環境依賴,是確保軟件包正常運行的必要依賴項;而 devDependencies 則是僅在開發階段需要使用的依賴項。

舉個例子,假設你應用邏輯中直接使用了 lodash 的方法,那么 lodash 必然是 dependencies;但假設你只是在一些構建腳本之類的非應用邏輯中使用了 lodash ,那么應該將其注冊到 devDependencies 中。

PS:對于需要將代碼和依賴全部打包在一起的應用 —— 例如常見的基于 Webpack 的 web 應用,從功效上 dependencies 與 devDependencies 并無差別,但建議還是根據語義對依賴做好分類管理。

換個視角,假設正在編寫的代碼最終會被發布成 NPM Package 供其他方消費,那么我們必須慎重許多,因為你的決策會深刻影響消費者的使用體驗。首先,你必須非常謹慎地使用 dependencies,因為 NPM 在安裝你這個 Package 會順帶將你的 package.json 中的 dependencies 也都安裝一遍,錯誤的依賴分類可能會帶來一些影響開發體驗的 Bad Case:

  • 需要占用更多的安裝依賴的時間;
  • 依賴結構更復雜,容易導致“菱形依賴”(后面會會展開解釋)問題;

舉個例子,@vue/cli 的 package.json 部分內容如下:

{
  "name": "@vue/cli",
  "version": "5.0.8",
  ...
  "dependencies": {
    ...
    "vue": "^2.6.14",
    ...
  },
  ...
}

那么使用者安裝 @vue/cli 之后,還會強制安裝 vue@^2.6.14 版本 —— 即使用戶消費的可能是其他 Vue 版本,這種行為無疑都會給用戶增加不必要的負擔,因此,在開發 Package 時,除非有非常明確且強烈的訴求,否則都應該優先使用 devDependencies!

那么,假設你的 Package 確實存在一些必要,但又不適合注冊到 dependencies 的依賴,該怎么辦呢?這種 Case 也非常常見,例如 Webpack 插件通常對 Webpack 存在強依賴,但并不適合直接使用 dependencies,否則可能導致用戶安裝多份 Webpack 副本。針對這種情況 NPM 提供了另外一種依賴類型:peerDependencies,語義上可以理解為:Package 希望宿主環境提供的“對等”依賴,NPM 對這種類型的處理邏輯稍微有點復雜:

若宿主提供了對等依賴聲明(無論是 dependencies 還是 devDependencies),則優先使用宿主版本,若版本沖突則報出警告:

若宿主未提供對等依賴,則嘗試自動安裝對應依賴版本(NPM 7.0 之后支持)。

PS:正是因為 peerDependencies 的復雜性,不同包管理器,甚至同一包管理器的不同版本對其處理邏輯都有所不同,例如 NPM 在 3.0 之前支持自動安裝 peerDependencies,但這一特性帶來的問題比較多,3.0 之后取消了自動下載,交由消費者自行維護,一直到 7.0 版本設計了一種更高效的依賴推算算法之后,才又重新引入這一特性。

peerDependencies 能幫助我們實現:“「即要」”確保 Package 能正常運行,“「又要」”避免給用戶帶來額外的依賴結構復雜性,在開發 NPM Package,特別是一些“框架”插件、組件時可以多加使用,實踐中通常還會:

  • 使用 peerDependencies 聲明 Wepack 為對等依賴,要求宿主環境安裝對應依賴副本。
  • 同時使用 devDependencies 聲明 Wepack 為開發依賴,確保開發過程中能正確安裝必要依賴項。

接下來聊一個相對冷門的類型:optionalDependencies,也就是“可選”依賴,雖然多數時候我們對 Package 的依賴應該是比較明確的:要么有要么沒有,但某些特定場景下也可能是“可以有也可以沒有”。

舉個例子,fsevents 是一個針對 「Mac OSX」 系統的文件系統事件監控庫 —— 注意啊,它只適用于 「Mac OSX」 系統,因此在其他操作系統上都不能使用 —— 自自然然的也不需要安裝這個 Package,因此可以是一個“可選”依賴,實際上在知名構建工具 rollup 中就是以 optionalDependencies 方式引入 fsevents 的:

{
  "name": "rollup",
  "version": "4.1.4",
  // ...
  "optionalDependencies": {
    "fsevents": "~2.3.2"
  },
  // ...
}

需要注意,optionalDependencies 意味著“可能有也可能沒有”,因此消費方式上也需要加以區分,例如 rollup 是這么導入 fsevents 的:

import type FsEvents from 'fsevents';

export async function loadFsEvents(): Promise<void> {
  try {
    // 使用 `import` 函數異步導入,并做好異常判斷
    ({ default: fsEvents } = await import('fsevents'));
  } catch (error: any) {
    fsEventsImportError = error;
  }
}

// ...

代碼位置:rollup/src/watch/fsevents-importer.ts

optionalDependencies 非常適合用作處理“平臺”強相關的依賴,除此之外還可用于性能兜底、交互功能兜底等場景,這里就不一一贅述了。

簡單總結下,package.json 提供了若干影響安裝行為的依賴類型屬性,以應對不同場景的管理需求,開發者需要基于性能、可用性、穩定性等角度考慮謹慎判斷依賴類型。當然,也有一些基本規則能幫助我們快速識別依賴類型,包括:

  • 常見的各類工程化工具,如 eslint、vitest、vite、jest、webpack 等等都適合放在 devDependencies。
  • 各類 TS 類型包,例如 @types/react、@types/react-dom 一般也可以放在 devDependencies 中。
  • 開發框架插件時,盡可能將框架聲明為 peerDependencies,例如 webpack 與 cache-loader。
  • 平臺強相關的依賴,可以考慮使用 optionalDependencies,之后配合 postinstall 鉤子執行平臺相關的依賴安裝 or 編譯動作。
  • 等等。

3. 失控的依賴結構

思考一下:「安裝某個依賴時,需要附帶安裝多少子孫依賴」?很多同學此前可能沒關注過這一塊,這個問題并沒有具體的通用答案,取決于你實際安裝的包,但這個數量通常都不會很小。舉個例子,知名的 React 組件庫 antd 的依賴結構是這樣的:

這張圖肉眼可見的復雜。。。一旦我們決定使用 antd 則必須引入這一坨復雜的依賴結構,而這并不是孤例,不少知名框架都有類似問題,包括 jest、webpack、http-parser 等等,當我們依賴這些 Package 時,依賴結構最終會合并成一張龐大、復雜,且沖突不斷的網絡。

造成這一現象的原因其實不難理解,在當下開源文化環境下,跨組織的代碼共享變得如此簡單平常,即使是非常小的代碼片段都可以以極低的成本貢獻到社區供人使用。舉個例子,在 NPM 上有一個這么一個 Package:escape-string-regexp,它的核心代碼算上注釋才不到十行,但周下載量達到驚人的一億次:

export default function escapeStringRegexp(string) {
        if (typeof string !== 'string') {
                throw new TypeError('Expected a string');
        }

        // Escape characters with special meaning either inside or outside character sets.// Use a simple backslash escape when it’s always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns’ stricter grammar.return string
                .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
                .replace(/-/g, '\\x2d');
}

在此背景下,多數時候 —— 包括開發 Package 時,多數開發者在解決特定需求時自然都會傾向于使用開源代碼片段,這可以幫助作者跳過代碼「設計、開發、測試、調試、維護」等步驟,進而提升開發效率。

這本是一種良好實踐,但當它被廣泛采用時,不可避免的會帶來一個副作用:依賴粒度變得非常細小,依賴網絡結構變得無比復雜龐大,而這又容易(或者說必然)觸發更多負面效應,包括:

  • 需要計算依賴包之間的關系并下載大量依賴包,CPU 與 IO 占用都非常高,導致項目初始化與更新性能都比較差,我就曾經歷過初始 yarn install 需要跑兩個小時,加一個依賴需要跑半個小時的巨石項目。。。開發體驗一言難盡;
  • 多個 Package 的依賴網絡可能存在版本沖突,輕則導致重復安裝,或重復打包,嚴重時可能導致 Package 執行邏輯與預期不符,引入一些非常難以定位的 bug,這個問題比較隱晦卻重要,后面我們還會展開細講;
  • 由于可能存在大量沖突,項目的依賴網絡可能變得非常脆弱,某些邊緣節點的微小變化可能觸發依賴鏈條上層大量 Package 的版本發生變化,引起雪崩效應,進而影響軟件最終執行效果,這同樣可能引入一些隱晦的 bug;
  • 等等吧。

那么,如何應對這些問題呢?先說結論:沒有一勞永逸完美方案,只能盡力降低問題出現的范圍和影響。首先我們不應該因噎廢食,即使存在上述問題,依賴外置更有助于提升模塊之間的低耦合高內聚,保持更佳的可維護性,在此基礎上,可以適當引入一些管理措施緩解癥狀,包括:

  • 設定更嚴格的開源包審核規則:除了周下載量、Star 數這些指標外,可以適當打開倉庫看看代碼結構是否合理,是否有單測,單測覆蓋率多少,是否能通過單測,Issue 持續時間,二級依賴網絡結構是否合理等等,確保依賴的質量是穩定可信賴的;
  • 盡可能減少不必要的依賴:在引入第三方庫時,仔細審查其功能,看看是否真的需要使用整個庫,或者我們僅需要其中的部分功能,有時我們可能自己實現(甚至 Copy)這些功能更為快速、更為簡單,同時也減少了對第三方庫的依賴;
  • 分層依賴:如果項目較大(monorepo?),可以將項目分層,每一層只能依賴相同層級或更基礎的層級的庫,這樣可以降低各層之間的相互依賴,也有助于分層級管理依賴結構,減小變動對上游的影響;
  • 避免循環依賴:循環依賴絕對是一種可怕的災難!它不僅會急劇提升依賴網絡的結構復雜度,還很可能導致一些難以預料的問題,因此在做依賴結構審計時務必盡可能規避這類情況。

4. 幽靈依賴

“幽靈依賴”是指我們明明沒有在 package.json 中注冊聲明某個依賴包,卻能在代碼中引用消費該 Package,之所以出現這個問題,歸根到底主要是兩個因素引起的:

  • NodeJS 的模塊尋址邏輯;
  • 包管理器執行 install 命令后,安裝下來的 node_modules 文件目錄結構。

眾所周知(吧?),在 NodeJS 以及 Webpack、Babel 等常見工程化工具中,當我們使用 require/import 導入外部依賴包時,NodeJS 會首先嘗試在當前目錄下的 node_modules 尋找同名模塊,若未找到則沿著目錄結構逐級向上遞歸查找 node_modules 直至系統根目錄,例如在 /home/user/project/foo.js 文件中查找模塊時,可能會在如下目錄嘗試尋找 Package:

/home/user/project/node_module
/home/user/node_module
/home/node_module
/node_module

若此時某些 Package 被安裝在項目 project 路徑的上層,則必然會被尋址邏輯命中,導致代碼中能夠“錯誤”引用到這個包。其次,即使不考慮這個目錄遞歸尋址邏輯,NPM 與 Yarn 的扁平化 node_modules 結構也非常容易引起幽靈依賴問題。這里補充點歷史知識,在 NPM@3 之前,每個模塊的依賴項都會被放置在自己專屬的 node_modules 文件夾內,即所謂的"「嵌套依賴」",例如:

  • 依賴結構:
- A
  - B 
    - C 
- D
  - C
  • node_module 結構:
- node_modules
  - A
    - node_modules
      - B
        - node_modules
          - C
  - D
    - node_modules
      - C

這種方案非常容易導致依賴結構深度過大,最終可能導致文件路徑超過了一些系統的最大文件路徑長度限制(主要是Windows 系統),導致奔潰。這就引入 NPM@3 的優化策略:扁平化依賴結構,也就是將所有的模塊 —— 無論是頂層依賴還是子依賴,都會直接寫入到在項目頂層的 node_modules 目錄中,例如:

  • 依賴結構:
- A
  - B 
    - C 
- D
  - C
  • node_module 結構:
- node_modules
  - A
  - B
  - C
  - D

這種目錄結構看起來更簡潔清晰,也確實解決了目錄過深的問題。「但是」,根據 NodeJS 的尋址邏輯,這也就意味著我們可以引用到任意子孫依賴!這種不明確的依賴關系是非常不穩定的,可能觸發很多問題:

  • 不一致性:幽靈依賴可能導致應用程序的行為在不同的環境中表現不一致,因為不同環境中可能缺少或包含不同版本的幽靈依賴;
  • 不可預測性:本質上,幽靈依賴的是頂層依賴的依賴網絡的一部分,你很難精細控制這些子孫依賴的版本,完全隨緣;
  • 難以維護:若你的代碼中存在幽靈依賴,在依賴庫升級或遷移時,幽靈依賴可能導致意外的兼容性問題或升級困難。

那么如何解決幽靈依賴問題呢?其實也比較簡單,核心準則:請務必確保依賴關系是清晰明確的,一旦消費則必須在項目工程內注冊依賴!有許多工具能幫我們達成這一點:

  • 使用 pnpm:與 yarn、npm 不同,pnpm 不是簡單的扁平化結構,而是使用符號鏈接將物理存儲的依賴鏈接到項目的 node_modules 目錄,確保每個項目只能訪問在其 package.json 中明確聲明的依賴;
  • 使用 ESLint:ESLint 提供了不少規則用于檢測幽靈依賴,例如 import/no-extraneous-dependencies,只需要在項目中啟用即可;
  • 使用 depcheck:這是一個用于檢測未使用的或缺失的 npm 包依賴,可以協助發現現存代碼可能存在的幽靈依賴,類似的還有:npm-check 等。

5. 依賴沖突

依賴沖突通常發生在兩個或多個包依賴不同版本的同一庫時。設想這樣一個場景:包 app 依賴了 lib-a、lib-b,而 lib-a、lib-b 又依賴了 lib-d,此時這幾個實體之間之間形成了一種菱形依賴關系:

圖解:菱形依賴

ok,菱形依賴本身是一種非常常見且合理的依賴結構,這不是問題,真正的問題出現在若此時 lib-a/lib-b 所依賴的 lib-d 版本不一致時,就會產生依賴沖突現象:

圖解:依賴沖突

而這輕則導致 lib-d 被重復安裝;嚴重時可能導致如構建失敗、應用運行錯誤(例如 bundle 中同時存在兩個 react 實例)等問題。其次,更大的隱患在于,依賴沖突會使得依賴網絡的復雜度進一步提升惡化,降低項目的可維護性和擴展性,長期難以維護。

圖解:進一步劣化的結構

比較難受的是,依賴沖突問題多數時候出現在次級依賴中,我們通常無法細粒度地管控好這些底層依賴,悲觀地說,我們還無法從根本上解決這些問題,只能采取一些手段盡可能緩解:

  • 打包構建時,可以借助 webpack alias 之類的手段,強制指定版本包位置。
  • 可以借助 package.json 的 resolution 字段強制綁定版本號。
  • 必要時,借助 patch-package 或 pnpm patch 對依賴包做微調。

6. 循環依賴

循環依賴是指兩個或多個 Package 之間相互依賴,形成鏈式閉環的情況。這種循環結構可能很明顯也可能很隱蔽,但總之在依賴鏈條上形成了一個環狀的結構關系。

循環依賴的問題在于,它會使得依賴關系變得非常復雜 —— 從有向無環到更復雜的有向有環圖,這會增加依賴網絡解析成本,包管理器通常需要為此編寫復雜的循環依賴安裝算法;也會增加“開發者”的理解成本 —— 而這必然也會進一步降低項目的可維護性。

其次,循環依賴的更新邏輯也會變得特別啰嗦,假設存在 A=>B=>C=>A 這樣的循環依賴鏈條,那么 B 的更新可能會導致 C/A 需要同步更新,整體結構的穩定性變得非常脆弱。

7. 依賴更新鏈路長

設想一個場景,存在依賴鏈條:A => B => C => D,若底層 D 包發布了一個新版本(比如修復了一個重要的安全問題),那么有時候可能需要鏈條上的 B 與 C 包都隨之更新版本之后,A 才能得到相應更新。關鍵問題在于,中間節點越多,完成更新所需要的時間往往越長,如果中間某些節點的更新活躍度并不高的時候,延遲問題必然會更嚴重,這些風險點最終都會嫁接到頂層 A 包身上。

當然,當下的開源依賴包也并沒有如上述設想的那般脆弱,質量“良好”的開源 Package 往往有較強的容錯性,對底層的依賴往往也會優先遵循 semver 的范圍版本規則。但“閉源”軟件包通常就沒這么高的質量要求了,可能會設置一些拙劣的兼容策略,甚至為了避免向前向后兼容的麻煩,直接“鎖死”核心依賴版本,導致底層包出現問題時,頂層依賴可能難以得到更新。

8. 大型應用中的依賴更新

設想我們正在維護代碼總量超過 10w 行且持續迭代的一個大型應用,若此時需要對某些基礎依賴做比較大的版本升級,那么你所面臨工作量與復雜度都會非常高。

首先,你需要細致地梳理出新舊版本之間的接口、行為差異,這一步需要做許多調研工作,甚至可能需要仔細比對兩個版本源碼之間的區別;其次,按照這些差異點對 10w 行代碼都做一次更新適配,以使得代碼在新版本中能夠正常運行,某些命中 Breaking change 的地方可能還需要重新設計實現方案。

這個過程隱含著非常大的開發與測試的工作量,通常需要持續投入一段時間做開發,但問題是業務本身還在持續迭代,不可能把所有事情停下來等著你慢慢把版本升上去;也通常,這件事情很難僅僅通過“增加人力”就能提高執行效率,因為改造過程隨時可能出現一些始料未及的新問題,需要有足夠技術功力的人才能高效做出新的判斷與決策。

應對這些問題,一個 「理所當然」 的解決方案是 Case by case 地設計一些技術方案來實現漸進式代碼升級,例如在微前端場景中可以通過子應用方式,將頁面與模塊逐個遷移到新的依賴版本,直至整體升級完畢;此外,也可以適當設計一些接口適配器,盡可能減少直接改動頂層代碼。

其次,對于一些工程能力比較強的團隊,推薦引入一些 E2E 技術(彩蛋:為什么這里不是 UT?)并持續維護一套至少覆蓋核心鏈路的測試用例,發生變更時由自動化測試技術確保應用狀態符合功能預期。這是一種一本萬利的技術投入,同樣適用于驗證日常業務迭代中的代碼變動。

一些最佳實踐

綜上,依賴管理是一個復雜問題,天然存在著許多復雜性與不可控因素,并且當下并沒有任何解決方案能普適地解決所有問題。不過,也有一些值得在日常工作中遵循的最佳實踐,能夠一定程度上緩解各種問題的影響面。

1. 嚴格審查

在引入新的三方依賴時,不要輕易做決定!雖然 NPM 已經注冊了數不勝數的各種類型的依賴,足以覆蓋我們日常遇到的多數開發場景,并且使用成本都非常低,但這并不意味著我們可以未經思考通通采用!請記住,在軟件工程中,治理問題的成本與復雜度多數時候比開發一個新功能特性要高出許多,一個錯誤的決策在未來可能需要花十倍力氣解決問題(總是要還的)。

因此,在使用某個 Package 之前,我們至少應該對它做一些基礎的調研,雖然很難完全準確評估一個 Package 的好壞,但某些關鍵特性還是有助于側面了解它的質量,例如:

  • 是否有完備詳盡的 Readme:這體現了作者的用心程度與專業度,也同時決定了我們使用這個包的成本。理想的 Readme 應該至少包含這個包的使用方法與基本原理,內容越詳細越好;
  • 更新頻率:更新頻率越高通常證明作者或者社區的活躍度越高,也通常意味著出現 Issue 時解決速度越快,你也不想在遇到問題時沒有被及時解決吧?
  • 單測:作為開源框架,穩定性是一個非常重要的指標,而單測又是一種能夠確保穩定性的重要工具,因此可以在做決策時建議看看框架源碼本身的單測覆蓋率,以及單測斷言的使用情況;反之,如果連單測都沒做好,建議慎重!
  • Benchmark:與單測類似,若源碼中包含一定比例的 Benchmark,則意味著作者對作品的性能有一定要求,那么自然地質量相對更值得信任一些;
  • 下載或 Star 量:這兩個指標通常意味著這個開源作品被使用的頻率,頻率越高通常意味著被越多人消費、驗證過,也就越能證明這個框架不會存在一些基本的質量問題 —— 至少能跑的通嘛;不過請注意,不要迷信這兩個指標,有許多場外因素(例如發布時間、作者影響力等)都會影響這些數量的變化,數量大不足以證明質量高;
  • 代碼結構:如果時間允許,非常建議審查開源框架的代碼結構,如果發現明顯的 Bad Smell,例如圈復雜度明顯很高,或者有許多重復代碼,則建議慎重采用;

2. 定期清理無用依賴

隨項目迭代,依賴列表通常會逐漸增加,但很少被及時清理,導致無用依賴逐漸增多,甚至可能引發上述諸多依賴問題,因此建議有一套機制,定期掃描 & 刪除項目中的無用依賴。社區已經提供了不少依賴掃描工具,例如 depcheck,借助這些工具我們能快速找出無用依賴。

3. 定期 review 依賴結構圖

同樣,隨項目迭代,依賴結構圖持續發生變化,且通常會越來越復雜,可能多數開發者體感上覺得依賴安裝的時間越來越長,但沒有深究或觀察過依賴結構正在出現一些不合理的劣化,可能那天想起來要優化的時候,問題已經變得非常復雜,難以糾偏。

因此,建議在日常工作中關注依賴結構的變化情況,是否出現上述異常,例如:重復依賴、依賴沖突等。一個比較簡單的方式,是觀察 pnpm-lock.yaml、yarn.lock 等文件的內容,可以考慮借助 CI,寫腳本,在合碼之前對比 Merge Request 前后的結構圖,檢查是否出現一些 bad case。

4. 使用 Pnpm

在 JS 社區,目前比較主流的包管理器有:NPM、Yarn、Pnpm 三種,從底層實現邏輯來說,更推薦使用 Pnpm (Performance NPM),它安裝下來的依賴結構更合理,能避開大多數幽靈依賴問題,更重要的,它的緩存結構更合理,也因此有更好的安裝、更新性能。

結語

綜上,社區開源能切實提升整個軟件工業的發展速度,極大降低開發成本,但不可忽視的也帶來了一些新的復雜性 —— 依賴管理,這其中隱含著許多很少被關注的隱患,多數時候這并不會直接造成問題,但疊加時間與規模兩個因素后,通常會慢慢會演變的越來越復雜,積重難返!所以,一方面日常需要警惕依賴結構的劣化,一方面真遇到問題時,可以參照上面梳理的各種 case,分析具體問題,予以解決。

責任編輯:姜華 來源: Tecvan
相關推薦

2020-06-28 09:30:37

Linux內存操作系統

2021-04-20 08:40:11

內存管理Lwip

2022-08-22 09:20:05

Kubernetes工作負載管理

2018-04-19 10:22:06

數據中心連接性托管

2023-07-25 15:06:39

2022-08-30 10:15:27

Kubernetes數據持久化管理

2022-05-18 16:35:43

Redis內存運維

2023-07-06 13:56:14

微軟Skype

2023-06-25 09:44:00

一致性哈希數據庫

2020-09-08 06:54:29

Java Gradle語言

2020-06-02 15:06:13

Tomcat配置頁面

2023-09-22 17:36:37

2021-01-28 22:31:33

分組密碼算法

2020-05-22 08:16:07

PONGPONXG-PON

2018-06-07 13:17:12

契約測試單元測試API測試

2021-08-01 09:55:57

Netty時間輪中間件

2024-10-28 21:02:36

消息框應用程序

2023-09-27 16:39:38

2021-12-06 09:43:01

鏈表節點函數

2021-07-16 11:48:26

模型 .NET微軟
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 99精品视频网 | 亚洲高清一区二区三区 | 国产精品久久久精品 | 久久久久久久久久久福利观看 | 黄网站免费观看 | 在线国产一区二区 | 7777在线视频免费播放 | 欧美精品一区在线 | 成人av一区二区亚洲精 | 国产综合精品一区二区三区 | 日韩欧美在线视频一区 | 成人精品毛片 | 中文字幕一区二区三区精彩视频 | 免费视频一区二区三区在线观看 | 337p日本欧洲亚洲大胆 | 青青草一区二区三区 | 91久久久久久| 久久这里只有精品首页 | 91精品国产高清久久久久久久久 | 亚洲日本欧美日韩高观看 | 欧美精品在欧美一区二区少妇 | 成人超碰在线 | 成人免费福利视频 | 免费污视频 | 亚洲高清av | 天天操,夜夜爽 | 久久精品亚洲一区 | 亚洲福利视频网 | 91视频久久| 日韩毛片在线免费观看 | 亚洲成人av | 成人在线中文字幕 | 亚洲欧美一区二区三区在线 | 欧美一区二区三区电影 | 亚洲大片 | 亚洲日韩视频 | 性一爱一乱一交一视频 | 蜜臀网站 | 欧美精品video | 精品美女视频在线观看免费软件 | 最新超碰 |