Rolf B: html2canvas in .js importieren

Beitrag lesen

Hallo ebody,

ich komme mit dieser Importlogik auch nicht wirklich klar. Es scheint aber, als müsstest Du das .esm.js für import verwenden, nur dieses verwendet die Syntax von ECMAScript-Modulen, um einen Export bereitzustellen.

Exportiert wird die html2canvas-Funktion als default, d.h. du musst

import html2canvas from 'html2canvas.esm.js'

machen. Zumindest hat das bei mir funktioniert.

Die "Normalversion" des Scripts, ohne .esm, hat eine Funktion "factory", die die html2canvas-Funktion zurückgibt. Wo die dann landet, dafür werden 3 Fälle unterschieden:

  1. es gibt ein Objekt exports und eine Variable module, die nicht auf undefined steht. In dem Fall wird module.exports auf das Ergebnis der Factory gesetzt.

  2. Es gibt eine Funktion define und diese Funktion hat ein Property namens amd. In diesem Fall wird die Factory-Funktion an define übergeben.

  3. Falls es ein globales this gibt, wird dies als globaler Kontext verwendet. Wenn nicht, wird der Inhalt von self als globaler Kontext verwendet. Und dann wird im globalen Kontext das Ergebnis der Factory als html2canvas abgelegt.

Was zum grundgütigen Geier bedeutet das?

(1) ist ein Pattern, dass meines Wissens nur in node.js funktioniert. Aber html2canvas in einem node.js Server laufen zu lassen, ergibt vermutlich keinerlei Sinn. Entweder gibt es browsertaugliche Modul-Lader, die sich so wie node.js verhalten, oder der liebe Niklas aus L.A. hat diese Präambel einfach irgendwoher kopiert. Das "irgendwoher" kann aber auch ein Tool sein, das ECMAScript-Module rückwärtskompatibel macht, und dieses Tool setzt diese Universal-Präambel blindlings ein.

(2) ist das Pattern eines common.js Modul-Laders wie z.B. require.js. Ein solcher Modul-Lader bietet 2 globale Funktionen an, require und define, und mit define macht man Module bekannt, die andere mit require erhalten können. Wenn ich foo.js lade und darin define(function()...) aufrufe, wird im Modul-Lader ein Modul namens 'foo' erzeugt. Wie auch immer diese Magie funktioniert...

(3) ist der Default und erzeugt einfach eine globale Funktion.

Nichts von den dreien ist für ECMAScript-Module geeignet.

TL;DR: Verwende die .esm.js Version.

Wenn Du html2canvas nicht global verwenden willst, sondern es erst im Konstruktor importieren möchtest, kannst Du die "function style" Version vom import nutzen. Die arbeitet allerdings asynchron - muss sie tun, weil ja eine externe Ressource geladen wird und das Script darauf nicht warten darf - und macht erstmal nur leere Versprechungen:

class SelectImage{
    constructor(pm) {
        import('/html2canvas/html2canvas.esm.js')
        .then(module => {
            this.html2canvas = module.default;
        });
    }
}

Das wird Dir aber vermutlich nicht schmecken, denn Du kannst nun nicht hergehen und dies hier tun:

let selector = new SelectImage(pm);
selector.capture();   // FAIL

Wenn die capture-Methode html2canvas verwenden will, schlägt das fehl. Diese Eigenschaft wird erst gesetzt, wenn das Modul geladen und das Promise erfüllt ist.

Was zum Teufel ist ein Promise?

Du bräuchtest in SelectImage also noch einen Mechanismus, mit dem Du das erfüllte Promise der import-Funktion an den Nutzer bekannt geben kannst. Zum Beispiel so:

class SelectImage{
    constructor(pm) {
        return import('/html2canvas/html2canvas.esm.js')
               .then(module => {
                   this.html2canvas = module.default;
                   return this;
               });
    }
}

new SelectImage(pm)
.then(selector => {
   selector.capture();
});

Wenn ein Konstruktor ein Objekt explizit zurückgibt, dann wird das zum Ergebnis des new. In diesem Fall ist es das von .then zurückgegebene Folge-Promise, dessen Wert das erzeugte Objekt ist. Und darauf kannst Du einen eigenen .then-Handler legen. Das ist allerdings ein Programmiermuster, bei dem sich dem unschuldigen Erstleser erstmal die Haare sträuben.

Das Laden mit der import-Funktion ist, denke ich, aber auch nachteilig. Sie verzögert das Laden von html2canvas so lange, bis das Modul wirklich gebraucht wird, und muss dann erstmal warten, bis es verfügbar ist. Das import-Statement sorgt dafür, dass das importierte Modul in dem Moment bestellt wird, wo das Importeur-Modul geladen ist. Der Browser wartet nicht darauf, dass der Code darin verwendet wird. Es könnte sogar sein, dass zuerst mal alle importierten Module geladen werden, bevor die erste Zeile Code des Importeurs losläuft.

Wenn deine Klasse SelectImage selbst in einem ECMAScript-Modul steckt, brauchst Du vor globalen Variablen, in denen importierte Module stecken, aber keine Angst zu haben. Die sind nur im Modul global, und außerhalb des Moduls nicht sichtbar. Das ist okay.

Rolf

--
sumpsi - posui - obstruxi