web-dev-qa-db-fra.com

Comment réparer cette dépendance circulaire du module ES6?

EDIT: pour plus de détails, voir également le discussion sur ES Discuter .


J'ai trois modules A, B et C. A et B importent l'exportation par défaut du module C et le module C importe les valeurs par défaut de A et B. Cependant, le module C ne dépend pas des valeurs importées de A et B lors de l'évaluation du module, mais uniquement à l'exécution à un moment donné après l'évaluation des trois modules. Les modules A et Bdo dépendent de la valeur importée de C lors de l'évaluation de leur module.

Le code ressemble à ceci:

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

J'ai le point d'entrée suivant:

// --- Entrypoint

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

Mais ce qui se passe réellement, c'est que le module B est évalué en premier, et il échoue avec cette erreur dans Chrome (en utilisant des classes natives ES6, sans transpiler):

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

Cela signifie que la valeur de C dans le module B lorsque le module B est en cours d'évaluation est undefined car le module C n'a pas encore été évalué.

Vous devriez être capable de reproduire facilement en créant ces quatre fichiers et en exécutant le fichier entrypoint.

Mes questions sont (puis-je avoir deux questions concrètes?): Pourquoi l'ordre de chargement est-il ainsi? Comment peut-on écrire les modules dépendant d'une circulaire pour qu'ils fonctionnent de sorte que la valeur de C lors de l'évaluation de A et de B ne soit pas undefined?

(Je pense que l’environnement du module ES6 pourra peut-être découvrir de façon intelligente qu’il devra exécuter le corps du module C avant de pouvoir éventuellement exécuter les corps des modules A et B.)

31
trusktr

Je recommanderais d'utiliser l'inversion de contrôle. Rendez votre constructeur C pur en ajoutant un paramètre A et un paramètre B comme ceci: 

// --- 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/-KlDeP9Rb60MehsCMa8u

Mise à jour, en réponse à ce commentaire: Comment résoudre cette dépendance circulaire du module ES6?

Sinon, si vous ne souhaitez pas que le consommateur de la bibliothèque soit informé de diverses implémentations, vous pouvez soit exporter une autre fonction/classe qui cache ces détails:

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

ou utilisez ce motif:

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

Mise à jour, en réponse à ce commentaire: Comment résoudre cette dépendance circulaire du module ES6?

Pour permettre à l'utilisateur final d'importer n'importe quel sous-ensemble des classes, créez simplement un fichier lib.js exportant l'api public:

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

ou: 

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

Ensuite vous pouvez:

// --- 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

Il y a une autre solution possible ..

// --- Entrypoint

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

Oui c'est un hack dégoûtant mais ça marche

1
Jon Wyatt

Vous pouvez le résoudre avec chargement dynamique de modules

J'ai eu le même problème et je viens d'importer des modules de manière dynamique.

Remplacer l'importation à la demande:

import module from 'module-path';

avec une importation dynamique:

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

Dans votre exemple, vous devriez changer c.js comme ceci:

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}

Pour plus d'informations sur l'importation dynamique:

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

Il existe un autre moyen d'expliquer par leo, uniquement pour ECMAScript 2019:

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

Pour analyser la dépendance circulaire, Artur Hebda l'expliquez ici:

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

0
Mehdi Yeganeh

Voici ce que j'ai utilisé dans ma propre bibliothèque:

  1. Déclarez les modules "internes" qui déclarent des classes sans aucune dépendance circulaire.
  2. Déclarez les modules faisant face au public qui chargent les modules internes dans le bon ordre et ajoutez les méthodes qui doivent référencer des dépendances circulaires.
  3. Demandez à l'utilisateur d'importer l'un des modules destinés au public

interne/a.js

import C from './internal/c'

class A extends C {
    // ...
}

export {A as default}

interne/b.js

import C from './internal/c'

class B extends C {
    // ...
}

export {B as default}

interne/c.js

class C {
}

export {C as default}

c.js

import C from './internal/c'
import A from './a'
import B from './b'

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

a.js

import './c.js'
import './internal/a.js'

export {A as default}

b.js

import './c.js'
import './internal/b.js'

export {B as default}

Point d'accès

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