|
|
| Zeile 150: |
Zeile 150: |
|
| |
|
| {{TBD}} | | {{TBD}} |
| ===Globale Funktionen als Methoden===
| |
|
| |
| Grundsätzlich sollten so wenige globale Funktionen wie möglich in einen JavaScript-Programm definiert werden,
| |
| um Namenskonflikte mit JavaScript-Modulen und -Bibliotheken anderer Autoren zu vermeiden.
| |
| In der Hello-World-Anwendung gibt es drei globale Funktionen: „<code>sayHello</code>“, „<code>sayHelloOnEnter</code>“ und „<code>init</code>“.
| |
| Die ersten beiden werden in [[Methode]]n umgewandelt. Das heißt, es wird ein Objekt „<code>greet</code>“ erstellt, das diese beiden Methoden
| |
| enthält. Die dritte Funktion „<code>init</code>“ wird durch eine anonyme Funktion ersetzt, die sofort ausgeführt wird.
| |
|
| |
| Erstellen sie die Datei „<code>js/app1/greet.js</code>“ und fügen Sie ein weiteres Script-Element in die Datei „<code>index1.html</code>“
| |
| ein ('''vor''' dem Skript-Element, das „<code>main1.js</code>“ lädt):
| |
|
| |
| <source lang="html5">
| |
| ...
| |
| <script type = "text/javascript" src = "js/app1/greet.js"></script>
| |
| <script type = "text/javascript" src = "js/main1.js" ></script>
| |
| </body>
| |
| </source>
| |
|
| |
| Fügen Sie in die Datei „<code>greet.js</code>“ ein Objekt ein, dass die beiden Methoden
| |
| „<code>sayHello</code>“ und „<code>sayHelloOnEnter</code>“ enthält.
| |
|
| |
| <source lang="javascript">
| |
| /**
| |
| * @namespace greet
| |
| * Contains methods to say hello to the user of the web app.
| |
| */
| |
| var
| |
| greet =
| |
| {
| |
| /**
| |
| * Welcomes the user of the web app1 by displaying a welcome message
| |
| * that includes his name. The name is fetched from a text input field.
| |
| */
| |
| sayHello:
| |
| function()
| |
| {
| |
| document.getElementById('heading_hello').innerHTML =
| |
| 'Hello, ' + document.getElementById('input_name').value + '!';
| |
| document.getElementById('section_form').classList.add('hidden');
| |
| document.getElementById('section_hello').classList.remove('hidden');
| |
| },
| |
|
| |
| /**
| |
| * An keyboard event observer. It tests whether the enter key has been pressed.
| |
| * If so, the greet method is activated. Default reactions of the browser are
| |
| * disabled.
| |
| * @param {KeyboardEvent} p_event - A standard JavaScript keyboard event object
| |
| * (https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent)
| |
| */
| |
| sayHelloOnEnter:
| |
| function(p_event)
| |
| {
| |
| if ((p_event.code === 'Enter' || p_event.keyCode === 13) &&
| |
| document.activeElement === document.getElementById('input_name')
| |
| )
| |
| {
| |
| p_event.preventDefault();
| |
| p_event.stopPropagation();
| |
| greet.sayHello();
| |
| }
| |
| }
| |
| };
| |
| </source>
| |
|
| |
| In der Datei „<code>main1.js</code>“ werden dagegen alle globalen Funktionen entfernt.
| |
| Dies gilt auch für die Definition des Eventlisteners am Ende der Datei, der dafür sorgt, dass die
| |
| Funktion „<code>init</code>“ ausgeführt wird, sobald die HTML-Seite sowie alle Unterseiten vollständig geladen wurden.
| |
|
| |
| Die Funktion „<code>init</code>“ wird durch eine anonyme Funktion ersetzt, die '''sofort''' nach ihrer Definition auch ausgeführt wird.
| |
| Da die Datei „<code>main1.js</code>“ als Letztes geladen wird, sind alle Objekte, auf die
| |
| während der Initialisierung zugegriffen werden muss, bereits vorhanden.
| |
|
| |
| <source lang="javascript">
| |
| // Initialize the app.
| |
| (function()
| |
| {
| |
| document.getElementById('button_submit')
| |
| .addEventListener('click', greet.sayHello);
| |
|
| |
| window.addEventListener('keydown', greet.sayHelloOnEnter);
| |
| }());
| |
| </source>
| |
|
| |
| Testen Sie Ihre Anwendung und speichern Sie sie zu guter Letzt im Repository.
| |
|
| |
| ==App2==
| |
|
| |
| Ein wesentliches [[Programmierprinzip]] ist das Prinzip der „Wiederverwendbarkeit“.
| |
| Man sollte Code so schreiben, dass man ihn möglichst häufig wiederverwenden kann.
| |
| Jede Wiederverwendung bereits bestehenden Codes bedeutet Arbeitsersparnis.
| |
| Man kann mit einem einfachen Test überprüfen, ob man Code erstellt hat, der
| |
| sogar innerhalb desselben Projektes wiederverwendbar ist. Man baut die neu
| |
| erstellte Anwendung einfach zweimal in ein HTML-Dokument ein. Wenn beide
| |
| „Instanzen“ der Anwendung unabhängig voneinander funktionieren, hat man
| |
| alles richtig gemacht.
| |
|
| |
| Zurzeit geht das bei unserer Anwendung noch nicht. Es gibt nämlich nur
| |
| ein Objekt „<code>greet</code>“, das nicht für mehrere Anwendungen parallel eingesetzt werden kann.
| |
| Wir müssen also bei Bedarf neue <code>greet</code>-Objekte erzeugen können.
| |
| Das geschieht in JavaScript mit Hilfe von sogenannten [[Konstruktor]]funktionen.
| |
|
| |
| Erstellen Sie in Ihrem WebStorm-Projekt Kopien Ihre Anwendungsdateien:
| |
|
| |
| * <code>index1.html</code> → <code>index2.html</code>
| |
| * <code>js/main1.js</code> → <code>js/main2.js</code>
| |
| * <code>js/app1/greet.js</code> → <code>js/app2/greet.js</code>
| |
|
| |
| In der Datei „<code>index2.html</code>“ müssen Sie dafür sorgen, dass
| |
| die neuen JavaScript-Dateien anstelle der alten geladen werden.
| |
| Und passen Sie den Titel im HTML-Header an.
| |
|
| |
| Das Objekt „<code>greet</code>“ der Datei „<code>js/app2/greet.js</code>“
| |
| wird durch der folgende Konstruktorfunktion „<code>Greet</code>“ ersetzt:
| |
|
| |
| <source lang="javascript">
| |
| /**
| |
| * @class
| |
| * @classdesc Contains methods to say hello to the user of the web app.
| |
| */
| |
| function Greet()
| |
| {
| |
| var l_this = this; // this-hack: l_this contains the new object
| |
|
| |
| /**
| |
| * Welcomes the user of the web app1 by displaying a welcome message
| |
| * that includes his name. The name is fetched from a text input field.
| |
| */
| |
| l_this.sayHello = // this.sayHello = ...
| |
| function()
| |
| {
| |
| document.getElementById('heading_hello').innerHTML =
| |
| 'Hello, ' + document.getElementById("input_name").value + '!';
| |
|
| |
| document.getElementById('section_form').classList.add('hidden');
| |
| document.getElementById('section_hello').classList.remove('hidden');
| |
| };
| |
|
| |
| /**
| |
| * An keyboard event observer. It tests whether the enter key has been pressed.
| |
| * If so, the greet method is activated. Default reactions of the browser are
| |
| * disabled.
| |
| * @param {KeyboardEvent} p_event - A standard JavaScript keyboard event object
| |
| * (https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent)
| |
| */
| |
| l_this.sayHelloOnEnter = // this.sayHelloOnEnter = ...
| |
| function(p_event)
| |
| {
| |
| if ((p_event.code === 'Enter' || p_event.keyCode === 13) &&
| |
| document.activeElement === document.getElementById('input_name')
| |
| )
| |
| {
| |
| p_event.preventDefault();
| |
| p_event.stopPropagation();
| |
| l_this.sayHello(); // NOT this.sayHello();
| |
| }
| |
| }
| |
| }
| |
| </source>
| |
|
| |
| In JavaScript kann jede Funktion auch als Konstruktor, {{dh}}
| |
| zum Erzeugen von neuen Objekte verwendet werden, indem man das Schlüsselwort
| |
| „<code>new</code>“ voranstellt:
| |
|
| |
| <source lang="javascript">
| |
| var l_greet = new Greet();
| |
| </source>
| |
|
| |
| In einer Konstruktorfunktion wird üblicherweise kein <code>return</code>-Befehl eingefügt,
| |
| da sie sich wie eine [[Prozedur (Informatik)|Prozedur]] und nicht wie
| |
| eine [[Funktion (Informatik)|Funktion]] verhält. Sie erstellt – als [[Seiteneffekt]] – ein neues Objekt und initialisiert es.
| |
| Allerdings wird dieses Objekt nirgendwo gespeichert, sondern – automatisch – als Ergebnis
| |
| des Befehls <code>new Greet()</code> zurückgeliefert. Der Programmierer muss anschließend selbst dafür sorgen,
| |
| dass das neu erzeugte Objekt
| |
| direkt im Anschluss an dessen Erzeugung irgendwo gespeichert wird,
| |
| in einer [[Variable]]n, in einem [[Array]], in einem [[Objekt]] oder in einem [[Funktionsparameter]].
| |
| Im obigen Beispiel wird es in der Variablen „<code>l_greet</code>“ abgelegt.
| |
|
| |
| Im Funktionsrumpf selbst kann über die Spezialvariable „<code>this</code>“ auf das neu erstellte Objekt
| |
| zugegriffen werden. Dem neu erstellten Objekt werden mittels
| |
| „<code>this.sayHello = ...</code>“ und „<code>this.sayHelloOnEnter = ...</code>“
| |
| etc. [[Attribut (Objekt)|Attribute]] zugewiesen, die Funktionen (welche [[Methode]]n genannt werden) als Werte enthalten.
| |
| Dem neu erstellten Objekt werden dieselben Methoden zugewiesen, wie dem Objekt „<code>greet</code>“ der Anwendung „App1“.
| |
| Daher kann das mittels Konstruktorfunktion erstellte Objekt „<code>l_greet</code>“ auch genauso verwendet werden,
| |
| wie das globale Objekt „<code>greet</code>“ der Anwendung „App1“.
| |
|
| |
| Beachten Sie bitte, dass in der Definition der Konstruktorfunktion der so genannte This-Hack zum Einsatz kommt.
| |
| Das Objekt <code>this</code> wird in einer lokalen Variablen <code>l_this</code> gespeichert.
| |
| Und anschließend wird diese lokale Variable anstelle von <code>this</code> benutzt.
| |
| Für die Definition der Attribute „<code>sayHello</code>“ und „<code>sayHelloOnEnter</code>“ macht es keinen Unterschied,
| |
| ob man „<code>this.sayHello = ...</code>“ oder „<code>l_this.sayHello = ...</code>“ schreibt.
| |
|
| |
| Allerdings wird auch innerhalb des Methodenrumpfes von „<code>sayHelloOnEnter</code>“ auf das neu erstellt
| |
| Objekt zugegriffen: <code>l_this.sayHello();</code>. Und hier könnte nun nicht <code>this.sayHello();</code>
| |
| verwendet werden. Der Grund ist, dass der Methodenrumpf erst ausgeführt wird, wen die Methode „<code>sayHelloOnEnter</code>“
| |
| zu irgendeinem anderen Zeitpunkt von irgendeinem anderen Objekt aufgerufen wird. Und <code>this</code> bezeichnet
| |
| bei „normalen“ Funktionsaufrufen ({{dh}} bei Aufrufen ohne das Schlüsselwort „<code>new</code>“)
| |
| '''immer''' den Aufrufer der Methode, und nicht das Objekt, das die Methode beinhaltet. Aber im Aufrufer der
| |
| Methode „<code>sayHelloOnEnter</code>“ ist die Methode „<code>sayHello</code>“ {{iAllg}} gar nicht definiert oder falls doch,
| |
| dann meist mit einer vollkommen anderen Funktionalität. Die Spezialvariable „<code>this</code>“ verhält sich hier vollkommen anders
| |
| als sämtliche andere Variablen und Parameter. (Fachsprachlich: In JavaScript ist die Variable „<code>this</code>“
| |
| [[Variable#Bindung|dynamisch gebunden]], alle übrigen Variablen sind [[Variable#Bindung|statisch gebunden]].)
| |
| Daher kopiert man den Wert von „<code>this</code>“ in eine lokale Variable „<code>l_this</code>“,
| |
| auf die die Methode „<code>sayHelloOnEnter</code>“ immer Zugriff hat, unabhängig davon, wer sie aufruft.
| |
|
| |
| In der Anwendung „App1“ wurde der This-Hack nicht benötigt, da die Methode „<code>sayHelloOnEnter</code>“
| |
| einfach die globale Variable Methode „<code>greet</code>“ verwendet hat, um auf die Methode „<code>sayHello</code>“
| |
| zuzugreifen (<code>greet.sayHello();</code>). Aber ein globales Greet-Objekt gibt es nun nicht mehr. Es gibt nur noch einen
| |
| globale Konstruktorfunktion „<code>Greet</code>“, mit der beliebig viele Greet-Objekte erzeugt werden können.
| |
|
| |
| '''Tipp''': Definieren Sie innerhalb einer Konstruktorfunktion immer eine lokale Variable „<code>var l_this = this</code>“ und verwenden Sie innerhalb
| |
| des Konstruktors nur „<code>l_this</code>“ und nie „<code>this</code>“. (Sie können natürlich auch einen anderen Alternativnamen für
| |
| „<code>this</code>“ verwenden. Übliche Bezeichnungen sind „<code>that</code>“, „<code>self</code>“ oder „<code>me</code>“.)
| |
|
| |
| '''Anmerkung''': Eigentlich ist der This-Hack unsauber. Besser wäre es, die Methode „<code>bind</code>“ zu verwenden
| |
| (siehe [https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Function/bind MDN] oder [http://stackoverflow.com/questions/2236747/use-of-the-javascript-bind-method Stackoverflow]):
| |
|
| |
| <source lang="javascript">
| |
| this.sayHelloOnEnter =
| |
| function(p_event)
| |
| {
| |
| ...
| |
| this.sayHello();
| |
| ...
| |
| }.bind(this);
| |
| </source>
| |
| Aber für JavaScript-Anfänger ist die Verwendung des This-Hacks deutlich einfacher.
| |
|
| |
| Fügen Sie als nächstes die zuvor erwähnte Zeile
| |
|
| |
| <source lang="javascript">
| |
| var l_greet = new Greet();
| |
| </source>
| |
| als erste Zeile '''in''' Ihre Initialisierungsfunktion (Datei „<code>main2.js</code>“)
| |
| ein und ersetzen Sie in derselben Funktion die beiden Zugriffe auf das globale Objekt
| |
| „<code>greet</code>“ durch Zugriffe auf das lokale Objekt „<code>l_greet</code>“.
| |
|
| |
| Danach sollte Ihre Anwendung (Datei „<code>index2.html</code>“) wieder wie gewohnt laufen.
| |
|
| |
| ==App3==
| |
|
| |
| Leider bringt uns hinsichtlich der Wiederverwendbarkeit die „Verbesserung“, die wir in App2 gegenüber App1
| |
| vorgenommen haben, gar nichts. Wir können jetzt zwar beliebig viele <code>greet</code>-Objekte
| |
| mit Hilfe von „<code>new Greet()</code>“ erstellen.
| |
| Aber wir haben nichts davon, da alle diese Objekte dieselben HTML-Elemente
| |
| im HTML-Dokument lesen und ändern. Das heißt, wir müssen die Konstanten und
| |
| am Besten auch gleich noch die Zugriffe auf globale Variablen aus der Konstruktorfunktion
| |
| „<code>greet</code>“ verbannen.
| |
|
| |
| Erstellen Sie in Ihrem WebStorm-Projekt folgende Kopien Ihre Anwendungsdateien:
| |
|
| |
| * <code>index2.html</code> → <code>index3.html</code>
| |
| * <code>js/main2.js</code> → <code>js/main3.js</code>
| |
| * <code>js/app2/greet.js</code> → <code>js/app3/greet.js</code>
| |
|
| |
| In der Datei „<code>index3.html</code>“ müssen Sie wiederum dafür sorgen, dass
| |
| die neuen JavaScript-Dateien anstelle der alten geladen werden.
| |
| Und passen Sie auch wieder den Titel im HTML-Header an.
| |
|
| |
| Ersetzen Sie nun die Konstruktorfunktion der Datei „<code>js/app3/greet.js</code>“
| |
| durch folgende Konstruktorfunktion:
| |
|
| |
| <source lang="javascript">
| |
| /**
| |
| * @class
| |
| * @classdesc Contains methods to say hello to the user of the web app.
| |
| *
| |
| * @param p_document {object}
| |
| * A DOM object (usually the global object <code>document</code>)
| |
| * @param p_section_form {string}
| |
| * The ID of the HTML element that contains the form section.
| |
| * @param p_input_name string}
| |
| * The ID of the text field via which the user passes her/his name.
| |
| * @param p_section_hello {string}
| |
| * The ID of the HTML element that contains the welcome section.
| |
| * @param p_output_hello {string}
| |
| * The ID of the HTML element into which the greeting is to be put.
| |
| * @param p_hello {string}
| |
| * The hello test to be displayed.
| |
| */
| |
| function Greet(p_document,
| |
| p_section_form, p_input_name,
| |
| p_section_hello, p_output_hello,
| |
| p_hello
| |
| )
| |
| {
| |
| var l_this = this; // this-hack
| |
|
| |
| /**
| |
| * Welcomes the user of the web app1 by displaying a welcome message
| |
| * that includes his name. The name is fetched from a text input field.
| |
| */
| |
| l_this.sayHello =
| |
| function()
| |
| {
| |
| p_document.getElementById(p_output_hello).innerHTML =
| |
| p_hello + ', ' + p_document.getElementById(p_input_name).value + '!';
| |
|
| |
| p_document.getElementById(p_section_form).classList.add('hidden');
| |
| p_document.getElementById(p_section_hello).classList.remove('hidden');
| |
| };
| |
|
| |
| /**
| |
| * An keyboard event observer. It tests whether the enter key has been pressed.
| |
| * If so, the greet method is activated. Default reactions of the browser are
| |
| * disabled.
| |
| * @param {KeyboardEvent} p_event - A standard JavaScript keyboard event object
| |
| * (https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent)
| |
| */
| |
| l_this.sayHelloOnEnter =
| |
| function(p_event)
| |
| {
| |
| if ((p_event.code === 'Enter' || p_event.keyCode === 13) &&
| |
| p_document.activeElement === p_document.getElementById(p_input_name)
| |
| )
| |
| {
| |
| p_event.preventDefault();
| |
| p_event.stopPropagation();
| |
| l_this.sayHello(); // NOT this.sayHello();
| |
| }
| |
| }
| |
| }
| |
| </source>
| |
|
| |
| Dieser Konstruktor enthält diverse Konstanten nicht mehr, wie {{zB}} <code>'input_name'</code>. Anstelle dessen gibt es diverse Parameter,
| |
| in denen diese Werte dem Konstruktor übergeben werden können, wie {{zB}} <code>p_input_name</code>. Der Zugriff auf das globale Objekt
| |
| „<code>document</code>“ wurde auch gleich noch durch einen Zugriff auf einen lokalen Parameter „<code>p_document</code>“ ersetzt.
| |
|
| |
| Damit ist es jetzt möglich, <code>greet</code>-Objekte für ganz unterschiedliche HTML-Elemente zu definieren.
| |
|
| |
| Ersetzen Sie zunächst die Initialisierungsmethode in der Datei „<code>main3.js</code>“
| |
| durch folgenden Code:
| |
|
| |
| <source lang="javascript">
| |
| (function(p_window, Greet)
| |
| {
| |
| var l_document = p_window.document, // The window object contains the document object.
| |
| l_greet = new Greet(l_document,
| |
| 'section_form', 'input_name',
| |
| 'section_hello', 'heading_hello',
| |
| 'Hello'
| |
| );
| |
|
| |
| l_document.getElementById('button_submit')
| |
| .addEventListener('click', l_greet.sayHello);
| |
|
| |
| p_window.addEventListener('keydown', l_greet.sayHelloOnEnter);
| |
| }(window, Greet));
| |
| </source>
| |
|
| |
| In dieser Datei werden noch gibt es noch zwei globale Variablen, die Browser-Variable „<code>window</code>“
| |
| sowie die von uns definierte Konstruktorfunktion „<code>Greet</code>“. Diese beiden globalen Größen werden der
| |
| anonymen Funktion als Argumente übergeben. Innerhalb des Rumpfes der Initialisierungsfunktion wird dann nicht
| |
| mehr auf globale Größen zugegriffen, sondern nur noch auch Funktionsparameter. Dies ist ein übliches
| |
| Vorgehen, um explizit klar zu machen, auf welche globalen Größen zugegriffen wird.
| |
|
| |
| Dem Konstruktor „<code>Greet</code>“ werden jetzt alle Information, die er benötigt,
| |
| explizit übergeben. Dies ist zum einen das HTML-Dokument, das mit Hilfe der von ihm erstellten
| |
| Methoden manipuliert werden soll, dann eine Reihe von ID-Attribut-Namen, die diejenigen
| |
| HTML-Elemente bezeichnen, die die Greet-Methoden lesen oder ändern sollen, sowie
| |
| der String „<code>'Hello'</code>“ der zur Begrüßung des Benutzers verwendet werden soll.
| |
| Alternativ könnte er auch mit „<code>Hallo</code>“ oder „<code>Howdy</code>“ begrüßt werden.
| |
|
| |
| Nun müsste Ihre Anwendung „<code>index3.html</code>“ wieder laufen. Wenn Sie es denn tut, vergessen Sie nicht, sie
| |
| im Repository zu speichern.
| |
|
| |
| Anschließend sollten Sie in Ihrem WebStorm-Projekt folgende Kopien erstellen:
| |
|
| |
| * <code>index3.html</code> → <code>index3b.html</code>
| |
| * <code>js/main3.js</code> → <code>js/main3b.js</code>
| |
|
| |
| Die Datei „<code>index3b.html</code>“ soll abermals die JavaScript-Datei „<code>js/app3/greet.js</code>“
| |
| verwenden, dafür aber „<code>js/main3b.js</code>“ an Stelle von „<code>js/main3.js</code>“ laden.
| |
|
| |
| Fügen Sie in der Datei „<code>index3b.html</code>“ in alle ID-Attribut-Bezeichner eine <code>1</code> ein:
| |
| <code>section_form</code> → <code>section1_form</code> etc.
| |
|
| |
| Schließen Sie dann die beiden Sections „<code>section1_form</code>“ und „<code>section1_hello</code>“
| |
| in ein <code>div</code>-Element ein und duplizieren Sie diese <code>div</code>-Elemente (natürlich samt Inhalt) anschließend.
| |
| Sie werden feststellen, dass WebStorm eine Reihe von Fehlern der Art „<code>Duplicate id reference</code>“ meldet.
| |
| Das ist nicht weiter verwunderlich, da Sie ja insbesondere alle ID-Attribut-Bezeichner dupliziert haben.
| |
| Sie müssen daher im zweiten <code>div</code>-Element alle Einser in den ID-Attribut-Bezeichnern durch Zweier ersetzen.
| |
|
| |
| Nun können Sie noch versuchen, die beiden <code>div</code>-Elemente mittels CSS nebeneinander zu legen. Für den
| |
| angestrebten Test, ob Sie die Hello-World-App in einem HTML-Dokument zweimal starten können, ist das allerdings irrelevant.
| |
|
| |
| Passen Sie nun Ihre Datei „<code>main3b.js</code>“ an die neuen Gegebenheiten an:
| |
| Erzeugen Sie zwei Greet-Objekte „<code>greet1</code>“ und „<code>greet2</code>“ (für jedes der beiden neuen <code>div</code>-Elemente eines),
| |
| fügen Sie passende <code>click</code>-Event-Listener für die beiden Buttons „<code>button1_submit</code>“ sowie
| |
| „<code>button2_submit</code>“ hinzu und sorgen Sie zu guter Letzt dafür, dass beide Methoden „<code>l_greet1.sayHelloOnEnter</code>“
| |
| und „<code>l_greet2.sayHelloOnEnter</code>“ bei jedem Tastendruck aufgerufen werden.
| |
|
| |
| Wenn Sie alles richtig gemacht haben, sollten jetzt in Ihrer Web-Anwendung „<code>index3b.html</code>“
| |
| zwei Hello-World-Anwendungen parallel laufen: [https://glossar.hs-augsburg.de/beispiel/tutorium/es5/hello_world/WK_HelloWorld04/web/index3b.html <code>index3b.html</code>]
| |
|
| |
| ==App4==
| |
| Hinsichtlich der Wiederverwendbarkeit der Module ist die Version 3 der Web-Anwendung schon ganz gut.
| |
| Allerdings gibt es noch ein paar Unsauberkeiten, die beseitigt werden sollten:
| |
|
| |
| # In den Initialisierungsdateien „<code>main3.js</code>“ und „<code>main3b.js</code>“ sind immer noch konstante Werte enthalten.
| |
| # Die Anzahl der Parameter der Konstruktorfunktion ist zu groß.
| |
| # In den Initialisierungsdateien ist immer noch zu viel Anwendungscode enthalten.
| |
| # Die potentielle Zahl der globalen Größen ist noch zu hoch.
| |
|
| |
| Diese Probleme werden folgendermaßen gelöst:
| |
|
| |
| # Die Konstanten werden in eine [[JSON]]-Datei ausgelagert, die im ersten Schritt von der jeweiligen Initialisierungsdatei eingelesen wird.
| |
| # Der Konstruktorfunktion wird das gesamte JSON-Objekt mit allen Initialisierungswerten in einem einzigen Parameter übergeben.
| |
| # Es wird eine anwendungsspezifische Initialisierungsdatei „<code>hello.js</code>“ erstellt. Die Initialisierungsfunktion in der Datei „<code>main4.js</code>“ hat nur noch die Aufgabe, die JSON-Datei zu lesen und die darin enthalten Informationen anschließend an die Methode „<code>hello.init</code>“ weiterzuleiten.
| |
| # Es wird '''ein''' globales Objekt namens „<code>hello</code>“ erstellt, das die gesamte Web-Anwendung enthält.
| |
|
| |
| Erstellen Sie in Ihrem WebStorm-Projekt Kopien Ihrer Anwendungsdateien:
| |
|
| |
| * <code>index3b.html</code> → <code>index4.html</code>
| |
| * <code>js/main3b.js</code> → <code>js/main4.js</code>
| |
| * <code>js/app3/greet.js</code> → <code>js/app4/greet.js</code>
| |
|
| |
| Im Folgenden wird die HTML-Datei „<code>index4.html</code>“
| |
| als HTML-[[Template]] verwendet, {{dh}} als Strukturrahmen. '''Sämtliche''' Texte,
| |
| die in diesem Dokument enthalten sind, werden dynamisch mittels JavaScript eingefügt.
| |
|
| |
| In der Datei „<code>index4.html</code>“ müssen Sie also nicht nur dafür sorgen, dass
| |
| die neuen JavaScript-Dateien anstelle der alten geladen werden und dass der Titel angepasst wird.
| |
| Sie müssen auch sämtliche Text-Elemente mit eindeutigen ID-Attributen versehen.
| |
|
| |
| Die HTML-Datei der Musterlösung sieht folgendermaßen aus. Sie können auch Ihre
| |
| eigene Datei verwenden, müssen dann aber die JSON-Datei, die anschließend erstellt wird,
| |
| entsprechend anpassen.
| |
|
| |
| <source lang="javascript">
| |
| <!DOCTYPE html>
| |
| <html lang="en">
| |
|
| |
| <head>
| |
| <meta charset = "UTF-8">
| |
| <meta name = "viewport"
| |
| content = "width=device-width, initial-scale=1.0, user-scalable=yes"
| |
| >
| |
|
| |
| <title>WK_HelloWorld04 (4)</title>
| |
|
| |
| <link rel = "stylesheet" type = "text/css" href = "css/main.css"/>
| |
| </head>
| |
|
| |
| <body>
| |
| <div class="left">
| |
| <section id="section1_form">
| |
| <h1 id="heading1_form"></h1>
| |
| <form>
| |
| <div>
| |
| <label id="label1_name" for="input1_name"></label>
| |
| <input id="input1_name" type="text" autofocus="autofocus"/>
| |
| </div>
| |
| <div>
| |
| <button id="button1_reset" type="reset"></button>
| |
| <button id="button1_submit" type="button"></button>
| |
| </div>
| |
| </form>
| |
| </section>
| |
| <section id="section1_hello" class="hidden">
| |
| <h1 id="heading1_hello"></h1>
| |
| <p id="paragraph1_hello"></p>
| |
| </section>
| |
| </div>
| |
|
| |
| <div class="right">
| |
| <section id="section2_form">
| |
| <h1 id="heading2_form"></h1>
| |
| <form>
| |
| <div>
| |
| <label id="label2_name" for="input2_name"></label>
| |
| <input id="input2_name" type="text" autofocus="autofocus"/>
| |
| </div>
| |
| <div>
| |
| <button id="button2_reset" type="reset"></button>
| |
| <button id="button2_submit" type="button"></button>
| |
| </div>
| |
| </form>
| |
| </section>
| |
| <section id="section2_hello" class="hidden">
| |
| <h1 id="heading2_hello"></h1>
| |
| <p id="paragraph2_hello"></p>
| |
| </section>
| |
| </div>
| |
|
| |
| <script type = "text/javascript" src = "js/lib/wk/ajax.js"></script>
| |
| <script type = "text/javascript" src = "js/app4/greet.js"></script>
| |
| <script type = "text/javascript" src = "js/app4/hello.js"></script>
| |
| <script type = "text/javascript" src = "js/main4.js"></script>
| |
| </body>
| |
|
| |
| </html>
| |
| </source>
| |
|
| |
| ===Modularisierung===
| |
|
| |
| Um möglichst wenige globale Größen definieren zu müssen, legt man für eine
| |
| Anwendung oder eine Bibliothek typischerweise eine einzige globale Variable an – Namensraum-Variable genannt –, die ein Objekt enthält.
| |
| In diesem Objekt wird die gesamte Anwendung (Daten, Klassen, Funktionen etc.) gespeichert.
| |
|
| |
| Für die Hello-World-Anwendung wird die globale Namensraum-Variable „<code>hello</code>“ verwendet.
| |
| In jeder Datei, in der ein Teilaspekt der Anwendung definiert wird, kommt folgender Rahmencode zum Einsatz:
| |
|
| |
| <source lang="javascript">
| |
| /** @namespace hello */
| |
| var hello =
| |
| (function(p_hello)
| |
| { "use strict";
| |
|
| |
| // anwendungsspezifischer Code
| |
|
| |
| return p_hello;
| |
| }(hello ? hello : {}));
| |
| </source>
| |
|
| |
| In der Variablen „<code>hello</code>“ wird das Ergebnis eines Aufrufs einer anonymen Funktion gespeichert.
| |
| Dieser Funktion wird als Argument der Inhalt der Variablen „<code>hello</code>“ oder, falls dieses noch nicht
| |
| existiert, ein leeres Objekt als Argument übergeben (<code>hello ? hello : {}</code>). Die anonyme Funktion
| |
| fügt irgendwelche Daten, Klassen oder Funktionen in dieses Objekt ein und gibt es dann als Ergebnis zurück.
| |
|
| |
| Dies hat den Effekt, dass beim ersten Aufruf einer derartigen Funktion eine neues Objekt erstellt und
| |
| in „<code>hello</code>“ gespeichert wird, bei jedem weiteren Aufruf eines derartigen Codes wird
| |
| das bestehende <code>hello</code>-Objekt einfach um weitere Inhalte ergänzt. Wenn man die Definition
| |
| eines Namensraums über mehrere Dateien verteilt – die Forderung „Jedes Modul erledigt nach Möglichkeit
| |
| nur eine einzige Aufgabe.“ erzwingt diese Vorgehensweise geradezu –,
| |
| ist es egal in welcher Reihenfolge diese Dateien geladen werden. Sie können daher auch [[asynchron]],
| |
| {{dh}} parallel geladen werden. Wichtig ist nur, dass eine Datei nicht die Inhalte einer anderen Datei überschreiben darf.
| |
|
| |
| Die Datei „<code>js/app4/greet.js</code>“ sieht nun folgendermaßen aus:
| |
|
| |
| <source lang="javascript">
| |
| /** @namespace hello */
| |
| var hello =
| |
| (function(p_hello)
| |
| { "use strict";
| |
|
| |
| /**
| |
| * @class hello.Greet
| |
| * @classdesc Contains methods to say hello to the user of the web app.
| |
| *
| |
| * @param p_document A DOM object (usually the global object <code>document</code>)
| |
| * @param p_init A JSON object containing all init data.
| |
| */
| |
| p_hello.Greet =
| |
| function(p_document, p_init)
| |
| {
| |
| var l_this = this; // this-hack
| |
|
| |
| /**
| |
| * Welcomes the user of the web app1 by displaying a welcome message
| |
| * that includes his name. The name is fetched from a text input field.
| |
| */
| |
| l_this.sayHello =
| |
| function()
| |
| {
| |
| var l_init_elements = p_init.HTMLElements,
| |
| l_init_text = p_init.text,
| |
| l_init_hidden = p_init.css.hidden,
| |
| l_name = p_document.getElementById(l_init_elements.inputName).value;
| |
|
| |
| p_document.getElementById(l_init_elements.welcome).innerHTML
| |
| = l_init_text.welcome;
| |
| p_document.getElementById(l_init_elements.hello).innerHTML
| |
| = l_init_text.hello.replace('$1', l_name);
| |
|
| |
| p_document.getElementById(l_init_elements.sectionHello)
| |
| .classList.remove(l_init_hidden);
| |
| p_document.getElementById(l_init_elements.sectionForm)
| |
| .classList.add(l_init_hidden);
| |
| };
| |
|
| |
|
| |
| /**
| |
| * An keyboard event observer. It tests whether the enter key has been pressed.
| |
| * If so, the greet method is activated. Default reactions of the browser are
| |
| * disabled.
| |
| *
| |
| * @param {KeyboardEvent} p_event - A standard JavaScript keyboard event object
| |
| * (https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent)
| |
| */
| |
| l_this.sayHelloOnEnter =
| |
| function(p_event)
| |
| {
| |
| if ((p_event.code === 'Enter' || p_event.keyCode === 13) &&
| |
| p_document.activeElement ===
| |
| p_document.getElementById(p_init.HTMLElements.inputName)
| |
| )
| |
| {
| |
| p_event.preventDefault();
| |
| p_event.stopPropagation();
| |
| l_this.sayHello(); // NOT this.sayHello();
| |
| }
| |
| };
| |
| };
| |
|
| |
| return p_hello;
| |
| }(hello ? hello : {}));
| |
| </source>
| |
|
| |
| Gegenüber der Version 3 dieser Datei hat sich Folgendes geändert:
| |
|
| |
| * Es wird keine globale Konstruktorfunktion „<code>Greet</code>“ mehr definiert. Diese Konstruktorfunktion wird vielmehr in der Namensraum-Variablen „<code>hello</code>“ gespeichert: „<code>hello.Greet</code>“
| |
| * Die Konstruktorfunktion hat nur noch zwei Parameter: „<code>p_window</code>“ und „<code>p_init</code>“. Der erste Parameter enthält wie in Version 3 das globale Window-Objekt, der zweite ein JSON-Objekt, in dem alle Initialisierungsinformationen enthalten sind.
| |
| * Im Rumpf der Konstruktorfunktion werden alle notwendigen Informationen aus <code>p_init</code> extrahiert: Der Name der Formular-Section, der Name des Text-Eingabefelds, der Text der Hello-Nachricht etc.
| |
|
| |
| Legen Sie gleich noch die Datei „<code>js/app4/hello.js</code>“ für die Web-App an:
| |
| <source lang="javascript">
| |
| /** @namespace hello */
| |
| var hello =
| |
| (function(p_hello)
| |
| { "use strict";
| |
|
| |
| /**
| |
| * Initializes the HTML sections of the app that are
| |
| * stated in <code>p_init</code>.
| |
| *
| |
| * @param p_window The browser window that contains the HTML document to be initialized.
| |
| * @param p_init The initialization info for the form section and welcome section.
| |
| */
| |
| p_hello.init =
| |
| function(p_window, p_init)
| |
| {
| |
| var l_document = p_window.document,
| |
| l_init_elements = p_init.HTMLElements,
| |
| l_init_text = p_init.text,
| |
| l_greet = new p_hello.Greet(l_document, p_init);
| |
|
| |
| l_document.getElementById(l_init_elements.headingForm)
| |
| .innerHTML = l_init_text.title;
| |
| l_document.getElementById(l_init_elements.inputNameLabel)
| |
| .innerHTML = l_init_text.query;
| |
| l_document.getElementById(l_init_elements.buttonReset)
| |
| .innerHTML = l_init_text.reset;
| |
| l_document.getElementById(l_init_elements.buttonSubmit)
| |
| .innerHTML = l_init_text.submit;
| |
| l_document.getElementById(l_init_elements.buttonSubmit)
| |
| .addEventListener('click', l_greet.sayHello);
| |
| p_window.addEventListener('keydown', l_greet.sayHelloOnEnter);
| |
| };
| |
|
| |
| return p_hello;
| |
| }(hello ? hello : {}));
| |
| </source>
| |
|
| |
| Sie ist wieder aufgebaut wie zuvor beschrieben: Sie definiert, falls notwendig, das globale Objekt „<code>hello</code>“
| |
| und fügt anschließend eine Funktion „<code>init</code>“ in dieses Objekt ein. Die Methode „<code>hello.init</code>“
| |
| hat dieselben Parameter, wie die zuvor definierte Konstruktorfunktion und erwartet denselben Input.
| |
| Ihre Aufgabe ist es, die derzeit noch leeren Text-Elemente des HTML-Templates mit Inhalt zu füllen
| |
| soweit der Inhalt schon bekannt ist (der Name des Benutzers ist noch unbekannt). Außerdem erstellt sie
| |
| ein passendes Greet-Objekt „<code> l_greet</code>“ mit Hilfe der Konstruktorfunktion „<code>hello.Greet</code>“ und weist dem
| |
| Submit-Button die darin enthaltene Methode „<code>sayHello</code>“ zu. Außerdem definiert sie
| |
| eine Eventlistener, der im Falle eines Key-Down-Ereignisses die Methode „<code> l_greet.sayHelloOnEnter</code>“
| |
| ausführt.
| |
|
| |
| ===Die JSON-Datei===
| |
| Zur Initialisierung der Web-Anwendung werden JSON-Dateien benötigt.
| |
|
| |
| Eine JSON-Datei enthält genau ein JavaScript-Objekt in textueller Darstellung.
| |
| Allerdings werden nur folgende Datentypen unterstützt<ref>[http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf ECMA International, The JSON Data Interchange Format, 2013]</ref>:
| |
| * Nullwert: <code>null</code>
| |
| * Boolesche Werte: <code>true</code>, <code>false</code>
| |
| * Zahlen
| |
| * Strings
| |
| * Arrays
| |
| * Objekte
| |
|
| |
| Insbesondere Funktionen können in einer JSON-Datei nicht enthalten sein. Außerdem müssen
| |
| Strings immer in doppelte Anführungszeichen eingeschlossen sein, einfache Anführungszeichen sind nicht erlaubt.
| |
| Attributnamen werden ebenfalls durch Strings dargestellt. Auch diese müssen gemäß ECMA-Standard in doppelte
| |
| Anführungszeichen eingeschlossen werden. Außerdem können in JSON-Dateien (leider) keine Kommentare
| |
| enthalten sein.
| |
|
| |
| Folgendes Objekt ist zwar ein korrektes Javascript-Objekt, aber kein gültiges JSON-Objekt:
| |
|
| |
| <source lang="javascript">
| |
| { a: 55, 'b': 'Wolfgang' }
| |
| </source>
| |
|
| |
| In JSON muss dieses Objekt folgendermaßen geschrieben werden:
| |
|
| |
| <source lang="javascript">
| |
| { "a": 55, "b": "Wolfgang" }
| |
| </source>
| |
|
| |
| Diese Objekt ist auch ein korrektes JavaScript-Objekt. Da die Syntaxregeln von JSON strenger sind
| |
| als von JavaScript, gilt folgende Regel: Jedes JSON-Objekt ist auch ein JavaScript-Objekt, aber nicht
| |
| jedes JavaScript-Objekt ist auch ein JSON-Objekt.
| |
|
| |
| Ein Problem bereitet die Verwendung von JSON. Da ein JSON-Objekt beliebige Attribute enthalten kann,
| |
| kann eine IDE nicht überprüfen, ob ein bestimmtes Attribut oder Element in einem JSON-Objekt
| |
| mit Sicherheit enthalten ist oder nicht. Das hat {{zB}} zur Folge, das WebStorm bei einem Zugriff
| |
| auf eine Attribut in einem JSON-Objekt den Attributnamen unterringelt und mit der Warnung
| |
| „<code>Unresolved variable ...</code>“ versieht.
| |
|
| |
| Ein Lösung für dieses Problem wäre die Verwendung von [http://json-schema.org/ JSON Schema].
| |
| Aber darauf wird zunächst verzichtet.
| |
|
| |
| Legen Sie für die zuvor definierte HTML-Datei „<code>index4.html</code>“ folgende JSON-Initialisierungsdatei
| |
| unter dem Namen „<code>web/json/hello_de.json</code>“ an:
| |
|
| |
| <source lang="javascript">
| |
| {
| |
| "HTMLElements":
| |
| {
| |
| "sectionForm": "section1_form",
| |
| "headingForm": "heading1_form",
| |
| "inputNameLabel": "label1_name",
| |
| "inputName": "input1_name",
| |
| "buttonReset": "button1_reset",
| |
| "buttonSubmit": "button1_submit",
| |
| "sectionHello": "section1_hello",
| |
| "hello": "heading1_hello",
| |
| "welcome": "paragraph1_hello"
| |
| },
| |
|
| |
| "text":
| |
| {
| |
| "title": "Hallo, Fremder!",
| |
| "query": "Wie heißen Sie?",
| |
| "reset": "Reset",
| |
| "submit": "Begrüßung",
| |
| "hello": "Hallo, $1!",
| |
| "welcome": "Willkommen bei Multimedia-Programmierung!"
| |
| },
| |
|
| |
| "css":
| |
| {
| |
| "hidden": "hidden"
| |
| }
| |
| }
| |
| </source>
| |
|
| |
| Diese Datei besteht aus einem anonymen Objekt. Es enthält seinerseits drei Objekte „<code>HTMLElements</code>“,
| |
| „<code>text</code>“ und „<code>css</code>“, die für die Initialisierung der Web-Anwendung gedacht sind.
| |
| Das Objekt „<code>HTMLElements</code>“ enthält sämtliche ID-Attribut-Bezeichner der Web-Anwendung.
| |
| Wenn das JSON-Objekt im beispielsweise im Parameter „<code>p_init</code>“ der Web-Anwendung übergeben wird,
| |
| kann diese den Namen des Formularabschnitts mittels „<code>p_init.HTMLElements.sectionForm</code>“ ermitteln.
| |
| Wenn Sie in Ihrer <code>index4.html</code> andere ID-Attibute-Bezeichner verwenden, müssen Sie die JSON-Datei
| |
| entsprechend anpassen, {{dh}}, Sie müssen beispielsweise den Attribut''wert'' <code>"section1_form"</code>
| |
| durch den von Ihnen verwendeten Bezeichner ersetzen. Die Attribut''namen'' (<code>"sectionForm"</code> etc.) dürfen Sie dagegen nicht ändern,
| |
| da sonst die Methoden (<code>init</code>, <code>sayHello</code> und <code>sayHelloOnEnter</code>)
| |
| Ihrer Web-Anwendung nicht mehr funktionieren würden.
| |
|
| |
| Im Objekt „<code>text</code>“ sind alle Texte enthalten, die zu gegebener Zeit in das HTML-Dokument an die geeignete
| |
| Stelle geschrieben werden sollen. Der Text „<code>hello</code>“ enthält „<code>$1</code>“ als Platzhalter.
| |
| Dieser Platzhalter soll von der Anwendung durch den Namen des Benutzers ersetzt werden, nachdem er ihn eingegeben hat.
| |
|
| |
| In Objekt „<code>css</code>“ sind alle Namen von CSS-Klassen enthalten, die von der Web-Anwendung zur Laufzeit
| |
| zu HTML-Elementen hinzugefügt oder aus HTML-Elementen entfernt werden. Der Bezeichner, der der Anwendung bekannt ist
| |
| („<code>hidden</code>“), stimmt in diesem Fall mit dem Namen der zugehörigen CSS-Klasse überein. Das ist nicht verboten.
| |
|
| |
| Erstellen Sie analog die Datei „<code>web/json/hello_en.json</code>“. Diese Datei soll für die zweite Web-Anwendung
| |
| definiert werden, die allerdings englischsprachige Texte enthält (Sie erinnern sich? In <code>index4.html</code>
| |
| laufen zwei unabhängige Web-Anwendungen parallel).
| |
|
| |
| ===Das Laden der JSON-Dateien===
| |
|
| |
| Für das [[asynchron]]e Laden von beliebigen Dateien von einem Web-Server gibt es in JavaScript den Befehl „<code>XMLHttpRequest</code>“.
| |
| Wie man am Namen sieht, war er ursprünglich für das Laden XML-Dateien vorgesehen. Heute kann dieser Befehl allerdings für
| |
| jede Art von Datei verwendet werden. Sie könnten jetzt natürlich die Dokumentation (https://wiki.selfhtml.org/wiki/JavaScript/XMLHttpRequest)
| |
| lesen und den Befehl dann verwenden. Einfacher ist es jedoch, wenn Sie von mir erstellte die Datei
| |
| „<code>[https://glossar.hs-augsburg.de/beispiel/tutorium/es5/hello_world/WK_HelloWorld04/web/js/lib/wk/ajax.js ajax.js] </code>“
| |
| aus dem Glossar-Repository herunterladen und in Ihrem Projekt unter dem Namen „<code>web/js/lib/wk/ajax.js</code>“ speichern.
| |
|
| |
| Dieses Module stellt unter der globalen Variablen „<code>wk</code>“ – diese globale Variable ist für meine Module reserviert :-) –
| |
| im Modul „<code>wk.ajax</code>“ mehrere Methoden zum Laden von Dateien bereit. Zum Laden von JSON-Dateien
| |
| sollten Sie „<code>wk.ajax.getJSON</code>“ verwenden (obwohl es mit „<code>wk.ajax.getFile</code>“ auch ginge; Sie müssten
| |
| dann allerdings den Inhalt der Datei, der als String geliefert wird, noch in ein JavaScript-Objekt umwandeln).
| |
|
| |
| Die Methode „<code>getJSON</code>“ erwartet drei Argumente, einen URI, der den Speicherort der JSON-Datei beschreibt und zwei Callback-Funktionen.
| |
| Die erste Funktion wird aufgerufen, sobald die JSON-Datei erfolgreich geladen wurde. Ihr wird das geladen JSON-Objekt als Argument
| |
| übergeben. Die zweite Callback-Funktion wird aufgerufen, falls beim Laden etwas schief gehen sollte. Die Angabe dieser Funktion ist
| |
| optional. In der Musterlösung wurde Sie definiert, hier wird darauf verzichtet.
| |
|
| |
| Die Initialisierungsfunktion (Datei „<code>main4.js</code>“) sieht nun folgendermaßen aus:
| |
|
| |
| <source lang="javascript">
| |
| // Initialize the app.
| |
| (function(p_window, p_wk, p_hello)
| |
| { "use strict";
| |
|
| |
| // The list of all json files to be loaded
| |
| var l_json_files = ['json/hello_de.json', 'json/hello_en.json'];
| |
|
| |
| // For each element in the JSON file list:
| |
| for (var i = 0, n = l_json_files.length; i < n; i++)
| |
| p_wk.ajax.
| |
| getJSON
| |
| ( l_json_files[i], // Load the JSON file.
| |
| function(p_init) // If ready, perform the callback function.
| |
| { p_hello.init(p_window, p_init); } // Initialize the corresponding hello section.
| |
| );
| |
| }(window, wk, hello));
| |
| </source>
| |
|
| |
| Die Initialisierungsfunktion liest die beiden zuvor erstellten JSON-Dateien ein
| |
| und reicht jeweils das daraus erzeugte JSON-Objekt „<code>main4.js</code>“ an die Initialisierungsfunktion der Web-Anwendung weiter.
| |
| Beachten Sie, dass die Reihenfolge, in der die beiden JSON-Dateien geladen wird, nicht feststeht. Sie werden parallel geladen. Sobald eine
| |
| Datei vollständig geladen wurde, wird die zugehörige Callback-Funktion
| |
|
| |
| <source lang="javascript">
| |
| function(p_init) { p_hello.init(p_window, p_init); }
| |
| </source>
| |
|
| |
| ausgeführt. Dieser wird im Parameter „<code>p_init</code>“ das zugehörige JSON-Objekt überreicht.
| |
| Sie reicht es (zusammen mit die Window-Objekt) an die Methode „<code>p_hello.init</code>“ weiter.
| |
|
| |
| '''Anmerkung''': Man könnte die beiden JSON-Dateien natürlich auch mittels zweier Aufrufe von „<code>getJSON</code>“
| |
| laden:
| |
|
| |
| <source lang="javascript">
| |
| // Initialize the app.
| |
| (function(p_window, p_wk, p_hello)
| |
| { "use strict";
| |
|
| |
| p_wk.ajax.
| |
| getJSON
| |
| ( 'json/hello_de.json', // Load the JSON file 'json/hello_de.json'.
| |
| function(p_init) // If ready, perform the callback function.
| |
| { p_hello.init(p_window, p_init); } // Initialize the corresponding hello section.
| |
| );
| |
|
| |
| p_wk.ajax.
| |
| getJSON
| |
| ( 'json/hello_en.json', // Load the JSON file 'json/hello_en.json'.
| |
| function(p_init) // If ready, perform the callback function.
| |
| { p_hello.init(p_window, p_init); } // Initialize the corresponding hello section.
| |
| );
| |
| }(window, wk, hello));
| |
| </source>
| |
|
| |
| Diese Verdopplung von Code würde allerdings gegen das Programmierprinzip „[[Don't repeat yourself]]“ (DRY) verstoßen.
| |
|
| |
| ==App5==
| |
|
| |
| Im Prinzip kann die in App4 vorgestellte Lösung zur Modularisierung einer Web-Anwendung
| |
| gut eingesetzt werden. Allerdings hat sie immer noch zwei Schwächen:
| |
|
| |
| * Der Rahmencode, der in jeder Datei der Anwendung verwendet werden muss, ist lästig. (Das ist ein kleineres Problem.)
| |
| * Es gibt immer noch die globale Variable „<code>hello</code>“. Wenn man viele Bibliotheken verwendet, die auf diese Weise realisiert werden, gibt es wieder viele globale Variable. Und damit steigt das Risiko von Namenskollisionen.
| |
|
| |
| Schöner wäre es, wenn JavaScript die Modularisierung von sich aus unterstützen würde. Das ist tatsächlich geplant. In EcmaScript 6
| |
| gibt es zu diesem Zweck die Befehle „<code>[https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Statements/import import]</code>“
| |
| und „<code>[https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Statements/export export]</code>“.
| |
| Leider unterstützen die meisten Browser diese Befehle noch nicht.
| |
|
| |
| Solange dies der Fall ist, kann man auf JavaScript-Bibliotheken zurückgreifen, die dieselbe Aufgabe (wenn auch weniger elegant) erfüllen.
| |
| Hier könnte die Bibliothek „<code>RequireJS</code>“ zum Einsatz (http://requirejs.org/). An der Struktur der Anwendung ändert sich nichts.
| |
| Nur die Art, wie Module definiert und geladen werden, wird angepasst.
| |
|
| |
| Erstellen Sie in Ihrem WebStorm-Projekt letztmalig Kopien Ihre Anwendungsdateien:
| |
|
| |
| * <code>index4.html</code> → <code>index5.html</code>
| |
| * <code>js/main4.js</code> → <code>js/main5.js</code>
| |
| * <code>js/app4/greet.js</code> → <code>js/app5/greet.js</code>
| |
| * <code>js/app4/hello.js</code> → <code>js/app5/init.js</code>
| |
|
| |
| In der Datei „<code>index5.html</code>“ müssen Sie wiederum dafür sorgen, dass
| |
| die neuen JavaScript-Dateien anstelle der alten geladen werden.
| |
| Und passen Sie auch wieder den Titel im HTML-Header an.
| |
|
| |
| Als nächstes müssen Sie, um <code>RequireJS</code> nutzen zu können, den Ordner „<code>web/js/lib/require</code>“
| |
| erstellen und folgende drei Dateien (einschließlich des Inhalts, der sich hinter den drei Links verbirgt) dort speichern:
| |
| * <code>[http://requirejs.org/docs/release/2.3.2/comments/require.js require.js]</code>
| |
| * <code>[https://raw.githubusercontent.com/millermedeiros/requirejs-plugins/master/lib/text.js text.js]</code>
| |
| * <code>[https://raw.githubusercontent.com/millermedeiros/requirejs-plugins/master/src/json.js json.js]</code>
| |
|
| |
| ===Modularisierung mit RequireJS===
| |
|
| |
| Wenn man konsequent RequireJS zur Modularisierung seine Anwendung
| |
| einsetzt, gibt es neben den globalen Objekten des Browsers (<code>window</code>,
| |
| <code>document</code> etc.) nur noch das globales Objekt „<code>require</code>“ sowie die globale
| |
| Funktion „<code>define</code>“ zum Erstellen neuer Module.
| |
|
| |
| Es wird auch nur noch eine einzige JavaScript-Datei statisch geladen:
| |
| „<code>require.js</code>“. Alle anderen Dateien werden dynamisch geladen.
| |
| Die in diesen Dateien enthaltenen Module werden mittels
| |
| Callback-Funktionen an andere Module übergeben. Auf diese
| |
| Weise ist sicher gestellt, dass man keine anderen globalen
| |
| Variablen und Funktionen mehr benötigt.
| |
|
| |
| RequireJS-Moduldefinitionen sollten gemäß der AMD-Spezifikation
| |
| (AMD = [[WikipediaEN:Asynchronous_module_definition|Asynchronous Module Definition]]) erfolgen. RequireJS
| |
| kann allerdings auch Bibliotheken wie {{zB}} „<code>[https://jquery.com/ jQuery]</code>“ laden, die nicht
| |
| gemäß dieser Spezifikation erstellt wurden.
| |
|
| |
| Für RequireJS gibt es einige Plugins. Eines davon ermöglicht
| |
| das Lesen von JSON-Dateien („<code>json.js</code>“). Dieses Plugin wird verwendet,
| |
| um die Dateien „<code>json/hello_xx.json</code>“ einzulesen.
| |
|
| |
| Wenn man mit Hilfe von „<code>define</code>“ ein neues (AMD-)Modul definiert,
| |
| gibt man {{iAllg}} eine ganze Liste von Modulen an, auf denen das neue Modul basiert.
| |
| Beispielsweise benötigt das Modul „<code>hello</code>“ (welches in der Datei „<code>js/app5/hello.js</code>“ definiert wird)
| |
| das Modul „<code>greet</code>“ (welches in der Datei „<code>js/app5/greet.js</code>“ definiert wird).
| |
|
| |
| Die Moduldefinition von „<code>hello</code>“ sieht in diesem Fall folgendermaßen aus:
| |
|
| |
| <source lang="javascript">
| |
| define
| |
| ( ['app/greet'],
| |
| function (Greet)
| |
| {
| |
| return ...
| |
| }
| |
| );
| |
| </source>
| |
|
| |
| Die Funktion „<code>define</code>“ lädt zunächst alle Module, die in der Modulliste aufgeführt werden, asynchron (und damit meist auch parallel).
| |
| Allerdings wird jedes Modul nur einmal geladen. Das heißt, wenn zwei Module dasselbe Modul benötigen, wird dieses Modul trotzdem
| |
| nur einmal geladen. Sobald alle Module geladen sind, wird die im <code>define</code>-Aufruf definierte Callback-Funktion ausgeführt.
| |
| Die Funktion muss für jedes Modul, das in der Modulliste enthalten ist, einen Parameter zur Verfügung stellen,
| |
| in der das jeweilige Modul-Objekt an die Callback-Funktion übergeben wird.
| |
|
| |
| Mann könnte im <code>define</code>-Aufruf auch noch einen Modulnamen angeben. Das ist aber {{iAllg}}
| |
| nicht notwendig, da RequireJS defaultmäßig den Dateinamen (ohne Pfad und ohne Endung „<code>js</code>“)
| |
| als Modulnamen verwendet.
| |
|
| |
| Wenn man in der Definition eines Moduls auf keine anderen Module zurückgreifen will, kann man auf die Callback-Funktion verzichten.
| |
| Um ein Modul zu schreiben, das lediglich ein Objekt enthält, schreibt man einfach:
| |
|
| |
| <source lang="javascript">
| |
| define
| |
| ({
| |
| // Inhalt des Objekts, den das Modul definiert.
| |
| });
| |
| </source>
| |
|
| |
| Wenn das Modul lediglich eine Funktion, wie {{zB}} eine Konstruktorfunktion „<code>C</code>“ zur Verfügung stellen soll, schreibt man
| |
|
| |
| <source lang="javascript">
| |
| define
| |
| ( function ()
| |
| {
| |
| function C(...) {...}
| |
|
| |
| return C;
| |
| }
| |
| );
| |
| </source>
| |
|
| |
| Hier gibt es zwar eine Callback-Funktion. Diese wird jedoch sofort ausgeführt, da nicht auf das Laden anderer Module gewartet werden muss.
| |
|
| |
| ====Laden von RequireJS====
| |
|
| |
| Damit Module auch gefunden werden, muss man RequireJS zunächst konfigurieren.
| |
| Das erfolgt üblicherweise in der Datei „<code>main.js</code>“ (hier: „<code>main5.js</code>“).
| |
| Diese Datei ist auch dafür verantwortlich, das erste Modul zu laden und damit den Ladevorgang für
| |
| alle benötigten Module in Gang zu setzten. Bei uns ist das das Modul „<code>hello</code>“.
| |
|
| |
| Allerdings kann man <code>main.js</code> nicht direkt laden, sondern man muss den
| |
| Browser anweisen, zuvor <code>require.js</code> zu laden. Sobald diese Datei vollständig geladen (und übersetzt) wurde,
| |
| lädt <code>require.js</code> von sich aus <code>main.js</code> (bzw <code>main5.js</code>).
| |
| Um dies zu erreichen, fügen Sie folgendes Script-Element als letztes und einziges Script-Element
| |
| in die Datei „<code>index5.html</code>“ ein:
| |
|
| |
| <source lang="html5">
| |
| <script data-main="js/main5" src="js/lib/require/require.js"></script>
| |
| </source>
| |
|
| |
| Weitere Script-Elemente werden nicht mehr benötigt, das RequireJS Module jeweils dynamisch
| |
| nachlädt, sobald sie benötigt werden.
| |
|
| |
| ====Landen von <code>main.js</code>====
| |
|
| |
| Nachdem <code>require.js</code> geladen wurde, liest das Programm das Script-Attribut „<code>data-main</code>“
| |
| und erfährt so, dass die Datei „<code>js/main5.js</code>“ für die Initialisierung und den Start der Web-Anwendung zuständig ist (die fehlende Dateiendung
| |
| „<code>.js</code>“ ergänzt RequireJS automatisch).
| |
|
| |
| Die Datei „<code>js/main5.js</code>“ konfiguriert zunächst RequireJS:
| |
|
| |
| <source lang="javascript">
| |
| requirejs.config
| |
| ({
| |
| baseUrl: 'js', // By default load any modules from directory js
| |
| paths :
| |
| {
| |
| app: 'app5',
| |
| loadjson: 'lib/require/json',
| |
| text: 'lib/require/text',
| |
| json: '../json'
| |
| }
| |
| });
| |
| </source>
| |
|
| |
| Hier wird festgelegt, dass sich die JavaScript-Dateien im Ordner „<code>js</code>“ befinden. Das Attribut „<code>path</code>“
| |
| legt ein paar spezielle Unterordner fest. Modulnamen, die mit „<code>app/</code>“ beginnen (wie {{zB}} „<code>app/greet</code>“),
| |
| werden im Ordner „<code>js/app5/</code>“ gesucht. Für die Module „<code>json</code>“ (ein RequireJS-Plugin zum Laden von
| |
| JSON-Dateien) und „<code>text</code>“ (welches von
| |
| „<code>json</code>“ benötigt wird) wurde der Pfad direkt angegeben. Die JSON-Dateien finden sich im Ordner
| |
| „<code>js/../json</code>“, {{dh}} im Ordner „<code>init</code>“.
| |
|
| |
| Nun kann die eigentliche Anwendung gestartet werden:
| |
|
| |
| <source lang="javascript">
| |
| requirejs
| |
| ( ['loadjson!json/hello_de.json', 'loadjson!json/hello_en.json', 'app/init'],
| |
| function(helloDeJSON, helloEnJSON, init)
| |
| {
| |
| init(window, helloDeJSON);
| |
| init(window, helloEnJSON);
| |
| }
| |
| );
| |
| </source>
| |
|
| |
| Die Anwendung benötigt zwei JSON-Dateien (das dafür notwendige JSON-Plugin wird automatisch geladen)
| |
| und das Modul „<code>app/hello</code>“ (welches sich gemäß Konfiguration in der Datei „<code>js/app5/hello.js</code>“ befindet).
| |
|
| |
| Sobald die drei Dateien vollständig geladen wurden, wird die Callback-Funktion aufgerufen.
| |
| Dieser werden die beiden JSON-Objekte sowie das Hello-Initialisierungs-Modul-Objekt übergeben.
| |
| Die Callback-Funktion kann somit problemlos die beiden Hello-World-Anwendungen mittels „<code>p_hello.init</code>“ initialisieren und starten.
| |
|
| |
| ====Das Modul „<code>hello</code>“====
| |
|
| |
| Der Code des Moduls „<code>init</code>“ unterscheidet sich nicht nur hinsichtlich des Modulrahmens, sondern auch noch in
| |
| eine zweiten in einem wesentlichen Aspekt vom
| |
| Code des Moduls „<code>hello</code>“ der <code>App4</code>: In der App4 gibt es ein globales Objekt namens „<code>hello</code>“, das die gesamte Web-Anwendung enthält. Insbesondere gibt es die Methode „<code>hello.init</code>“ zum Initialisieren der App. Im Gegensatz dazu gibt es in der App5 überhaupt keine anwendungsspezifischen globalen Objekte. Daher ist es nicht notwendig die Funktion „<code>init</code>“ als Methode in irgendein Objekt einzufügen.
| |
| Dementsprechend wurde auch der Modulname von „<code>hello</code>“ in „<code>init</code>“ geändert.
| |
|
| |
| <source lang="javascript">
| |
| define
| |
| ( ['app/greet'],
| |
| function (Greet) // The module 'app/greet' contains the constructor 'Greet'.
| |
| {
| |
| /**
| |
| * Initializes the HTML sections of the app that are
| |
| * stated in <code>p_init</code>.
| |
| *
| |
| * @param p_window The browser window that contains the HTML document to be initialized.
| |
| * @param p_init The initialization info for the form section and welcome section.
| |
| */
| |
| function init(p_window, p_init)
| |
| {
| |
| var l_document = p_window.document,
| |
| l_init_elements = p_init.HTMLElements,
| |
| l_init_text = p_init.text,
| |
| l_greet = new Greet(l_document, p_init);
| |
|
| |
| l_document.getElementById(l_init_elements.headingForm)
| |
| .innerHTML = l_init_text.title;
| |
| l_document.getElementById(l_init_elements.inputNameLabel)
| |
| .innerHTML = l_init_text.query;
| |
| l_document.getElementById(l_init_elements.buttonReset)
| |
| .innerHTML = l_init_text.reset;
| |
| l_document.getElementById(l_init_elements.buttonSubmit)
| |
| .innerHTML = l_init_text.submit;
| |
| l_document.getElementById(l_init_elements.buttonSubmit)
| |
| .addEventListener('click', l_greet.sayHello);
| |
| p_window.addEventListener('keydown', l_greet.sayHelloOnEnter);
| |
| }
| |
|
| |
| return init;
| |
| }
| |
| );
| |
| </source>
| |
|
| |
| Sobald das Modul „<code>app/greet</code>“ ({{dh}} die Datei „<code>js/app5/greet.js</code>“) vollständig geladen wurde,
| |
| wird die von diesem Modul bereitgestellte Konstruktorfunktion „<code>Greet</code>“ der Callback-Funktion
| |
| im Parameter „<code>Greet</code>“ übergeben. Die Callback-Funktion erstellt daraufhin ein Objekt, welches eine einzige
| |
| Methode enthält: „<code>init</code>“. Die Aufgabe dieser Methode ist es, die Web-Anwendung zu initialiseren, sobald sie
| |
| (von „<code>main5.js</code>“) aufgerufen wird. Sie erstellt insbesondere mit Hilfe der Konstruktorfunktion „<code>Greet</code>“
| |
| ein Objekt „<code>l_greet</code>“, welche speziell angepasste Methoden „<code>sayHello</code>“ und „<code>sayHelloOnEnter</code>“
| |
| zur Verfügung stellt.
| |
|
| |
| ====Das Modul „<code>greet</code>“====
| |
|
| |
| Das Modul „<code>greet</code>“ benötigt überhaupt keine anderen Module.
| |
| Daher ist die Moduldefinition noch etwas einfacher (die Liste mit benötigten Modulen entfällt ganz). Es ändert sich gegenüber App4 ebenfalls nur der Modulrahmen.
| |
|
| |
| <source lang="javascript">
| |
| define
| |
| (function ()
| |
| {"use strict";
| |
|
| |
| /**
| |
| * @class
| |
| * @classdesc Contains methods to say hello to the user of the web app.
| |
| *
| |
| * @param p_document A DOM object (usually the global object <code>document</code>)
| |
| * @param p_init A JSON object containing all init data.
| |
| */
| |
| function Greet(p_document, p_init)
| |
| {
| |
| var l_this = this; // this-hack
| |
|
| |
| /**
| |
| * Welcomes the user of the web app1 by displaying a welcome message
| |
| * that includes his name. The name is fetched from a text input field.
| |
| */
| |
| l_this.sayHello =
| |
| function ()
| |
| {
| |
| var l_init_elements = p_init.HTMLElements,
| |
| l_init_text = p_init.text,
| |
| l_init_hidden = p_init.css.hidden,
| |
| l_name = p_document.getElementById(l_init_elements.inputName).value;
| |
|
| |
| p_document.getElementById(l_init_elements.welcome)
| |
| .innerHTML = l_init_text.welcome;
| |
| p_document.getElementById(l_init_elements.hello)
| |
| .innerHTML = l_init_text.hello.replace('$1', l_name);
| |
|
| |
| p_document.getElementById(l_init_elements.sectionHello)
| |
| .classList.remove(l_init_hidden);
| |
| p_document.getElementById(l_init_elements.sectionForm)
| |
| .classList.add(l_init_hidden);
| |
| };
| |
|
| |
| /**
| |
| * An keyboard event observer. It tests whether the enter key has been pressed.
| |
| * If so, the greet method is activated. Default reactions of the browser are
| |
| * disabled.
| |
| *
| |
| * @param {KeyboardEvent} p_event - A standard JavaScript keyboard event object
| |
| * (https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent)
| |
| */
| |
| l_this.sayHelloOnEnter =
| |
| function (p_event)
| |
| {
| |
| if ((p_event.code === 'Enter' || p_event.keyCode === 13) &&
| |
| p_document.activeElement ===
| |
| p_document.getElementById(p_init.HTMLElements.inputName)
| |
| )
| |
| {
| |
| p_event.preventDefault();
| |
| p_event.stopPropagation();
| |
| l_this.sayHello(); // NOT this.sayHello();
| |
| }
| |
| };
| |
| }
| |
|
| |
| return Greet; // Return the constructor defined above.
| |
| });
| |
| </source>
| |
|
| |
|
| ==Quellen== | | ==Quellen== |
| <references/> | | <references/> |
| <ol> | | <ol> |
| <li value="4"> {{Quelle|Kowarschick, W.: Multimedia-Programmierung}}</li> | | <li value="2"> {{Quelle|Kowarschick, W.: Multimedia-Programmierung}}</li> |
| </ol> | | </ol> |
| <noinclude>[[Kategorie: HTML5-Tutorium: JavaScript: Hello World]][[Kategorie: HTML5-Beispiel]][[Kategorie:Kapitel:Multimedia-Programmierung:Beispiele]]</noinclude> | | <noinclude>[[Kategorie: HTML5-Tutorium: JavaScript: Hello World]][[Kategorie: HTML5-Beispiel]][[Kategorie:Kapitel:Multimedia-Programmierung:Beispiele]]</noinclude> |