Heim > Artikel > Web-Frontend > Beheben von Problemen mit der zirkulären Abhängigkeit in ESrojects
Ein Leitfaden zum Identifizieren und Beheben zirkulärer Abhängigkeiten in JavaScript-Projekten mit Madge und ESLint.
Beim Ausführen des Projekts wird eine referenzierte Konstante als undefiniert ausgegeben.
Zum Beispiel: FOO, das aus utils.js exportiert wurde, wird in index.js importiert und sein Wert wird als undefiniert ausgegeben.
// 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` // ...
Erfahrungsgemäß wird dieses Problem wahrscheinlich durch eine zirkuläre Abhängigkeit zwischen index.js und utils.js verursacht.
Der nächste Schritt besteht darin, den zirkulären Abhängigkeitspfad zwischen den beiden Modulen zu identifizieren, um die Hypothese zu überprüfen.
In der Community stehen viele Tools zur Verfügung, um zirkuläre Abhängigkeiten zu finden. Hier verwenden wir Madge als Beispiel.
Madge ist ein Entwicklertool zum Erstellen eines visuellen Diagramms Ihrer Modulabhängigkeiten, zum Auffinden zirkulärer Abhängigkeiten und zum Bereitstellen anderer nützlicher Informationen.
// 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
Nachdem das Skript ausgeführt wurde, wird ein 2D-Array erhalten.
Das 2D-Array speichert alle zirkulären Abhängigkeiten im Projekt. Jedes Unterarray stellt einen bestimmten zirkulären Abhängigkeitspfad dar: Datei am Index n verweist auf die Datei am Index n + 1, und die letzte Datei verweist auf die erste Datei und bildet so eine zirkuläre Abhängigkeit.
Es ist wichtig zu beachten, dass Madge nur direkte zirkuläre Abhängigkeiten zurückgeben kann. Wenn zwei Dateien über eine dritte Datei eine indirekte zirkuläre Abhängigkeit bilden, wird diese nicht in die Ausgabe von Madge einbezogen.
Basierend auf der tatsächlichen Projektsituation gab Madge eine Ergebnisdatei mit über 6.000 Zeilen aus. Die Ergebnisdatei zeigt, dass die vermutete zirkuläre Abhängigkeit zwischen den beiden Dateien nicht direkt referenziert wird. Das Finden der indirekten Abhängigkeit zwischen den beiden Zieldateien war wie die Suche nach der Nadel im Heuhaufen.
Als nächstes habe ich ChatGPT gebeten, beim Schreiben eines Skripts zu helfen, um basierend auf der Ergebnisdatei direkte oder indirekte zirkuläre Abhängigkeitspfade zwischen zwei Zieldateien zu finden.
/** * 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);
Unter Verwendung der 2D-Array-Ausgabe von Madge als Skripteingabe zeigte das Ergebnis, dass tatsächlich eine zirkuläre Abhängigkeit zwischen index.js und utils.js bestand, die aus einer Kette mit 26 Dateien bestand.
Bevor wir das Problem lösen, müssen wir die Grundursache verstehen: Warum führt eine zirkuläre Abhängigkeit dazu, dass die referenzierte Konstante undefiniert ist?
Um das Problem zu simulieren und zu vereinfachen, nehmen wir an, dass die zirkuläre Abhängigkeitskette wie folgt lautet:
index.js → Component-entry.js → request.js → utils.js → Component-entry.js
Da der Projektcode letztendlich von Webpack gebündelt und mit Babel in ES5-Code kompiliert wird, müssen wir uns die Struktur des gebündelten Codes ansehen.
(() => { "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 ======== })();
Im Beispiel gibt [Zahl] die Ausführungsreihenfolge des Codes an.
Vereinfachte Version:
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'
Während der AST-Analysephase sucht Webpack nach ES6-Import- und Exportanweisungen. Wenn eine Datei diese Anweisungen enthält, markiert Webpack das Modul als „Harmony“-Typ und führt die entsprechende Codetransformation für Exporte durch:
https://github.com/webpack/webpack/blob/c586c7b1e027e1d252d68b4372f08a9bce40d96c/lib/dependencies/HarmonyExportInitFragment.js#L161
https://github.com/webpack/webpack/blob/c586c7b1e027e1d252d68b4372f08a9bce40d96c/lib/RuntimeTemplate.js#L164
Problemsymptom: Ein Modul importiert eine Konstante, aber ihr tatsächlicher Wert ist beim Ausführen undefiniert.
Bedingungen, unter denen das Problem auftritt:
Grundursachen:
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"], } }, }, ];
Das obige ist der detaillierte Inhalt vonBeheben von Problemen mit der zirkulären Abhängigkeit in ESrojects. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!