web-dev-qa-db-ja.com

このES6モジュールの循環依存関係を修正するには?

編集:より多くの背景については、 ESの議論に関する議論 も参照してください。


ABCの3つのモジュールがあります。 AおよびBは、モジュールCからデフォルトのエクスポートをインポートし、モジュールCは、AおよびBの両方からデフォルトをインポートします。ただし、モジュールCは、モジュール評価中にAおよびBからインポートされた値に依存せず、3つのモジュールすべてが評価された後のある時点でのみ実行されます。モジュールAおよびBdoは、モジュールの評価中にCからインポートされた値に依存します。

コードは次のようになります。

// --- Module A

import C from 'C'

class A extends C {
    // ...
}

export {A as default}

// --- Module B

import C from 'C'

class B extends C {
    // ...
}

export {B as default}

// --- Module C

import A from 'A'
import B from 'B'

class C {
    constructor() {
        // this may run later, after all three modules are evaluated, or
        // possibly never.
        console.log(A)
        console.log(B)
    }
}

export {C as default}

次のエントリポイントがあります。

// --- Entrypoint

import A from './app/A'
console.log('Entrypoint', A)

しかし、実際に起こるのは、モジュールBが最初に評価され、Chrome(トランスペアリングではなく、ネイティブES6クラスを使用)でこのエラーが発生して失敗することです。

Uncaught TypeError: Class extends value undefined is not a function or null

つまり、モジュールCが評価されているときのモジュールBBの値は、モジュールundefinedがまだ評価されていないため、Cになります。

これらの4つのファイルを作成し、エントリポイントファイルを実行することで、簡単に再現できるはずです。

私の質問は次のとおりです(具体的な質問が2つありますか?):読み込み順序がそのようになっているのはなぜですか? CおよびAを評価するときのBの値がundefinedにならないように循環依存モジュールをどのように書くことができますか?

(ES6モジュール環境は、モジュールCおよびAの本体を実行する前に、モジュールBの本体を実行する必要があることをインテリジェントに検出できると思います。)

35
trusktr

制御の反転を使用することをお勧めします。次のようなAおよびBパラメーターを追加して、Cコンストラクターを純粋にします。

// --- Module A

import C from './C';

export default class A extends C {
    // ...
}

// --- Module B

import C from './C'

export default class B extends C {
    // ...
}

// --- Module C

export default class C {
    constructor(A, B) {
        // this may run later, after all three modules are evaluated, or
        // possibly never.
        console.log(A)
        console.log(B)
    }
}

// --- Entrypoint

import A from './A';
import B from './B';
import C from './C';
const c = new C(A, B);
console.log('Entrypoint', C, c);
document.getElementById('out').textContent = 'Entrypoint ' + C + ' ' + c;

https://www.webpackbin.com/bins/-KlDeP9Rb60MehsCMa8

このコメントへの応答として更新します: このES6モジュールの循環依存関係を修正する方法?

または、ライブラリコンシューマにさまざまな実装について知らせたくない場合は、それらの詳細を隠す別の関数/クラスをエクスポートできます。

// Module ConcreteCImplementation
import A from './A';
import B from './B';
import C from './C';
export default function () { return new C(A, B); }

または、次のパターンを使用します。

// --- Module A

import C, { registerA } from "./C";

export default class A extends C {
  // ...
}

registerA(A);

// --- Module B

import C, { registerB } from "./C";

export default class B extends C {
  // ...
}

registerB(B);

// --- Module C

let A, B;

const inheritors = [];

export const registerInheritor = inheritor => inheritors.Push(inheritor);

export const registerA = inheritor => {
  registerInheritor(inheritor);
  A = inheritor;
};

export const registerB = inheritor => {
  registerInheritor(inheritor);
  B = inheritor;
};

export default class C {
  constructor() {
    // this may run later, after all three modules are evaluated, or
    // possibly never.
    console.log(A);
    console.log(B);
    console.log(inheritors);
  }
}

// --- Entrypoint

