前端模塊依賴複雜度太高,一團亂麻了怎麼辦

隨着項目規模龐大,文件層級與結構的複雜度越來越高,模塊關係混亂,循環依賴,反向依賴行爲越來越多。

爲了保持項目穩定和架構良好,需要進行模塊依賴關係治理

dependency-cruiser 在這方面是個很棒的工具,今天主要介紹下它的應用:

dependency-cruiser 介紹

Github 倉庫:https://github.com/sverweij/dependency-cruiser

dependency-cruiser 直譯過來就是「依賴巡洋艦」,用於可視化校驗模塊之間的依賴關係。

它支持前端常用的 JavaScript,TypeScript 語言和 ESM,CommonJS 等模塊規範。

在項目裏通常與 ESLint 配套使用,一個用於代碼檢查,一個用於依賴檢查。

依賴關係可視化

可視化依賴關係能夠幫助你更快地瞭解和洞察一個項目,下圖是使用 dependency-cruiser 生成的 preact 依賴關係圖。

它就像一張代碼地圖,“一覽衆山小”,依賴關係一目瞭然。

使用方式

  1. 依賴安裝

首先安裝 Graphviz,用來生成依賴關係圖。

brew install graphviz

接着在項目裏安裝 dependency-cruiser

npm i dependency-cruiser -D
  1. 生成圖片

安裝好就可以直接執行命令使用了:

npx depcruise —-output-type dot src | dot -T svg > dependency-graph.svg

執行後會在根目錄下得到一張 svg 格式的依賴關係圖。

上方的核心命令是 depcruise src,表示對 src 下的文件進行依賴追蹤。

其餘參數用來控制輸出格式:

建議將該命令放在 package.json 的 npm 腳本中,還能配合 CI/CD 完成依賴圖生成自動化。

其它參數

除了控制輸出格式,我們還能通過一些參數對依賴圖進行控制。

比如設置 --max-depth 1後生成的 preact 依賴圖爲:

複雜度會減小很多。

更多案例可以參考官方文檔 Real world samples[1]。

VS Code 插件

除了命令,使用 VSCode 插件 Dependency Cruiser Extension[2] 也能快速查看依賴圖。

使用方式也很簡單,安裝插件後在文件右鍵菜單中點擊「View Dependencies」即可看到基於該文件的依賴圖。

圖中可以看到它的文件層級與下游依賴,對於臨時地查看某個文件的依賴關係,這樣會更方便。

依賴關係校驗

dependency-cruiser 也可以像 ESLint 一樣自定義規則來對依賴關係進行校驗。

它能夠前置規避掉可能出現的各種依賴關係問題,使用起來也非常簡單。

使用方式

  1. 依賴安裝
npm i dependency-cruiser -D
  1. 初始化配置文件
npx depcruise -—init

根據命令行提示完成操作。

會在根目錄下生成 .dependency-cruiser.js 配置文件。

配置文件看起來很長很勸退,其實主要由兩大塊組成:

規則列表由一個個規則項組成,自動生成後會內置一些推薦規則

包括依賴追蹤範圍,模塊規範,TS、Webpack config 文件路徑等,也是自動生成的,通常不需要改動。

簡化後的整體結構如下:

// .dependency-cruiser.js
module.exports = {
  forbidden: [
    {...}, // 規則1
    {...}, // 規則2
    {...}, // 規則3
    // ...
 ],
  options: {
   // ...
  }
}

排除掉不太需要關心的配置後,是不是看起來更清晰了?

後文會詳細說明規則項如何配置。

  1. 校驗依賴

執行命令

npx depcruise src --config .dependency-cruiser.js

依賴關係校驗未通過會像 ESLint 一樣拋出異常。

建議將命令放在 package.json 中的 npm scripts 中,並結合 git hook 或 CI 設置卡點。

內置規則

默認配置文件爲我們內置了一些推薦規則,一起來看看。

「禁止循環引用」規則

循環引用指的是,模塊 A 依賴模塊 B,而模塊 B 又依賴模塊 A。

// moduleA.ts
import moduleB from './moduleB';

export default function moduleA() {
  return 'moduleA';
}

moduleB();
// moduleB.ts
import moduleA from './moduleA';

export default function moduleB() {
  return 'moduleB';
}

moduleA();

如果處理地不好,可能導致程序異常。依賴關係複雜的大項目難免會出現這樣的情況。

禁止循環引用規則就是用來檢測這種場景的,規則配置如下:

