首页 > web前端 > js教程 > 正文

解决 ESrojects 中的循环依赖问题

王林
发布: 2024-09-03 21:04:32
原创
990 人浏览过

Resolving Circular Dependency Issues in ESrojects

使用 Madge 和 ESLint 识别和修复 JavaScript 项目中的循环依赖关系的指南。

长话短说

  • 建议使用 ESLint 和规则来检查项目中的循环依赖。
  • 如果您的构建目标是 ES5,一旦模块导出常量,它就不应该导入其他模块。

问题症状

运行项目时,引用的常量输出为未定义。

例如:从 utils.js 导出的 FOO 被导入到 index.js 中,并且其值打印为 undefined。

// 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之间的循环依赖引起的。

下一步是识别两个模块之间的循环依赖路径以验证假设。

寻找循环依赖

安装 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
登录后复制

运行脚本后,得到一个二维数组。

二维数组存储了项目中所有的循环依赖。每个子数组代表一个特定的循环依赖路径:索引n处的文件引用索引n+1处的文件,最后一个文件引用第一个文件,形成循环依赖。

需要注意的是,Madge 只能返回直接循环依赖。如果两个文件通过第三个文件形成间接循环依赖关系,则该文件不会包含在 Madge 的输出中。

根据项目实际情况,Madge 输出了 6000 多行的结果文件。结果文件显示,两个文件之间可疑的循环依赖关系并不是直接引用的。寻找两个目标文件之间的间接依赖就像大海捞针一样。

编写脚本来查找间接循环依赖项

接下来,我请 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 → 组件入口.js → request.js → utils.js → 组件入口.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 ========
})();
登录后复制

示例中,[number]表示代码的执行顺序。

简化版:

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 会将模块标记为“harmony”类型,并为导出执行相应的代码转换:

https://github.com/webpack/webpack/blob/c586c7b1e027e1d252d68b4372f08a9bce40d96c/lib/dependencies/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中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责声明 Sitemap
PHP中文网:公益在线PHP培训,帮助PHP学习者快速成长!