Andreas Bruns

Softwareentwicklung für Oldenburg und Bremen

Modul-Systeme für JavaScript im Überblick

Wenn man die ersten Tutorials eines Frameworks im JavaScript-Universum durchführt (z.B. Node, TypeScript, Angular, React, Ionic), funktionieren meistens alle Schritte relativ problemlos. Doch sobald wir fremde Bibliotheken einbinden oder sogar selber wiederverwendbare Bibliotheken erstellen wollen, müssen wir uns mit dem Modul-Begriff in JavaScript auseinandersetzen.

In diesem Artikel versuche ich, für JavaScript-Neulinge (wie ich es war) ein bischen Klarheit in den Zoo von Begriffen im Modul-Umfeld zu schaffen. Folgende Stichwörter sollten wir anschließend einordnen können: ES2015, AMD, CommonJS, UMD, SystemJS, Babel, Browserify, RequireJS, Webpack, Module Loader, Module Bundler, import, export, define, require.

Grundlagen

Ursprünglich gab es für das JavaScript der Browser kein Modul-Konzept, was bei wachsender Popularität von Browser-Anwendungen zu Problemen führte:

  • nur ein globaler Namensraum für Variablen
    => unterschiedliche Bibliotheken konnten sich ihre Variablen überschreiben
  • synchrones Laden und Ausführen des JavaScripts blockiert Browser
    => große Anwendungen wurden immmer langsamer
  • abhängige Module müssen manuell eingebunden werden
    => Auflösung von Abhängigkeiten erfolgt automatisch

Durch die Einführung eines Modul-Konzepts lassen sich Variablen und Methoden kapseln. Bei entsprechender Unterstützung werden abhängige Module asynchron und in der richtigen Reihenfolge geladen.

Für unseren eigenen Code müssen wir zunächst die Runtime-Umgebung des JavaScript-Codes festlegen, also ob unser JavaScript im Browser oder auf dem Server ausgeführt wird. Und für die Browser-Umgebungen ist es relevant, ob unsere Anwendung auch in veralteten Browsern ohne moderne Modul-Unterstützung funktionieren muss.

Modul-Systeme

Prinzipiell haben wir die Wahl zwischen den folgenden Modul-Sytemen:

  • kein Modul-System: entspricht der urspüngliche Verarbeitung von JavaScript in den Browsern mit den genannten Nachteilen
  • AMD: von RequireJS und DOJO beeinflusster Modul-Standard , um im Browser Module asynchron laden zu können (Asynchronous Module Definition)
  • CommonJS: Modul-System von NodeJS für Serveranwendungen, das nur das synchrone Laden von Modulen vorsieht
  • UMD: Modul-System um CommonJS– und AMD-Module gemeinsam einbinden zu können
  • ES2015/ES6-Module: Modul-System der ECMAScript-Specifikation, der derzeit von neuen Browsern aber nicht von NodeJS unterstützt wird
  • SystemJS: unterstützt ES2015-Module als Polyfill (unabhängig vom Browser-Support) und CommonJS– und AMD-Module

Falls man nicht das korrekte Modul-System verwendet, erzeugt JavaScript einen Fehler, dass ein unbekanntes Schlüsselwort verwendet wird. Der folgenden Tabelle können wir die Schlüsselwörter der Modul-Systeme entnehmen.

Modul-SystemSchlüsselwörter
AMDdefine, require
CommonJSmodule.exports, require,
ES2015-Module (Standard)export, import

Für eine einfache Einbindung von verbreiteten Bibliotheken werden diese oft zusätzlich in einer ES2015-Version angeboten (moment.js / moment-es6, lodash / lodash-es).

Loader und Bundler

Um eine Vielzahl von Modulen und deren abhängigen Module im eigenen Webprojekt verwenden zu können, lohnt sich ein Blick auf sogenannte Module Loader und Module Bundler.

Module Loader laden während der App-Initialisierung im Browser alle benötigten Module in der korrekten Reihenfolge. Bekannte Vertreter sind RequireJS (AMD) und SytemJS (CommonJS, AMD, ES2015-Module).

