Heim  >  Artikel  >  Web-Frontend  >  Beheben von Problemen mit der zirkulären Abhängigkeit in ESrojects

Beheben von Problemen mit der zirkulären Abhängigkeit in ESrojects

王林
王林Original
2024-09-03 21:04:321027Durchsuche

Resolving Circular Dependency Issues in ESrojects

Ein Leitfaden zum Identifizieren und Beheben zirkulärer Abhängigkeiten in JavaScript-Projekten mit Madge und ESLint.

TL;DR

  • Es wird empfohlen, ESLint mit der Regel zu verwenden, um in Ihrem Projekt auf zirkuläre Abhängigkeiten zu prüfen.
  • Wenn Ihr Build-Ziel ES5 ist, sollte ein Modul, sobald es Konstanten exportiert, keine anderen Module importieren.

Problemsymptome

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.

Zirkuläre Abhängigkeiten finden

Installieren Sie das Madge-Tool

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.

Schritt 1: Madge konfigurieren

// 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);
  });

Schritt 2: Führen Sie das Skript aus

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.

Schreiben eines Skripts zum Finden indirekter zirkulärer Abhängigkeiten

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.

Grundursache

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.

Beispiel für gebündelten Webpack-Code

(() => {
  "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'

Bündelungsprozess des Webpack-Moduls

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

Zusammenfassung der Grundursache

  1. Problemsymptom: Ein Modul importiert eine Konstante, aber ihr tatsächlicher Wert ist beim Ausführen undefiniert.

  2. Bedingungen, unter denen das Problem auftritt:

    • Das Projekt wird von Webpack gebündelt und in ES5-Code kompiliert (nicht mit anderen Bundlern getestet).
    • Modul A definiert eine Konstante foo und exportiert sie, und dieses Modul weist zirkuläre Abhängigkeiten mit anderen Modulen auf.
    • Modul B importiert foo aus Modul A und wird während des Modulinitialisierungsprozesses ausgeführt.
    • Modul A und Modul B haben eine zirkuläre Abhängigkeit.
  3. Grundursachen:

    • 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"],
      }
    },
  },
];

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!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn