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 B
do 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
.)
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();
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
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/
Voici ce que j'ai utilisé dans ma propre bibliothèque:
import C from './internal/c'
class A extends C {
// ...
}
export {A as default}
import C from './internal/c'
class B extends C {
// ...
}
export {B as default}
class C {
}
export {C as default}
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}
import './c.js'
import './internal/a.js'
export {A as default}
import './c.js'
import './internal/b.js'
export {B as default}
import A from './app/a'
console.log('Entrypoint', A)