ホームページ  >  記事  >  ウェブフロントエンド  >  ESrojects での循環依存関係の問題の解決

ESrojects での循環依存関係の問題の解決

王林
王林オリジナル
2024-09-03 21:04:321026ブラウズ

Resolving Circular Dependency Issues in ESrojects

Madge と ESLint を使用して JavaScript プロジェクトの循環依存関係を特定して修正するためのガイド。

TL;DR

  • プロジェクト内の循環依存関係をチェックするには、ルールとともに ESLint を使用することをお勧めします。
  • ビルド ターゲットが ES5 の場合、モジュールが定数をエクスポートしたら、他のモジュールをインポートしないでください。

問題の症状

プロジェクトを実行すると、参照された定数が未定義として出力されます。

例: utils.js からエクスポートされた FOO は、index.js にインポートされ、その値は未定義として出力されます。

// 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 間の循環依存関係が原因である可能性があります。

次のステップは、仮説を検証するために 2 つのモジュール間の循環依存関係パスを特定することです。

循環依存関係の検索

Madge ツールをインストールする

コミュニティには、循環依存関係を見つけるために利用できるツールが多数あります。ここでは例として Madge を使用します。

Madge は、モジュールの依存関係の視覚的なグラフの生成、循環依存関係の検索、その他の有用な情報の提供を行う開発者ツールです。

ステップ 1: 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);
  });

ステップ 2: スクリプトを実行する

node madge.js

スクリプトを実行すると、2D 配列が取得されます。

2D 配列には、プロジェクト内のすべての循環依存関係が保存されます。各サブ配列は特定の循環依存関係パスを表します。インデックス n のファイルはインデックス n + 1 のファイルを参照し、最後のファイルは最初のファイルを参照し、循環依存関係を形成します。

Madge は直接の循環依存関係のみを返すことができることに注意することが重要です。 2 つのファイルが 3 番目のファイルを通じて間接的な循環依存関係を形成している場合、そのファイルは Madge の出力には含まれません。

実際のプロジェクトの状況に基づいて、Madge は 6,000 行を超える結果ファイルを出力しました。結果ファイルは、2 つのファイル間の循環依存関係が直接参照されていないことを示しています。 2 つのターゲット ファイル間の間接的な依存関係を見つけることは、干し草の山から針を探すようなものでした。

間接的な循環依存関係を見つけるためのスクリプトの作成

次に、結果ファイルに基づいて 2 つのターゲット ファイル間の直接的または間接的な循環依存関係パスを見つけるスクリプトの作成を 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→component-entry.js→request.js→utils.js→component-entry.js

プロジェクト コードは最終的に Webpack によってバンドルされ、Babel を使用して ES5 コードにコンパイルされるため、バンドルされたコードの構造を確認する必要があります。

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 ========
})();

例の [数字] はコードの実行順序を示します。

簡易バージョン:

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'

Webpack モジュールのバンドル プロセス

AST 分析フェーズ中に、Webpack は ES6 のインポートおよびエクスポート ステートメントを探します。ファイルに次のステートメントが含まれている場合、Webpack はモジュールを「ハーモニー」タイプとしてマークし、エクスポート用に対応するコード変換を実行します。

https://github.com/webpack/webpack/blob/c586c7b1e027e1d252d68b4372f08a9bce40d96c/lib/dependency/HarmonyExportInitFragment.js#L161

https://github.com/webpack/webpack/blob/c586c7b1e027e1d252d68b4372f08a9bce40d96c/lib/RuntimeTemplate.js#L164

根本原因の概要

  1. 問題の症状: モジュールは定数をインポートしますが、実行時の実際の値は未定義です。

  2. 問題が発生する条件:

      プロジェクトは Webpack によってバンドルされ、ES5 コードにコンパイルされます (他のバンドラーではテストされていません)。
    • モジュール A は定数 foo を定義してエクスポートし、このモジュールは他のモジュールと循環依存関係を持っています。
    • モジュール B はモジュール A から foo をインポートし、モジュールの初期化プロセス中に実行されます。
    • モジュール A とモジュール B には循環依存関係があります。
  3. 根本原因:

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

以上がESrojects での循環依存関係の問題の解決の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。