Michael_K: Anwendung vom ESM Modulen im Browser

Hallo,

irgendwie stehe ich auf dem Schlauch. Mir ist der grundsätzliche Unterschied zwischen CJS und ESM-Modulen bekannt. Was ich aber nicht verstehe ,wie ich ESM- Module im Browser nutzen kann (client-seitig), wenn das ESM-Modul Abhängigigkeiten zu anderen Modulen besitzt? Wie sollte man die Ordnerstruktur wählen?

Als Beispiel: Ich würde gerne das npm Module file-type (https://www.npmjs.com/package/file-type) verwenden (im Browser client-seitig JS). Dieses Module hat aber Abhängigkeiten zu anderen Modulen. In der Docu steht, dass die Verwendung im Browser durch import {fileTypeFromStream} from 'file-type'; erfolgt. Aber wie und wo muss ich die anderen abhängigen Module hinterlegen?

Gruss, Michael

  1. Hallo Michael_K,

    Hallo,

    irgendwie stehe ich auf dem Schlauch. Mir ist der grundsätzliche Unterschied zwischen CJS und ESM-Modulen bekannt. Was ich aber nicht verstehe ,wie ich ESM-Module im Browser nutzen kann (client-seitig),

    ESM-Module sind ja EcmaScript-Module, die Du mit ES6 einbindest.

    JavaScript/Tutorials/OOP/Module_und_Kapselung

    wenn das ESM-Modul Abhängigigkeiten zu anderen Modulen besitzt? Wie sollte man die Ordnerstruktur wählen?

    In Node.js würde das geladene Modul die dependencies bereits enthalten und beide würden in der package.json bzw. package-lock.json erscheinen.

    Ich würde gerne das npm Module file-type (https://www.npmjs.com/package/file-type) verwenden (im Browser client-seitig JS). Dieses Module hat aber Abhängigkeiten zu anderen Modulen. In der Docu steht, dass die Verwendung im Browser durch import {fileTypeFromStream} from 'file-type'; erfolgt. Aber wie und wo muss ich die anderen abhängigen Module hinterlegen?

    Da Du das clientseitig machen willst, würde ich schauen, ob ein Minimal-Beispiel funktioniert und entsprechend mit der Konsole untersuchen / debuggen.

    Bis bald! Jonathan

    --
    "Ich habe heute ein Elan-Problem und mein Tatenvolumen ist fast aufgebraucht!"
    1. Hallo Jonathan,

      ich hatte Michaels Frage gelesen und mich bisher zurückgehalten, weil ich mit der Verwendung von NPM-Modulen im Browser keine Erfahrung habe.

      Was ich insbesondere nicht weiß, ist, wie sich die Logik von Node von der Browserlogik unterscheidet. Die ES6-Spec sagt nämlich, dass der Modulname, der im Import angegeben wird, abhängig von der Hostumgebung interpretiert wird. Der Host ist entweder Node, oder der Browser. Und in einer package.json kann man wohl angeben, ob das Paket für Node oder für den Browser geeignet ist. Bei file-type ist das der Fall, soweit konnte ich schon ins Thema einsteigen.

      Wenn ich in einem node.js Programm import { xyz } from 'some-module'; schreibe, und some-module der Name eines per NPM installierten Moduls ist, dann ist es meines Wissens node.js, der die package.json interpretiert und von dort erfährt, welche .js Datei zu laden ist. Und node.js ist es auch, der die package.json-Dateien der abhängigen Module einliest und daher weiß, wie er Folge-Imports aufzulösen hat.

      Im Browser gibt's das nach meiner Kenntnis so nicht, dort muss ich "./somefile.js" als Modulname angeben, sprich: direkt die benötigte .js Datei. Und bei Folgeimports ist keine Instanz dazwischen, die bei einem import ... from 'other-module'; eingreift und in einer package.json nachschaut, welche .js Datei im Browser-Kontext zu laden ist. Und genau an der Stelle dürfte auch Michaels Problem sein.

      Es gibt diverse Beispiele im Netz, die erklären möchten, wie man das im Browser tut. Zentral dafür scheint das NPM-Paket browserify zu sein, und man muss wohl auch ein Kapselmodul erstellen, von dem aus man die NPM-Module importiert.

      Ob das tatsächlich alles so ist und wo die Fallstricke liegen, dafür müsste ich mich ein paar Stunden hinsetzen und basteln. Dafür hatte ich nur bisher keine Zeit. Wenn Du einschlägige Erfahrungen hast, dann würde ich mich freuen, wenn Du die Punkt für Punkt darstellen könntest. Gerne als Blog-Beitrag.

      Rolf

      --
      sumpsi - posui - obstruxi
      1. Hallo Rolf,

        du hast es sehr gut erklärt, wo meine Fragestellung liegt. Die selfhtml-Seiten hatte ich schon durchgelesen, bin aber nicht richtig fündig geworden. Die Frage stellt sich, ob und wie ich bei einem Browser eine package.json oder wie auch immer hinterlegen kann, damit der Browser weiss, wo die Module liegen, um diese dann zu importieren. Oder ob esper Spezifikation ein Logik gibt, nach der eine Browser das passende Module findet. Bei Nodejs sucht zum Beispiel die Module schrittweise im übergeordneten Order, sofern im Arbeitsverzeichnis ein passendes Modul nicht gefunden wird.

        Viele Grüße Michael

        1. Hallo Michael_K,

          nee, das ist alles nicht so einfach. Man braucht wohl Webpack, um das Ding im Browser zum Fliegen zu bringen. Und da brech ich mir gerade die Finger - WebPack 4 versteht den Sourcecode von file-type nicht mehr und Webpack 5 schmeißt mir andere Fehler um die Ohren.

          Kennst Du Dich mit Webpack aus?

          Rolf

          --
          sumpsi - posui - obstruxi
          1. Update: Ich konnte file-type jetzt mit Webpack 5 zum Übersetzen bringen, aber es ist eine Quälerei. Der Autor pfeift auf den Browser und entwickelt primär für Node - es gibt keine Doku, was man alles tun muss, damit es im Browser funktioniert.

            (1) Man muss seinen eigenen Code in ein ES6-Modul stecken

            (2) Man muss entweder aus seinem Modul heraus eine globale Variable setzen, um auf exportierte Elemente zugreifen zu können, ODER man muss nach jeder Codeänderung Webpack neu laufen lassen. Denn ein export Statement wird von Webpack nicht ins Bundle übertragen.

            (3) Man muss im Projektordner

            npm install webpack --save-dev npm install webpack-cli --save-dev npm install buffer npm install file-type

            ausführen, und nun kommt der Extraspaß: Das richtige Setup von Package und Webpack.

            In der package.json wird gebraucht:

            • "private": "true"
            • "type": "module"

            Die webpack.config.js, die in allen Beispielen mit module.exports hantiert, funktioniert in einem ESM-Projekt natürlich anders. Man muss das Config-Objekt nicht an module.exports zuweisen, sondern als Default exportieren:

            const nodeResolverPlugin = ?!?!?!;
            const distPath = ?!?!?!;
            
            export default {
               entry: "./src/main.js",
               output: {
                  path: distPath,
                  filename: 'app.js'
               },
               mode: "production",
               plugins: [ nodeResolverPlugin ]
            };
            

            Im src Order steht der js Code, den man selbst schreibt. Webpack bündelt das zusammen mit allen node-modules und schreibt es in den output-Ordner. Unter path muss ein absoluter Path angegeben werden. Das kann man so lösen:

            import path from 'path';
            const distPath = path.resolve("./dist");
            

            dist steht für Distribution, und die Angaben von ./src und ./dist setzen voraus, dass diese beiden Unterordner im gleichen Ordner wie die webpack.config.js stehen.

            Für Code in der webpack.config.js kann man munter mit Node-Libraries herumhantieren, weil dieses Script von Webpack ausgeführt wird, also in der Node-Umgebung.

            Das nodeResolverPlugin ist lebensnotwendig, das habe ich mühsam aus den Issues vom file-type Projekt herausgefunden. Einige der Dependencies von file-type verwenden nämlich `import xyz from 'node:stream' - und das will man nicht - der Code soll ja im Browser laufen. Deshalb schreibt man diesen Helper (1:1 aus den Issues kopiert):

            import webpack from 'webpack';
            
            const nodeResolverPlugin = new webpack.NormalModuleReplacementPlugin(/node:/, (resource) => {
              const mod = resource.request.replace(/^node:/, "");
              switch (mod) {
                case "buffer":
                  resource.request = "buffer";
                  break;
                case "stream":
                  resource.request = "readable-stream";
                  break;
                default:
                  throw new Error(`Not found ${mod}`);
              }
            });
            

            Damit werden die diversen import ... from 'node:buffer' Anweisungen von der nodeJs Abhängigkeit befreit. readable-stream ist ein npm-Paket, das mit file-type mitkommen sollte, bei mir aber nicht immer mitkam. Wenn es fehlt, kann es explizit als readable-stream@3.6.2 nachinstalliert werden. Nicht die 4-er Version verwenden, die hat bei mir nur gekracht.

            Damit konnte ich dann die Application durch Webpack durchlaufen lassen. Ohne Webpack geht gar nichts, weil die Import-Laufzeitumgebung von Node im Browser komplett fehlt.

            Nur - ein Aufrufversuch schlug dann wieder fehl, weil eins der Module auf die Variable process zugreifen wollte, die in Node möglicherweise global ist und im Browser nicht existiert. Das muss man patchen, das scheint ein Fehler in readable-stream.js zu sein.

            Man installiere noch das NPM-Modul process (Userland-Implementierung von 'node:process') und ändere in readable-stream/lib die Datei _stream_readable.js so ab:

            'use strict';
            
            module.exports = Readable;
            
            var process = require('process');   // <<- diese Zeile einfügen
            

            Und HEY, nach nur wenigen Stunden läuft dieser Dreck!!!

            Meine Sourcen:

            // main.js
            import {getFileType} from "./getFileType.js";
            
            const url = 'https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg';
            
            console.log("Query file type for ", url);
            try {
              const t = await getFileType('https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg');
              console.log("File Type is ", t);
            }
            catch (err) {
              console.log("that failed: ", err);
            }
            
            // getFileType.js
            import { fileTypeFromStream } from "file-type";
            
            export { getFileType };
            
            async function getFileType(url) {
            
            	const response = await fetch(url);
            	const fileType = await fileTypeFromStream(response.body);
            	
            	return fileType;
            }
            
            // webpack.config.js
            import path from 'path';
            import webpack from 'webpack';
            
            const distPath = path.resolve('./dist');
            console.log("Distpath=",distPath);
            
            const nodeResolverPlugin = new webpack.NormalModuleReplacementPlugin(/node:/, (resource) => {
              const mod = resource.request.replace(/^node:/, "");
              switch (mod) {
                case "buffer":
                  resource.request = "buffer";
                  break;
                case "stream":
                  resource.request = "readable-stream";
                  break;
                default:
                  throw new Error(`Not found ${mod}`);
              }
            })
            export default {
              entry: './src/main.js',
              output: {
                path: distPath,
                filename: 'app.js',
              },
              mode: 'production',
              plugins: [
                nodeResolverPlugin
              ]
            };
            

            Und die package.json - in der sind aber nur die beide Zeilen mit private und type von mir ergänzt. Die Dependencies macht NPM. Wichtig ist nur, dass Webpack mit --save-dev installiert wird, damit es in den devDependencies erscheint. Es schadet tatächlich nichts, wenn es falsch steht, weil es in der App keinen Import darauf gibt und es deshalb vom Shaker wieder entfernt wird, aber es stört nur beim Build.

            {
              "name": "file-type-test",
              "version": "1.0.0",
              "description": "",
              "private": "true",
              "type": "module",
              "scripts": {
                "test": "echo \"Error: no test specified\" && exit 1"
              },
              "keywords": [],
              "author": "",
              "license": "ISC",
              "dependencies": {
                "buffer": "^6.0.3",
                "file-type": "^18.4.0",
                "process": "^0.11.10"
              },
              "devDependencies": {
                "webpack": "^5.85.0",
                "webpack-cli": "^5.1.1"
              }
            }
            

            Test.html zum Einbinden:

            <!doctype html>
            <html>
            <head>
            <title>NPM Module</title>
            <script type="module" src="./dist/app.js"></script>
            </head>
            <body>
            <h1>Hello World</h1>
            </body>
            </html>
            

            Ist das jetzt schön? Nö. Aber göht - öhm - geht.

            Rolf

            --
            sumpsi - posui - obstruxi
      2. Hallo Rolf,

        Hallo Jonathan,

        ich hatte Michaels Frage gelesen und mich bisher zurückgehalten, weil ich mit der Verwendung von NPM-Modulen im Browser keine Erfahrung habe.

        Ich auch nicht. Ich habe hier mal was Allgemeines zu Modulen geschreiben:

        https://wiki.selfhtml.org/wiki/Node.js/Erste_Schritte#Module

        Ob das tatsächlich alles so ist und wo die Fallstricke liegen, dafür müsste ich mich ein paar Stunden hinsetzen und basteln. Dafür hatte ich nur bisher keine Zeit. Wenn Du einschlägige Erfahrungen hast, dann würde ich mich freuen, wenn Du die Punkt für Punkt darstellen könntest. Gerne als Blog-Beitrag.

        Ich habe jetzt 2 Stunden gegoogelt und suche irgendwie noch den Use-Case für so etwas.

        Das könnten Ansatzpunkte sein:

        https://www.freecodecamp.org/news/how-to-set-up-a-front-end-development-project/

        https://blog.cloudboost.io/how-to-run-node-js-apps-in-the-browser-3f077f34f8a5

        Das werde ich aber nicht heute und nicht nächste Woche schaffen!

        Bis bald! Jonathan

        --
        "Ich habe heute ein Elan-Problem und mein Tatenvolumen ist fast aufgebraucht!"
        1. Hallo Jonathan,

          vielen Dank, ich bin schon gespannt auf das Weitere.

          Der "How to set up a front end development project" Artikel bezieht sich, wenn ich das beim Überfliegen richtig verstand, auf Node.js als Tool-Runner beim Entwickeln. Da gibt's viel Potenzial, vor allem für Webpack als Bundler oder Grunt als Taskrunner.

          "How to Run Node.js Apps in the Browser" ist wieder was anderes, der Autor startet Node im Hintergrund und zeigt das Node-Terminal offenbar mit einer XServer-Implementierung im Browser an.

          Michael möchte ein NPM Moduls, das bestimmte Funktionen anbietet, im Browser benutzen. Je nachdem, wie intensiv sich dieses Modul auf Node verlässt, ist das einfach oder kompliziert.

          Ich guck mal, dass ich meine Webpack-Erkenntnisse noch besser aufgeschrieben bekomme.

          Rolf

          --
          sumpsi - posui - obstruxi