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#L161https://github.com/webpack/webpack/blob/c586c7b1e027e1d252d68b4372f08a9bce40d96c/lib/RuntimeTemplate.js#L164
根本原因の概要
- 問題の症状: モジュールは定数をインポートしますが、実行時の実際の値は未定義です。
- 問題が発生する条件:
- プロジェクトは Webpack によってバンドルされ、ES5 コードにコンパイルされます (他のバンドラーではテストされていません)。
- モジュール A は定数 foo を定義してエクスポートし、このモジュールは他のモジュールと循環依存関係を持っています。
- モジュール B はモジュール A から foo をインポートし、モジュールの初期化プロセス中に実行されます。
- モジュール A とモジュール B には循環依存関係があります。
- 根本原因:
- 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 サイトの他の関連記事を参照してください。

ホットAIツール

Undresser.AI Undress
リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover
写真から衣服を削除するオンライン AI ツール。

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

Video Face Swap
完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

人気の記事

ホットツール

メモ帳++7.3.1
使いやすく無料のコードエディター

SublimeText3 中国語版
中国語版、とても使いやすい

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

SublimeText3 Mac版
神レベルのコード編集ソフト(SublimeText3)

ホットトピック











さまざまなJavaScriptエンジンは、各エンジンの実装原則と最適化戦略が異なるため、JavaScriptコードを解析および実行するときに異なる効果をもたらします。 1。語彙分析:ソースコードを語彙ユニットに変換します。 2。文法分析:抽象的な構文ツリーを生成します。 3。最適化とコンパイル:JITコンパイラを介してマシンコードを生成します。 4。実行:マシンコードを実行します。 V8エンジンはインスタントコンピレーションと非表示クラスを通じて最適化され、Spidermonkeyはタイプ推論システムを使用して、同じコードで異なるパフォーマンスパフォーマンスをもたらします。

Pythonは、スムーズな学習曲線と簡潔な構文を備えた初心者により適しています。 JavaScriptは、急な学習曲線と柔軟な構文を備えたフロントエンド開発に適しています。 1。Python構文は直感的で、データサイエンスやバックエンド開発に適しています。 2。JavaScriptは柔軟で、フロントエンドおよびサーバー側のプログラミングで広く使用されています。

C/CからJavaScriptへのシフトには、動的なタイピング、ゴミ収集、非同期プログラミングへの適応が必要です。 1)C/Cは、手動メモリ管理を必要とする静的に型付けられた言語であり、JavaScriptは動的に型付けされ、ごみ収集が自動的に処理されます。 2)C/Cはマシンコードにコンパイルする必要がありますが、JavaScriptは解釈言語です。 3)JavaScriptは、閉鎖、プロトタイプチェーン、約束などの概念を導入します。これにより、柔軟性と非同期プログラミング機能が向上します。

Web開発におけるJavaScriptの主な用途には、クライアントの相互作用、フォーム検証、非同期通信が含まれます。 1)DOM操作による動的なコンテンツの更新とユーザーインタラクション。 2)ユーザーエクスペリエンスを改善するためにデータを提出する前に、クライアントの検証が実行されます。 3)サーバーとのリフレッシュレス通信は、AJAXテクノロジーを通じて達成されます。

現実世界でのJavaScriptのアプリケーションには、フロントエンドとバックエンドの開発が含まれます。 1)DOM操作とイベント処理を含むTODOリストアプリケーションを構築して、フロントエンドアプリケーションを表示します。 2)node.jsを介してRestfulapiを構築し、バックエンドアプリケーションをデモンストレーションします。

JavaScriptエンジンが内部的にどのように機能するかを理解することは、開発者にとってより効率的なコードの作成とパフォーマンスのボトルネックと最適化戦略の理解に役立つためです。 1)エンジンのワークフローには、3つの段階が含まれます。解析、コンパイル、実行。 2)実行プロセス中、エンジンはインラインキャッシュや非表示クラスなどの動的最適化を実行します。 3)ベストプラクティスには、グローバル変数の避け、ループの最適化、constとletsの使用、閉鎖の過度の使用の回避が含まれます。

PythonとJavaScriptには、コミュニティ、ライブラリ、リソースの観点から、独自の利点と短所があります。 1)Pythonコミュニティはフレンドリーで初心者に適していますが、フロントエンドの開発リソースはJavaScriptほど豊富ではありません。 2)Pythonはデータサイエンスおよび機械学習ライブラリで強力ですが、JavaScriptはフロントエンド開発ライブラリとフレームワークで優れています。 3)どちらも豊富な学習リソースを持っていますが、Pythonは公式文書から始めるのに適していますが、JavaScriptはMDNWebDocsにより優れています。選択は、プロジェクトのニーズと個人的な関心に基づいている必要があります。

開発環境におけるPythonとJavaScriptの両方の選択が重要です。 1)Pythonの開発環境には、Pycharm、Jupyternotebook、Anacondaが含まれます。これらは、データサイエンスと迅速なプロトタイピングに適しています。 2)JavaScriptの開発環境には、フロントエンドおよびバックエンド開発に適したnode.js、vscode、およびwebpackが含まれます。プロジェクトのニーズに応じて適切なツールを選択すると、開発効率とプロジェクトの成功率が向上する可能性があります。
