Client-Side Javascript
Der Browser benötigt weiterhin Javascript für den Informationsaustausch mit dem Server. Der Click-Handler jedes Buttons soll den set URL aufrufen und anschliessend den Inhalt des HTMLs an den Spielzustand anpassen.
Relative URLs
Absolute URLs kennen wir: https://google.com/index.html ist beispielsweise absolut. Relative URLs haben keinen beginnenden Schrägstrich, sondern starten gleich mit dem Pfad, z.B. unterordner/datei.html.
Wenn eine Webseite eine relative URL enthält, so bastelt der Browser beim Aufruf einen neuen URL aus dem der Ursprungsseite und dem relativen Pfad:
- das letzte Segment („Dateiname“) des URLs wird abgetrennt
- und der relative Pfad wird angefügt.
Beispiel: Die Seite http://localhost/unterordner/index.html enthält den relativen URL <a href="foobar/seite.html">Link</a>. Der Browser lädt den URL http://localhost/unterordner/foobar/seite.html.
Erinnerung: unsere URLs verwenden das folgende Schema:
- Spiel-URL:
http://localhost:3001/42/connect4.html- Schnittstelle für Game-Zustand:
http://localhost:3001/42/game - Schnittstelle für Spielzug:
http://localhost:3001/42/set/4
Wir können also aus dem client-side Javascript einfach die relativen URLs game oder set/4 aufrufen, der Browser setzt automatisch den Seitenpfad inklusive Game-Id vorne dran (MDM: Resolving relative references).
Javascript Fetch
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.
Asynchronous code
Eine Herausforderung in Javascript (und nicht nur dort) ist der Umgang mit längeren Aufgaben. Reagiert unser Code lokal 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.
Promise
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.
Await & Async
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 function fetchGameStateFromServer() { try { let response = await fetch('game') let json = await response.json() updateUi(json)) } catch (error) { console.log(error) } }
Merke:
awaitwartet auf das Fulfilment einer Promise. Das bedeutet auch, dass die Funktion lange dauern könnte.awaitist deshalb nur inasyncFunktionen erlaubt. Async Funktionen geben immer eine Promise zurück, auch wenn das im Code nicht sichtbar ist.- Fehler können mit dem gewohnten
try-catchgefangen werden.
Umsetzung im Client-Javascript
Unser client-side Javascript muss immer noch Click-Handler installieren. Statt direkt das Game zu verändern, ruft der Handler den entsprechenden URL auf und reflektiert anschliessend den neuen Spielzustand im HTML:
- connect4.js
/** Updates the HTML user interface to match the given game state. */ function updateHtml(grid, game) { … } /** Drops a piece in the given column and updates the game state accordingly. */ async function dropPiece(grid, column) { // TODO: Check if move is valid. handleFetch(grid, `set/${column}`) } /** Fetches a game URL and updates the HTML using the new game state. */ async function handleFetch(grid, url) { let response = await fetch(url) let game = await response.json() // Update the HTML view. updateHtml(grid, game) // TODO: fetch game state until it's our turn again. } /* Connects the game state to the HTML user interface. */ async function init() { // Find the HTML game grid. let grid = document.getElementsByTagName('c4-grid')[0] // Fetch initial game state. await handleFetch(grid, `game`) // Install button click handlers for each button. let index = 0 for (let button of grid.getElementsByTagName("button")) { const column = index % 7 button.addEventListener("click", () => dropPiece(grid, column)) index++ } } init()
Weiter
Jetzt sollte es möglich sein, das gleiche Spiel von zwei unterschiedlichen Browser-Fenstern aus zu spielen.
Hm, und wie stellen wir sicher, dass immer nur der Spieler an der Reihe einen Zug machen kann?
Und was ist mit der Sicherheit gegenüber Dritten, die das Spiel beeinflussen könnten?
Weiter mit User Ids & Cookies