import A from "./A";
import B from "./B";
import C from "./C";
const c = new C();
console.log("Entrypoint", C, c);
document.getElementById("out").textContent = "Entrypoint " + C + " " + c;

このコメントへの応答として更新します: このES6モジュールの循環依存関係を修正する方法?

エンドユーザーがクラスのサブセットをインポートできるようにするには、lib.jsファイルを作成して、公開APIをエクスポートします。

import A from "./A";
import B from "./B";
import C from "./C";
export { A, B, C };

または:

import A from "./A";
import B from "./B";
import C from "./ConcreteCImplementation";
export { A, B, C };

その後、次のことができます。

// --- Entrypoint

import { C } from "./lib";
const c = new C();
const output = ["Entrypoint", C, c];
console.log.apply(console, output);
document.getElementById("out").textContent = output.join();
3
msand

別の解決策があります。

// --- Entrypoint

import A from './app/A'
setTimeout(() => console.log('Entrypoint', A), 0)

はい、それは嫌なハックですが、動作します

1
Jon Wyatt

動的にモジュールをロードすることで解決できます

同じ問題があり、モジュールを動的にインポートするだけです。

オンデマンドインポートの置換:

import module from 'module-path';

動的にインポートする場合:

let module;
import('module-path').then((res)=>{
    module = res;
});

この例では、c.jsを次のように変更する必要があります。

import C from './internal/c'
let A;
let B;
import('./a').then((res)=>{
    A = res;
});
import('./b').then((res)=>{
    B = res;
});

// See http://stackoverflow.com/a/9267343/14731 for why we can't replace "C.prototype.constructor"
let temp = C.prototype;
C = function() {
  // this may run later, after all three modules are evaluated, or
  // possibly never.
  console.log(A)
  console.log(B)
}
C.prototype = temp;

export {C as default}

動的インポートの詳細:

http://2ality.com/2017/01/import-operator.html

Leoで説明する別の方法があります、ECMAScript 2019のためだけに:

https://stackoverflow.com/a/40418615/1972338

循環依存関係を分析するには、Artur Hebdaがここで説明します。

https://railsware.com/blog/2018/06/27/how-to-analyze-circular-dependencies-in-es6/

0
Mehdi Yeganeh

ここに私のために働いた簡単な解決策があります。私は最初に trusktrのアプローチ を試しましたが、奇妙なeslintとIntelliJ IDEA=警告(クラスは宣言されていないと宣言されたと主張しました)。依存関係のループを排除します。

  1. 循環依存関係を持つクラスを2つの部分に分割します:ループをトリガーするコードとそうでないコード。
  2. ループをトリガーしないコードを「内部」モジュールに配置します。私の場合、スーパークラスを宣言し、サブクラスを参照するメソッドをすべて削除しました。
  3. 公開用モジュールを作成します。
    • import最初に内部モジュール。
    • import依存関係ループをトリガーしたモジュール。
    • 手順2で削除したメソッドを追加し直します。
  4. ユーザーに公開モジュールをインポートしてもらいます。

OPの例は、手順3でコンストラクターを追加するのが通常のメソッドを追加するよりもはるかに難しいため、少し工夫されていますが、一般的な概念は同じままです。

internal/c.js

// Notice, we avoid importing any dependencies that could trigger loops.
// Importing external dependencies or internal dependencies that we know
// are safe is fine.

class C {
    // OP's class didn't have any methods that didn't trigger
    // a loop, but if it did, you'd declare them here.
}

export {C as default}

c.js

import C from './internal/c'
// NOTE: We must import './internal/c' first!
import A from 'A'
import B from 'B'

// See http://stackoverflow.com/a/9267343/14731 for why we can't replace
// "C.prototype.constructor" directly.
let temp = C.prototype;
C = function() {
  // this may run later, after all three modules are evaluated, or
  // possibly never.
  console.log(A)
  console.log(B)
}
C.prototype = temp;

// For normal methods, simply include:
// C.prototype.strippedMethod = function() {...}

export {C as default}

他のすべてのファイルは変更されません。

0
Gili