使用 Madge 和 ESLint 來識別和修復 JavaScript 專案中的循環依賴關係的指南。
運行專案時,引用的常數輸出為未定義。
例如:從 utils.js 匯出的 FOO 被匯入到 index.js 中,並且其值列印為 undefined。
// utils.js // import other modules… export const FOO = 'foo'; // ...
// index.js import { FOO } from './utils.js'; // import other modules… console.log(FOO); // `console.log` outputs `undefined` // ...
根據經驗,這個問題很可能是由index.js和utils.js之間的循環依賴所引起的。
下一步是識別兩個模組之間的循環依賴路徑以驗證假設。
社群中有許多工具可用於尋找循環依賴項。這裡,我們以 Madge 為例。
Madge 是一個開發人員工具,用於產生模組依賴關係的視覺化圖表、尋找循環依賴關係並提供其他有用資訊。
// madge.js const madge = require("madge"); const path = require("path"); const fs = require("fs"); madge("./index.ts", { tsConfig: { compilerOptions: { paths: { // specify path aliases if using any }, }, }, }) .then((res) => res.circular()) .then((circular) => { if (circular.length > 0) { console.log("Found circular dependencies: ", circular); // save the result into a file const outputPath = path.join(__dirname, "circular-dependencies.json"); fs.writeFileSync(outputPath, JSON.stringify(circular, null, 2), "utf-8"); console.log(`Saved to ${outputPath}`); } else { console.log("No circular dependencies found."); } }) .catch((error) => { console.error(error); });
node madge.js
運行腳本後,得到一個二維數組。
二維數組儲存了專案中所有的循環相依性。每個子數組代表一個特定的循環依賴路徑:索引n處的文件引用索引n+1處的文件,最後一個文件引用第一個文件,形成循環依賴。
要注意的是,Madge 只能回傳直接循環相依性。如果兩個檔案透過第三個檔案形成間接循環依賴關係,則該檔案不會包含在 Madge 的輸出中。
根據專案實際情況,Madge 輸出了 6000 多行的結果檔。結果文件顯示,兩個文件之間可疑的循環依賴關係並不是直接引用的。尋找兩個目標檔案之間的間接依賴就像大海撈針一樣。
接下來,我請 ChatGPT 幫忙編寫一個腳本,根據結果檔案找出兩個目標檔案之間的直接或間接循環依賴路徑。
/** * Check if there is a direct or indirect circular dependency between two files * @param {Array<string>} targetFiles Array containing two file paths * @param {Array<Array<string>>} references 2D array representing all file dependencies in the project * @returns {Array<string>} Array representing the circular dependency path between the two target files */ function checkCircularDependency(targetFiles, references) { // Build graph const graph = buildGraph(references); // Store visited nodes to avoid revisiting let visited = new Set(); // Store the current path to detect circular dependencies let pathStack = []; // Depth-First Search function dfs(node, target, visited, pathStack) { if (node === target) { // Found target, return path pathStack.push(node); return true; } if (visited.has(node)) { return false; } visited.add(node); pathStack.push(node); const neighbors = graph[node] || []; for (let neighbor of neighbors) { if (dfs(neighbor, target, visited, pathStack)) { return true; } } pathStack.pop(); return false; } // Build graph function buildGraph(references) { const graph = {}; references.forEach((ref) => { for (let i = 0; i < ref.length; i++) { const from = ref[i]; const to = ref[(i + 1) % ref.length]; // Circular reference to the first element if (!graph[from]) { graph[from] = []; } graph[from].push(to); } }); return graph; } // Try to find the path from the first file to the second file if (dfs(targetFiles[0], targetFiles[1], new Set(), [])) { // Clear visited records and path stack, try to find the path from the second file back to the first file visited = new Set(); pathStack = []; if (dfs(targetFiles[1], targetFiles[0], visited, pathStack)) { return pathStack; } } // If no circular dependency is found, return an empty array return []; } // Example usage const targetFiles = [ "scene/home/controller/home-controller/grocery-entry.ts", "../../request/api/home.ts", ]; const references = require("./circular-dependencies"); const circularPath = checkCircularDependency(targetFiles, references); console.log(circularPath);
使用Madge輸出的2D陣列作為腳本輸入,結果顯示index.js和utils.js之間確實存在循環依賴,由包含26個檔案的鏈組成。
在解決問題之前,我們需要了解根本原因:為什麼循環依賴會導致引用的常數未定義?
為了模擬簡化問題,我們假設循環依賴鏈如下:
index.js → 元件入口.js → request.js → utils.js → 元件入口.js
由於專案程式碼最終是透過Webpack捆綁並使用Babel編譯成ES5程式碼,所以我們需要看一下捆綁後程式碼的結構。
(() => { "use strict"; var e, __modules__ = { /* ===== component-entry.js starts ==== */ 148: (_, exports, __webpack_require__) => { // [2] define the getter of `exports` properties of `component-entry.js` __webpack_require__.d(exports, { Cc: () => r, bg: () => c }); // [3] import `request.js` var t = __webpack_require__(595); // [9] var r = function () { return ( console.log("A function inside component-entry.js run, ", c) ); }, c = "An constants which comes from component-entry.js"; }, /* ===== component-entry.js ends ==== */ /* ===== request.js starts ==== */ 595: (_, exports, __webpack_require__) => { // [4] import `utils.js` var t = __webpack_require__(51); // [8] console.log("request.js run, two constants from utils.js are: ", t.R, ", and ", t.b); }, /* ===== request.js ends ==== */ /* ===== utils.js starts ==== */ 51: (_, exports, __webpack_require__) => { // [5] define the getter of `exports` properties of `utils.js` __webpack_require__.d(exports, { R: () => r, b: () => t.bg }); // [6] import `component-entry.js`, `component-entry.js` is already in `__webpack_module_cache__` // so `__webpack_require__(148)` will return the `exports` object of `component-entry.js` immediately var t = __webpack_require__(148); var r = 1001; // [7] print the value of `bg` exported by `component-entry.js` console.log('utils.js,', t.bg); // output: 'utils, undefined' }, /* ===== utils.js starts ==== */ }, __webpack_module_cache__ = {}; function __webpack_require__(moduleId) { var e = __webpack_module_cache__[moduleId]; if (void 0 !== e) return e.exports; var c = (__webpack_module_cache__[moduleId] = { exports: {} }); return __modules__[moduleId](c, c.exports, __webpack_require__), c.exports; } // Adds properties from the second object to the first object __webpack_require__.d = (o, e) => { for (var n in e) Object.prototype.hasOwnProperty.call(e, n) && !Object.prototype.hasOwnProperty.call(o, n) && Object.defineProperty(o, n, { enumerable: !0, get: e[n] }); }, // [0] // ======== index.js starts ======== // [1] import `component-entry.js` (e = __webpack_require__(148/* (148 is the internal module id of `component-entry.js`) */)), // [10] run `Cc` function exported by `component-entry.js` (0, e.Cc)(); // ======== index.js ends ======== })();
範例中,[number]表示程式碼的執行順序。
簡化版:
function lazyCopy (target, source) { for (var ele in source) { if (Object.prototype.hasOwnProperty.call(source, ele) && !Object.prototype.hasOwnProperty.call(target, ele) ) { Object.defineProperty(target, ele, { enumerable: true, get: source[ele] }); } } } // Assuming module1 is the module being cyclically referenced (module1 is a webpack internal module, actually representing a file) var module1 = {}; module1.exports = {}; lazyCopy(module1.exports, { foo: () => exportEleOfA, print: () => print, printButThrowError: () => printButThrowError }); // module1 is initially imported at this point // Assume the intermediate process is omitted: module1 references other modules, and those modules reference module1 // When module1 is imported a second time and its `foo` variable is used, it is equivalent to executing: console.log('Output during circular reference (undefined is expected): ', module1.exports.foo); // Output `undefined` // Call `print` function, which can be executed normally due to function scope hoisting module1.exports.print(); // 'print function executed' // Call `printButThrowError` function, which will throw an error due to the way it is defined try { module1.exports.printButThrowError(); } catch (e) { console.error('Expected error: ', e); // Error: module1.exports.printButThrowError is not a function } // Assume the intermediate process is omitted: all modules referenced by module1 are executed // module1 returns to its own code and continues executing its remaining logic var exportEleOfA = 'foo'; function print () { console.log('print function executed'); } var printButThrowError = function () { console.log('printButThrowError function executed'); } console.log('Normal output: ', module1.exports.foo); // 'foo' module1.exports.print(); // 'print function executed' module1.exports.printButThrowError(); // 'printButThrowError function executed'
在 AST 分析階段,Webpack 會尋找 ES6 匯入和匯出語句。如果檔案包含這些語句,Webpack 會將模組標記為「harmony」類型,並為匯出執行對應的程式碼轉換:
https://github.com/webpack/webpack/blob/c586c7b1e027e1d252d68b4372f08a9bce40d96c/lib/dependencies/HarmonyExportInitFragment.js#L161
https://github.com/webpack/webpack/blob/c586c7b1e027e1d252d68b4372f08a9bce40d96c/lib/RuntimeTemplate.js#L164
問題症狀:模組導入了一個常數,但運行時其實際值未定義。
問題發生的條件:
根本原因:
We can use ESLint to check for circular dependencies in the project. Install the eslint-plugin-import plugin and configure it:
// babel.config.js import importPlugin from 'eslint-plugin-import'; export default [ { plugins: { import: importPlugin, }, rules: { 'import/no-cycle': ['error', { maxDepth: Infinity }], }, languageOptions: { "parserOptions": { "ecmaVersion": 6, // or use 6 for ES6 "sourceType": "module" }, }, settings: { // Need this to let 'import/no-cycle' to work // reference: https://github.com/import-js/eslint-plugin-import/issues/2556#issuecomment-1419518561 "import/parsers": { espree: [".js", ".cjs", ".mjs", ".jsx"], } }, }, ];
以上是解決 ESrojects 中的循環依賴問題的詳細內容。更多資訊請關注PHP中文網其他相關文章!