Bisher spielt sich alles Wesentliche auf dem Client (im Browser) ab. Die TicTacToe-App kann zwar übers Internet ausgeliefert werden, aber nach der Übertragung der drei Dateien (HTML, CSS & JS) fliessen keine Daten mehr zwischen Browser und Server.
Insbesondere ist es auch nicht möglich, übers Netz gegeneinander zu spielen. Dafür müsste die Spiel-Logik nicht in Browser (also im Client) ausgeführt werden, sondern in einem Programm, das auf dem Server läuft. Die beiden Spieler:innen verbinden sich mit dem Server und rufen bestimmte Internetseiten auf, um den Zustand des Spiels zu verändern.
Wir trennen also die Darstellung (HTML & CSS) von der Spiellogik ab. Die Logik soll auf dem Server ausgeführt werden, die Browser rufen lediglich geeignete Schnittstellen (en. Application Programming Interface oder API) auf. Bei einer Web-App erfolgt der Aufruf der Schnittstelle über eine HTTP-Anfrage.
Das HyperText Transfer Protocol dient der Übertragung von Informationen zwischen Server und Client (Browser). Bisher haben wir ausschliesslich ganze Dateien (tictactoe.html
, tictactoe.css
, tictactoe.js
) übertragen, aber HTTP kann noch mehr. Wir schauen uns an, was passiert, wenn wir auf unsere Webapp zugreifen. In den DevTools zeigen wir den Network
Tab, wo die Folge der HTTP Requests angezeigt werden.
Jeder Request verlangt eine bestimmte Ressource (bisher: eine Datei) mit einer bestimmten Method. Die Method beschreibt die Aktion, typischerweise
GET
, um eine Ressource zu holen, POST
um eine Ressource auf dem Server zu verändern.
Der Request enthält eine Anzahl zusätzlicher Informationen als Headers, insbesondere:
Der Server antwortet mit einem Response Code und zusätzlichen Informationen, zum Beispiel der angeforderten Ressource. Die wichtigsten Codes sind:
Zusätzlich kann der Server noch Cookies mitliefern, die der Browser speichert und beim nächsten Request an den gleichen Server im Request mitgeliefert werden.
Session-Cookies leben nur, solange das Browser-Fenster offen ist und sind in der Regel unproblematisch. Long-lived cookies können über Jahre installiert bleiben und bei jedem Seitenbesuch erneuert werden. Sie werden zum Teil benützt, um Benutzer über Webseiten hinweg zu verfolgen, und haben zur ganzen Diskussion um Cookies geführt. Um den Benutzer zu identifizieren (zum Beispiel nach erfolgreichem Login) sind sie aber unverzichtbar.
Requests werden im klassischen Web vom Browser ausgelöst, um eine HTML-Datei und die darin eingebundenen Ressourcen (Stylesheets, Bilder, Javascript) zu laden. Sie können aber auch vom Javascript-Code angefordert werden. Dieser Mechanismus (Fetch) wird von allen modernen Webapps benutzt, um Inhalte dynamisch nachzuladen. Beispielsweise lädt Google Maps Kartenkacheln nach, wenn der Kartenausschnitt durch Zoomen oder Verschieben verändert wird.
Wir werden diese Technik verwenden, um den Spielstand auf dem Server nachzuladen, wenn der andere Spieler gespielt hat, und um Spielzüge auszuführen.
Eine Herausforderung in Javascript (und nicht nur dort) ist der Umgang mit längeren Aufgaben. Reagiert unser Code ja auf einen Event (z.B. einen Klick) und führt dann eine kurze Aufgabe (z.B. den Game-State ändern) aus, ist das kein Problem. Wollen wir eine potentiell längere Aufgabe (insbesondere: Network-Traffic über fetch()
, oder eine Animation) ausführen, reklamiert der Browser, weil wir damit den Event-Thread blockieren. Das heisst, dass andere Events nicht richtig ausgeliefert werden können, und die App „eingefroren“ ist.
Wir wollen längere Aufgaben asynchron oder nebenläufig ausführen können. Modernes Javascript unterstützt die Programmierung von asynchronem Code mit zwei Konstrukten: Promises
und den async
/ await
Keywords, die auf Promise
aufbauen.
Eine Promise
ist ein Versprechen für das Resultat einer nebenläufigen Aufgabe. Die Promise ist zu beginn im Zustand pending
und geht von da entweder über zu fulfilled
(die Aufgabe war erfolgreich) oder rejected
(die Aufgabe ist fehlgeschlagen). fulfilled
und rejected
werden zusammen als settled
bezeichnet.
Wenn wir im Code auf das Resultat einer Promise warten möchten, können wir das mit dem Keyword await
tun. Da die Ausführung dieses Codes eine Weile dauern könnte, ist die Verwendung von await
nur in Funktionen zulässig, die mit dem Keyword async
(für asynchronous, also nebenläufig) markiert sind. Eine solche Funktion gibt automatisch immer selbst eine Promise zurück, auch wenn das im Code nicht so markiert ist.
async fetchUpdateFromServer() { try { const response = await fetch('/game/0'); const json = await response.json(); updateUi(json)); } catch (error) { console.log(error); } }
Merke:
await
wartet auf das Fulfilment einer Promise. Das bedeutet auch, dass die Methode lange dauern könnte.await
ist deshalb nur in async
Funktionen erlaubt. Async Funktionen geben immer eine Promise zurück, auch wenn das im Code nicht mehr sichtbar ist.try-catch
gefangen werden.
Sollen nicht ganze Dateien, sondern nur kleine Informationsschnipsel übertragen werden, wird dafür die JavaScript Object Notation (JSON) verwendet. JSON sieht so aus:
{ "id": 0, "state": "ended", "grid": [ "X", " ", " ", "X", "O", " ", "X", "O", " " ], "winner": "X", "player": "X", "next": "O" }
Merke:
key: value
, wobei der key
ein String sein muss, der Wert aber eines der folgenden Elemente sein kann:„ended“
)42
)Bevor wir ein eigenes Web-API für TicTacToe schreiben, werden wir ein frei verfügbares Web-API verwenden, um eine dynamische Webseite zu erstellen.
Die Seite https://wiewarm.ch stellt neben der Webseite auch ein JSON-Web-API bereit, das die Wassertemperaturen von vielen Badeanstalten in der Schweiz rapportiert. Wichtig ist die Unterscheidung:
16
ist die Id der Badi Romanshorn
Erstelle eine neue HTML-Datei bodensee.html
mit folgendem Inhalt:
<html> <head> <title>Bodensee-Temperatur</title> </head> <body> <main> <h1>Bodensee</h1> <p>Temperatur: <span id="tempBodensee"></span></p> </main> <script src="bodensee.js" async></script> </body> </html>
Unser Javascript verlangt die JSON-API mittels fetch
. Die Antwort wird, sobald sie eintrifft, als JSON interpretiert und weiter verarbeitet:
const tempBodensee = document.getElementById('tempBodensee'); const badi_romanshorn_id = 16; const bodensee_id = 51; const url = 'https://www.wiewarm.ch/api/v1/temperature.json/' + badi_romanshorn_id; // Um await zu verwenden, muss die Funktion mit async markiert werden. async function requestTemp () { // fetch(url) wird nebenläufig ausgeführt, das Resultat // wird irgendwann in Zukunft zur Verfügung stehen. Die // nebenläufige Berechnung wird mit einer Promise verfolgt. let fetchPromise = fetch(url); // 'await' bewirkt, dass die Ausführung stoppt, // bis die Promise fulfilled ist. Dann wird die response-Variable // gesetzt und der weitere Code ausgeführt. let response = await fetchPromise; if (response.ok) { let json = await response.json(); tempBodensee.textContent = json[bodensee_id].temp + '°C'; } } requestTemp();
Füge obigen Code (bodensee.html
und bodensee.js
) in dein Projekt ein und öffne die Webseite im Browser. Wie warm ist der Bodensee?
console.log(Date.now())
Zeilen in den JS-Code ein, um festzustellen, in welcher Reihenfolge der Code ausgeführt wird