Maison >interface Web >js tutoriel >Résoudre les problèmes de dépendance circulaire dans ESrojects
Un guide pour identifier et corriger les dépendances circulaires dans les projets JavaScript à l'aide de Madge et ESLint.
Lors de l'exécution du projet, une constante référencée est affichée comme indéfinie.
Par exemple : FOO exporté depuis utils.js est importé dans index.js et sa valeur est imprimée comme indéfinie.
// 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` // ...
D'après l'expérience, ce problème est probablement dû à une dépendance circulaire entre index.js et utils.js.
L'étape suivante consiste à identifier le chemin de dépendance circulaire entre les deux modules pour vérifier l'hypothèse.
Il existe de nombreux outils disponibles dans la communauté pour trouver des dépendances circulaires. Ici, nous utilisons Madge comme exemple.
Madge est un outil de développement permettant de générer un graphique visuel des dépendances de vos modules, de trouver des dépendances circulaires et de fournir d'autres informations utiles.
// 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
Après avoir exécuté le script, un tableau 2D est obtenu.
Le tableau 2D stocke toutes les dépendances circulaires du projet. Chaque sous-tableau représente un chemin de dépendance circulaire spécifique : le fichier à l'index n fait référence au fichier à l'index n + 1, et le dernier fichier fait référence au premier fichier, formant une dépendance circulaire.
Il est important de noter que Madge ne peut renvoyer que des dépendances circulaires directes. Si deux fichiers forment une dépendance circulaire indirecte via un troisième fichier, celle-ci ne sera pas incluse dans la sortie de Madge.
Sur la base de la situation réelle du projet, Madge a généré un fichier de résultats de plus de 6 000 lignes. Le fichier de résultats montre que la dépendance circulaire suspectée entre les deux fichiers n'est pas directement référencée. Trouver la dépendance indirecte entre les deux fichiers cibles revenait à chercher une aiguille dans une botte de foin.
Ensuite, j'ai demandé à ChatGPT de m'aider à écrire un script pour trouver des chemins de dépendance circulaires directs ou indirects entre deux fichiers cibles en fonction du fichier résultat.
/** * 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);
En utilisant la sortie du tableau 2D de Madge comme entrée de script, le résultat a indiqué qu'il existait effectivement une dépendance circulaire entre index.js et utils.js, constituée d'une chaîne impliquant 26 fichiers.
Avant de résoudre le problème, nous devons comprendre la cause profonde : pourquoi la dépendance circulaire fait-elle que la constante référencée n'est pas définie ?
Pour simuler et simplifier le problème, supposons que la chaîne de dépendances circulaire est la suivante :
index.js → composant-entry.js → request.js → utils.js → composant-entry.js
Étant donné que le code du projet est finalement regroupé par Webpack et compilé en code ES5 à l'aide de Babel, nous devons examiner la structure du code regroupé.
(() => { "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 ======== })();
Dans l'exemple, [numéro] indique l'ordre d'exécution du code.
Version simplifiée :
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'
Pendant la phase d'analyse AST, Webpack recherche les instructions d'importation et d'exportation ES6. Si un fichier contient ces instructions, Webpack marque le module comme type « harmonie » et effectue la transformation de code correspondante pour les exportations :
https://github.com/webpack/webpack/blob/c586c7b1e027e1d252d68b4372f08a9bce40d96c/lib/dependencies/HarmonyExportInitFragment.js#L161
https://github.com/webpack/webpack/blob/c586c7b1e027e1d252d68b4372f08a9bce40d96c/lib/RuntimeTemplate.js#L164
Symptôme de problème : un module importe une constante, mais sa valeur réelle n'est pas définie lors de son exécution.
Conditions pour que le problème se produise :
Causes profondes :
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"], } }, }, ];
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!