Module Bundler hingegen werten die Modul-Eigenschaften während des Builds aus und führen zumeist noch Optimierungen durch:

  • Auflösen der Modul-Abhängigkeiten
  • Entfernung von nicht benötigten Code (Tree Shaking)
  • Zusammenführen aller JS-Dateien zu einer JS-Datei
  • Code Splitting aller JS-Dateien in mehrere Dateien (Chunks)
  • Umwandeln in JS-Version für ältere Browser (mit Babel)
  • Minify des JavaScript-Codes (z.B. Kommentare, Whitespaces entfernen)

Falls man CommonJS-Module im Browser verwenden möchte, bietet sich Browserify als Module Bundler an. Für einfache Bundle-Aufgaben mit ES2015-Module ohne Konfigurationsaufwand sind Parcel und Rollup sehr passend. Und wer ein mächtigeres Werkzeug benötigt und ein bischen Konfigurationsarbeit nicht scheut, kann Webpack für AMD-, CommonJS– und ES2015-Module nutzen.

TypeScript

Der Einsatz von TypeScript bietet sich in vielen Projekten an, weil wir dank der statischen Typisierung verständlicheren und fehlerfreieren Programmcode entwickeln können.

TypeScript generiert, wie ein Module Bundler, aus den TypeScript-Dateien entsprechende JavaScript-Dateien. In der TypeScript-Konfiguration tsconfig.json können wir dabei die zu erzeugende JavaScript-Version und das zu verwendende Modul-System angeben.

Bei installiertem TypeScript erhalten wir mit „tsc –init“ eine ausführliche TypeScript-Konfiguration und treffen in den obersten Zeilen auf die bekannten Modul-Systeme.

{
  "compilerOptions": {
    /* Specify ECMAScript target version: 'ES3' (default), 'ES5',
       'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 
       'ES2020', or 'ESNEXT'. */
    "target": "es5",
    /* Specify module code generation: 'none', 'commonjs', 'amd', 
       'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    "module": "commonjs", 
    /* Enables emit interoperability between CommonJS and ES Modules
       via creation of namespace objects for all imports. 
       Implies 'allowSyntheticDefaultImports'. */
    "esModuleInterop": true,
    ...
  }
}

Praxis

Wenn wir nur im Server-Umfeld mit NodeJS entwickeln, fällt die Wahl leicht, weil dort der ES2015-Modul-Standard noch nicht komplett unterstützt wird und wir daher die bewährten CommonJS-Module einsetzen müssen.

Im Browser-Umfeld sollten wir ES2015-Module einsetzen und benötigen für ältere Browser ohne ES2015-Unterstützung ggfs. eine der alternativen Modul-Lösungen.

Beim Einsatz von TypeScript können wir ES2015 als Ziel-Modul deklarieren. Dabei wird jede TypeScript-Datei zu einer gültigen JavaScript-Datei nach dem ES2015-Modul-Standard umgewandelt. Falls wir eine Einstiegsdatei index.ts definieren, die auf weitere TS-Dateien auch in Unterordnern verweist, entsteht wegen fehlerhaften Imports leider kein gültiges ES2015-Modul (Stackoverflow). Das sorgt für viel schlechte Laune bei Entwicklern und wird in den entsprechenden TypeScript-Issues (Issue-13422, Issue-16577) von der Community reghaft diskutiert. Dank Rollup und dem zugehörigen TypeScript-Plugin können wir uns trotzdem leicht ein entsprechendes ES2015-Modul erzeugen.

Fazit

Der Einsatz von fremden Bibliotheken in eigenen Projekte kann sich als schwieriger erweisen, als es zuvor gedacht war. Insbesondere bei der Ausführung von Unit-Tests können weitere Hürden entstehen, falls die Ablaufumgebung von der tatsächlichen Umgebung abweicht.

Bei selbst entwickelten Bibliotheken müssen wir uns für ein passendes Modul-System entscheiden.

Ein erfolgreiches Kompilieren bedeutet in Typescript-Projekten nicht, dass es auch zur Laufzeit korrekt geladen werden kann. Der Einsatz eines passenden TypeScript-Starter-Projekts lohnt sich oftmals, um den Aufwand für eigene Konfigurationarbeit zu vermeiden. Angular und React bieten entsprechende Starter-Projekte und bei Github sind auch einige verfügbar: Typescript-Library-Starter, TypeScript-Node-Starter.

Kommentare sind geschlossen.