
Das Modul-Muster für Javascript sollte jeder aus dem effeff kennen. Es ist eines der vielseitigsten Methoden, den eigenen Source-Code in Projekten zu strukturieren. Dieses Pattern spielt eine wichtige Rolle in vielen leistungsfähgien Javascript-Frameworks und Konzepten wie NodeJS, RequireJS, CommonJS, AMD und ECMAScript.
Der oben verlinkte Artikel erklärt das Grundprinzip recht gut. Deshalb soll im Folgenden auch nicht mehr viel Theorie vermittelt werden, sondern dieses Modul-Pattern so beschrieben werden, wie wir es in weiteren Artikeln benötigen.
Trotzdem zu Beginn ganz kurz das Grundprinzip, anhand dessen auch gleichzeitig ein paar Konventionen festgelegt werden, die in den folgenden Beiträgen beibehalten werden.
var demoModul = (function () { var privVar = 0; function helper() { ... privVar = 333; ... } function _a(x, y){ ... helper(); ... return something; } function _b(x, y){ ... // do something ... } function _setPrivVar(val) { privVar = val; } return { a: function(x, y) { return _a(x, y); }, b: function() { b(); }, publicVar: "foo", // init: _init(), setPrivVar: function(val){ _setPrivVar(val); } }; }()); console.log('demoModul.js');
Im Gegensatz zu den Beispielen in der verlinkten Beschreibung wird im Return-Objekt nur eine Funktion mit einem Unterstrich versehen aufgerufen, die dann im Deklarationsteil der Funktion implementiert ist.
Außerdem erkennt man, wie private bzw. öffentliche Eigenschaften gehandhabt werden. Will ich den Wert der eigentlich privaten Variablen privVar ändern, diese also im Prinzip öffentlich zugänglich machen, muss ich eine Funktion setPrivVar(val) spendieren, die mir den Zugriff ermöglicht.
Angedeutet wird im Beispiel auch eine Möglichkeit, eine öffentliche Eigenschaft per Funktion festzulegen (init). Wenn _init() implementiert wäre, würde sie bei der Initialisierung des Moduls sofort ausgeführt. Dieses Verhalten ist in der Regel unerwünscht, könnte aber in dem einen oder anderen Fall durchaus nützlich sein.
Beispiel für Varianten des Modul-Patterns
In meinem Artikel zu Drush und Grunt hatte ich das Modul-Pattern schon eingesetzt und zwar im Skript grunt-helpers.js (ungefähr in der Mitte des Artikels). Dort hatte es die Form eines NodeJS-Moduls (bzw. CommonJS, das von NodeJS verwendet wird) und war Bestandteil eines Grunt-Skripts.
Man erkennt die CommonJS-Notation an der ersten Zeile der Modul-Definition:
module.exports = function (grunt) { ...
Innerhalb des Grunt-Skripts wird dann das Modul grunt-helpers aufgerufen in der folgenden Weise:
grunt.initConfig({ pkg : grunt.file.readJSON('package.json'), ... util: require('./grunt-helpers.js')(grunt), ... shell : { ...
Man sieht sehr schön, dass die Art und Weise der Implementierung und des Aufrufs beeinflusst wird von der Kombination der verschiedenen Frameworks (hier: NodeJS, Grunt), die eingesetzt werden. Es gibt für fast alle Varianten bestimmte Wrapper-Konstrukte, die im Netz ausführlich beschrieben werden.
Bei der Verwendung von Modulen aus fremden Frameworks sind dann oft Einschränkungen zu beachten. Die wichtigste ist die, dass fremde Module ihrerseits gegenseitig keine Abhängigkeiten enthalten sollten.
Import Mixins
Vielleicht habt ihr euch schon über die beiden leeren Klammern () am Ende des Modul-Patterns gewundert. Diese dienen dazu, verschiedene Frameworks innerhalb des Moduls verfügbar zu machen, wie z.B. jQuery oder Underscore.js. Das Verfahren soll hier nur kurz angedeutet werden.
var demoModule = (function ( $, _ ) { function _a(){ $("#Debug_").html("xyz"); } function _b(){ return _.max([3, 15, 27]); } return{ a: function() { _a(); }, b: ... }; })( jQuery, _ ); demoModule.a();
Konkretes Beispiel
Im folgenden stelle ich mein Modul util.js vor, das gegenüber dem oben erwähnten grunt-helpers.js zwei weitere nützliche Funktionen enthält, die in den folgenden Beiträgen insbesondere zum Thema Hybride Apps von Bedeutung sind.
var util = (function () { // define(function() { // needed for '_replaceAll' function escapeRegExp(string) { return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); } function _replaceAll(string, find, replace) { return string.replace(new RegExp(escapeRegExp(find), 'g'), replace); } // e.g.: var S = new Object(); // S.val='Berlin-Wedding'; // P=shift_word('-', S); // => P='Berlin', S.val='Wedding' function _shiftWord(Separator, S){ var Q = ''; var P = S.val.indexOf(Separator, S.val); var L = Separator.length; if (P==-1) { Q = S.val; S.val = ''; return Q; } if (P==0) { S.val = S.val.substr(L); return ''; } Q = S.val.substr(0,P); S.val = S.val.substr(P+L); return Q; } function _timestamp() { var now = new Date(); var date = [ now.getFullYear(), now.getMonth() + 1, now.getDate()]; var time = [ now.getHours(), now.getMinutes(), now.getSeconds() ]; for ( var i = 1; i < 3; i++ ) { if ( time[i] < 10 ) { time[i] = "0" + time[i]; } } for ( var i = 1; i < 3; i++ ) { if ( date[i] < 10 ) { date[i] = "0" + date[i]; } } return date.join("") + "-" + time.join(""); } // Are we running in native app or in a browser? function _runsInNativeApp() { var res = false; if (document.URL.indexOf("http://") === -1 && document.URL.indexOf("https://") === -1) { res = true; } return res; } // Are we running in Ripple-emulator? function _runsInRippleEmu() { if (_runsInNativeApp()) { return false; } return urlExists('http://localhost:4400/ripple/assets/ripple.css')===true; } function urlExists(url) { var http = new XMLHttpRequest(); http.open('HEAD', url, false); http.send(); // return http.status!=404; return http.status===200; } return { replaceAll: function(aPath, find, replace) { return _replaceAll(aPath, find, replace); }, shiftWord: function(Separator, S) { return _shiftWord(Separator, S); }, timestamp: function(){ return _timestamp(); }, runsInRippleEmu: function() { return _runsInRippleEmu(); }, runsInNativeApp: function() { return _runsInNativeApp(); } }; // }); }()); console.log('util.js');
Ausgeklammert habe ich übrigens am Anfang und am Ende des Skripts die Notation, wie sie für die Verwendung innerhalb des Frameworks requireJS benötigt wird. In einem der nächsten Artikel wird dieses Framework zur Organisation des Source-Codes in Javascript-Projekten vorgestellt, vor allem im Hinblick auf die folgenden Artikel zum Thema Hybride Apps.
TIPP: Am Ende des Skripts steht eine console.log('modulname')-Ausgabe. Ich mache das deshalb, weil in require.js die Skripte asynchron und nach Bedarf geladen werden. Sie werden dann leider normalerweise nicht automatisch in den Workspace des Chrome-Debuggers aufgenommen, was die Handhabung dann umständlich macht. Mit der console-Ausgabe am Ende allerdings wird das Skript in dem Moment im Debugger sichtbar, indem das Modul geladen wird. Es gibt komplexe Lösungen für das Problem. Mir reicht aber die Zeile am Ende des Skripts.
TIPP: Wer übrigens dann noch vermisst, dass man keinen Breakpoint setzen kann durch Klick auf die Zeilennummer, der sollte die rechte Maustaste versuchen. Dort gibt es dann die Möglichkeit trotzdem einen Breakpoint zu setzen.
Fazit
Vorgestellt wurde hier das für Javascript wichtige Modul-Pattern und zwar so, wie wir es für die Projekte in den folgenden Beiträgen benötigen.
Es handelt sich um eine von mehreren Möglichen Best Practice - Implementierungen. Eine weitere interessante Variante wird in diesem Artikel beschrieben.
Meine Methode erwies sich allerdings als sehr brauchbar, um bespielsweise Events innerhalb des Moduls verarbeiten zu können. Dies geht sehr schlecht mit der Objekt Literal Notation, wie sie auch in dem schon verlinkten Artikel (s. ganz oben) vorgestellt wird. Ob dies so problemlos mit der Variante von Paul Lunow funktioniert, kann ich jetzt nicht sagen, weil ich das nicht mehr untersucht habe. Grundsätzlich halte ich meine Variante aber für intuitiver.
Am Ende des Artikels habe ich noch ein komplettes nützliches Modul vorgestellt, welches uns in weiteren Beiträgen in diesem Weblog wiederbegegnen wird.