Effiziente Codeorganisation in JavaScript: require.js

Dilemma:

Codeorganisation in JavaScript geht mir schon seit je her etwas auf die Nüsse. Wegen der Natur von JavaScript muss man entweder alle Skripte von Anfang an laden (= viel JavaScript parsen bei Seitenaufbau) oder man verwendet XHR/JSONP (JSONP ist in diesem Falle einfach ein dynamisches Einfügen von script Tags), was wiederum aufwendig ist, da asynchron. Eine include/import Direktive wie in anderen Sprachen existiert nicht.

Lösungsmöglichkeiten:

Netterweise haben sich viele (hauptsächlich Framework-) Entwickler ebenfalls schon Gedanken über Codeorganisation und dieses Problem gemacht und verschiedene Ansätze, teils recht ähnliche Ansätze entwickelt :

  • ExtJS hat eine recht feste Ordnervorgabe, an der sich die Anwendung orientiert. Dependencies müssen aber in den Klassen angegeben sein
  • Qooxdoo erkennt Abhängigkeiten automatisch, dafür muss man allerdings das Generator Skript bei Änderungen ausführen (entspricht daher in etwa dem klassischen Kompilieren/Linken)
  • CommonJS spezifiziert einen Standard für Modularisierung, der recht verbreitet bei Serveranwendungen ist (z.b. NodeJS, CouchDB)
  • Und last but not least (es gibt natürlich noch mehr) hat Dojo ihren Modularisierungsmechanismus AMD (Asynchronous Module Definition) getauft und mit require.jseine Implementierung zur Verfügung gestellt

Ich fand require.js den schönsten Ansatz wenn man gerade kein großes Framework verwenden will. Einerseits ist es recht schlank, benötigt wenig Setup und reduziert vor allem die Anzahl der globalen Browser-Objekte. Letzteres ist vor allem wichtig, wenn man Gefahr läuft zwei Versionen der gleichen Bibliothek verwenden zu müssen (weil man z.B. JQuery verwendet und ein Tool in die Applikation einbindet, das seine eigene JQuery Version mitliefert). Ausserdem ist AMD keine Insellösung, sondern basiert in weiten Teilen auch auf CommonJS.

Require.js – How To:

Die Einbindung von require.js ist recht einfach, man bindet einfach require.js ein und gibt optional an, welches Skript die Startlogik für die Applikation beinhaltet:

<script data-main="include/app.js" src="scripts/require.js"></script>

Jetzt wird beim Start direkt die Datei include/app.js eingebunden und ausgeführt.
(Man kann sehr einfach programmatisch auch noch extra Regeln für die Auflösung von Skripten, Grundpfade, etc. angeben, da verweise ich mal auf die gute Dokumentation)
AMD definiert jetzt zwei wichtige Funktionen: require() und define():

  • require() fordert require.js auf, erst die Module zu laden und mir danach bereitzustellen. Das kann z.B. so aussehen:

    require(["mein/modul1","mein/modul2"], function(modul1,modul2) {
    //hier kann jetzt mit modul1 und modul2 aus dem 'mein' Ordner (= Namespace) gearbeitet werden
        modul1.process(document.body);
    });

    So könnte jetzt unsere app.js aussehen und schonmal alle Module laden, die sie direkt benötigt. Nützlich ist das auch wenn man nur in speziellen Fällen eine Abhängigkeit braucht und diese nur bei Bedarf on-the-fly nachladen will. Ansonsten findet man sich häufiger dabei define() zu Verwenden:
  • define() definiert ein neues Modul und deren Abhängigkeiten. Die mein/modul1.js könnte jetzt so aussehen und z.B. JQuery und ein weiteres Modul benötigen (Achtung: $ ist nur eine Variable die auf JQuery zeigt, nichts besonderes.) :
     define(['lib/jquery','mein/modul3'],function($,modul3) {
        // Schnittstelle für modul1 wird zurückgegeben, hier z.B. ein Schnittstellenobjekt
        return {
           funktion1: function(el) { return modul3.process($(el)); } //tue irgendetwas mit dem element
        }   
    });

Für einfache Projekte braucht man oft auch gar nicht mehr – das Problem mit der Codeorganisation ist gelöst! Allerdings lohnt ein Blick in die Dokumentation, die ist wirklich sehr gut und bietet Anreize und Lösungen für viele Probleme, die im Alltag auftreten könnten.
Ein Nachteil an require.js ist natürlich, dass man sein Programmiermodell ein wenig Umstellen muss. Da der Aufwand jedoch nicht wirklich groß, der Nutzen jedoch schon ist, kann man das meiner Meinung nach verkraften.