Maison > interface Web > js tutoriel > le corps du texte

Résoudre les problèmes de dépendance circulaire dans ESrojects

王林
Libérer: 2024-09-03 21:04:32
original
935 Les gens l'ont consulté

Resolving Circular Dependency Issues in ESrojects

Un guide pour identifier et corriger les dépendances circulaires dans les projets JavaScript à l'aide de Madge et ESLint.

TL;DR

  • Il est recommandé d'utiliser ESLint avec la règle pour vérifier les dépendances circulaires dans votre projet.
  • Si votre cible de build est ES5, une fois qu'un module exporte des constantes, il ne doit pas importer d'autres modules.

Symptômes du problème

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';

// ...
Copier après la connexion
// index.js

import { FOO } from './utils.js';
// import other modules…

console.log(FOO); // `console.log` outputs `undefined`

// ...
Copier après la connexion

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.

Recherche de dépendances circulaires

Installer l'outil Madge

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.

Étape 1 : Configurer 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);
  });
Copier après la connexion

Étape 2 : Exécutez le script

node madge.js
Copier après la connexion

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.

Écrire un script pour rechercher des dépendances circulaires indirectes

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);
Copier après la connexion

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.

Cause première

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é.

Exemple de code groupé Webpack

(() => {
  "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 ========
})();
Copier après la connexion

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'
Copier après la connexion

Processus de regroupement de modules Webpack

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

Résumé de la cause profonde

  1. 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.

  2. Conditions pour que le problème se produise :

    • Le projet est regroupé par Webpack et compilé en code ES5 (non testé avec d'autres bundlers).
    • Le module A définit une constante foo et l'exporte, et ce module a des dépendances circulaires avec d'autres modules.
    • Le module B importe foo du module A et s'exécute pendant le processus d'initialisation du module.
    • Le module A et le module B ont une dépendance circulaire.
  3. Causes profondes :

    • In Webpack's module system, when a module is first referenced, Webpack initializes its exports using property getters and stores it in a cache object. When the module is referenced again, it directly returns the exports from the cache object.
    • let variables and const constants are compiled into var declarations, causing variable hoisting issues. When used before their actual definition, they return undefined but do not throw an error.
    • Function declarations are hoisted, allowing them to be called normally.
    • Arrow functions are compiled into var foo = function () {}; and function expressions do not have function scope hoisting. Therefore, they throw an error when run instead of returning undefined.

How to Avoid

ESLint

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"],
      }
    },
  },
];
Copier après la connexion

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!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal
À propos de nous Clause de non-responsabilité Sitemap
Site Web PHP chinois:Formation PHP en ligne sur le bien-être public,Aidez les apprenants PHP à grandir rapidement!