{
  // ----- 規則基本配置 -----
  name: 'no-circular', // 規則名稱
  severity: 'error', // 嚴重等級
  comment: '禁止循環引用', // 規則描述
  // ----- 規則內容 -----
  from: {}, // 不填則表示所有引用
  to: {
    "circular": true, // 不允許成環
  },
},

nameseveritycomment 用於描述規則的基本信息。

fromto 描述規則的具體內容,from 表示「依賴方」,to 表示「被依賴方」。

上方的配置表示:任何依賴引用,只要成環,就會報錯 error(默認等級是 warn,這裏改成 error 用於演示)。

測試一下上方 moduleA 和 B 循環依賴的例子,執行校驗命令會在控制檯看到報錯,阻塞後續流程。

其他內置規則

內置規則除了「禁止循環引用」,還有:

  1. not-to-unresolvable:禁止引用不存在的模塊,這會導致程序出錯。

2. no-orphans:檢測未被使用的模塊,提醒我們及時進行代碼清理,避免干擾。

3. not-to-dev-dep:禁止生產環境代碼使用開發依賴,這在開發 node 應用或者 npm 包時可能會存在問題。

自定義規則

我們也可以根據項目場景自定義規則。

「禁止跨模塊引用」規則

兩個非相關的模塊如果出現跨模塊引用會導致強耦合。

比如 pageA 和 pageB 是兩個獨立的頁面組件,但 pageA 下面的 component 直接引用了 pageB 的 utils。

// pageA/component.tsx
// 引用了 pageB 的模塊
import utils from '../pageB/utils';

export default function component() {
  // ...
  return 'page A 下的組件';
}

這樣會導致兩個頁面糾纏不清,應該儘量避免。

正確的做法應該是將用到公共模塊放到更高的層級 src/utils,頁面從公共模塊中引入。

但實際開發中這也是很難避免的,我們可以基於此場景配置一條規則:「禁止 pageA 引入其它頁面模塊」

{
  // ----- 規則基本配置 -----
  name: 'no-cross-module-import',
  severity: 'error', // 嚴重等級
  comment: '禁止跨模塊引用',
  // ----- 規則內容 -----
  from: {
    path: '^src/pageA',
  },
  to: {
    pathNot: [
      // 只能引入自己或公共的模塊
      '^src/pageA',
      '^src/utils',
    ]
 },
},

任何 pageA 的依賴引用,只要不是來自自身或者全局 utils,就會報錯 error。

測試一下,執行校驗命令會在控制檯看到報錯。

其它自定義規則

這裏再簡單介紹兩個自定義的規則。

  1. 「禁止直接引用某個模塊」

如果項目中有對 axios 進行封裝,業務開發時應該使用封裝後的請求庫。

爲了避免直接使用 axios,可以配置一條「禁止直接引用 axios」規則。

{
  // ----- 規則基本配置 -----
  name: 'not-direct-axios',
  comment: '禁止直接使用 axios',
  severity: 'error',
  // ----- 規則內容 -----
  from: {}, // 所有引用
  to: {
    path: 'axios',
  },
},
  1. 「葉子依賴禁止再依賴其它模塊」

還是封裝的場景,比如項目內基於瀏覽器的 cookie api 封裝了一個 cookie 庫。

cookie 庫只依賴 cookie api,不依賴其它模塊,可以配置一條規則來「強制它是一個葉子依賴」。

{
  // ----- 規則基本配置 -----
  name: 'cookies-leaf',
  comment: 'cookies 庫不應該有其它依賴',
  severity: 'error',
  // ----- 規則內容 -----
  from: {
    path: '^src/lib/cookies',
  },
  to: {}, // 不能引用任何其它依賴
},

其它更多場景,歡迎留言討論交流。

總結

本文我們介紹了 dependency-cruiser 治理項目模塊依賴關係的兩種使用方式。

  1. 依賴關係可視化:使用命令可以生成和控制輸出的依賴關係圖。對於追蹤單個文件的依賴關係的場景,使用 VS Code 插件會更方便。

  2. 依賴關係校驗:可以像 ESLint 一樣通過命令來校驗依賴關係,而且支持自定義規則,比如「禁止循環依賴」,「禁止跨模塊引用」等。

當然,這僅僅是它能力的冰山一角,可以去 Github 主頁 [3] 瞭解更多高級用法。

參考資料

[1]

Real world samples: https://github.com/sverweij/dependency-cruiser/blob/develop/doc/real-world-samples.md

[2]

Dependency Cruiser Extension: https://marketplace.visualstudio.com/items?itemName=juanallo.vscode-dependency-cruiser

[3]

Github 主頁: https://github.com/sverweij/dependency-cruiser

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ZYW2rqZuRIRs-pfoUunQLA