Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen der Seite angezeigt.
Nächste Überarbeitung | Vorherige Überarbeitung | ||
ksk_ef:web:apps:server [2024-08-11 09:54] – angelegt hof | ksk_ef:web:apps:server [2024-08-11 11:20] (aktuell) – hof | ||
---|---|---|---|
Zeile 1: | Zeile 1: | ||
## TicTacToe mit Server | ## TicTacToe mit Server | ||
+ | |||
+ | Mit unserem Wissen über Web-APIs und JSON wollen wir nun die TicTacToe Web-App weiterentwickeln, | ||
+ | |||
+ | Dazu muss Folgendes geschehen: | ||
+ | * Die Spiel-Logik muss vom Browser auf den Server wandern. | ||
+ | * Dazu muss ein Server-Prozess gestartet werden, wir verwenden dafür [[https:// | ||
+ | * Der Server muss geeignete Web-APIs zur Verfügung stellen: | ||
+ | * `/ | ||
+ | * `/join`: nimmt an einem Spiel teil (entweder an einem bereits mit einem Spieler wartenden, oder es wird ein neues Spiel gestartet). Gibt anschliessend den Game-Zustand zurück. | ||
+ | * `/ | ||
+ | * Das Javascript im Browser muss sich nur noch darum kümmern, die richtigen Web-APIs auf dem Server aufzurufen, und den Zustand des Spiels im HTML darzustellen. | ||
+ | |||
+ | |||
+ | ### Installation von node.js und express | ||
+ | |||
+ | * Node.js installieren: | ||
+ | * https:// | ||
+ | * Projekt initialisieren: | ||
+ | * Eine Eingabeaufforderung **im Ordner der Webapp** öffnen. | ||
+ | * `npm init` | ||
+ | * `npm install express cookie-parser` | ||
+ | * Öffne die Datei `package.json`: | ||
+ | * füge einen Eintrag hinzu: ` " | ||
+ | * Falls du git verwendest: | ||
+ | * füge `node-modules` zu `.gitignore` hinzu. | ||
+ | |||
+ | ### App definieren | ||
+ | |||
+ | Zuerst benötigen wir eine Datei `app.js`, die definiert, wie der Server funktioniert, | ||
+ | |||
+ | <code javascript app.js> | ||
+ | import express from ' | ||
+ | |||
+ | const app = express() | ||
+ | const port = 3000 | ||
+ | |||
+ | // Hello World. | ||
+ | app.get('/ | ||
+ | res.send(' | ||
+ | }) | ||
+ | |||
+ | // Listen on the given port | ||
+ | app.listen(port, | ||
+ | console.log(`Example app listening on port ${port}`) | ||
+ | }) | ||
+ | </ | ||
+ | |||
+ | Mit dem Kommandozeilenaufruf `node app.js` wird der Node-Server gestartet. Auf der Adresse http:// | ||
+ | |||
+ | Der Code bedeutet: Wann immer eine Anfrage für den Pfad `/hello` ankommt, so sende als _Response_ (_res_) den String `Hello World` zurück. | ||
+ | |||
+ | Es ist auch möglich, Teile der im Request (_req_) verlangten Adresse als Parameter zu erhalten: | ||
+ | |||
+ | <code javascript> | ||
+ | app.get('/ | ||
+ | let name = req.params[' | ||
+ | res.send(`Hello ${name}!``) | ||
+ | }) | ||
+ | </ | ||
+ | |||
+ | ### Statische Dateien | ||
+ | Die HTML und CSS Dateien bleiben unverändert - der Server soll diese bitte direkt zum Browser übertragen. Das erreichen wir, indem wir einen Ordner `static` anlegen, und die Dateien dorthin verschieben. Node wird instruiert, diese Dateien unverändert auszuliefern: | ||
+ | |||
+ | <code javascript app.js> | ||
+ | // Serve all files from static/ as is. | ||
+ | // For example, a request for '/ | ||
+ | // ' | ||
+ | app.use(express.static(' | ||
+ | </ | ||
+ | |||
+ | Ausprobieren: | ||
+ | |||
+ | ### Server-Side TicTacToe | ||
+ | |||
+ | Erstelle eine Kopie deiner `tictactoe.js` Datei, um auf dem Server ein Spiel laufen zu lassen. Idealerweise ist deine Datei bereits objektorientiert, | ||
+ | |||
+ | Was muss sich ändern? | ||
+ | * Der Spielzustand kann nicht mehr im HTML gespeichert werden. Es bietet sich an, ein Array von Buchstaben zu verwenden `let grid = Array(9).fill(' | ||
+ | * Der Spielzustand muss als JSON vom Server zu den Clients gesendet werden. Schreibe eine Funktion `toJson()`, die Code ähnlich wie den folgenden produziert: | ||
+ | |||
+ | <code javascript> | ||
+ | { | ||
+ | id: 0, // game id | ||
+ | state: " | ||
+ | grid: [ // the nine game grid cells | ||
+ | " | ||
+ | " ", | ||
+ | " ", | ||
+ | " | ||
+ | " | ||
+ | " ", | ||
+ | " | ||
+ | " | ||
+ | " " | ||
+ | ], | ||
+ | winner: " | ||
+ | player: " | ||
+ | next: " | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | In `app.js` erstellen wir ein Spiel, z.B. mit `let game = TicTacToe()`. Damit das funktioniert, | ||
+ | |||
+ | <code javascript tictactoe.js> | ||
+ | export class TicTacToe { | ||
+ | ... | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | <code javascript app.js> | ||
+ | import {TicTacToe} from ' | ||
+ | |||
+ | let the_game = new TicTacToe(); | ||
+ | |||
+ | app.get('/ | ||
+ | return the_game.toJson(); | ||
+ | }) | ||
+ | |||
+ | app.get('/ | ||
+ | the_game.set(req.params[' | ||
+ | res.json(the_game.toJson()) | ||
+ | }) | ||
+ | </ | ||
+ | |||
+ | Nun sollte unser Server ein einziges Game betreiben. Der Zustand sollte unter http:// | ||
+ | |||
+ | Was gibt es noch zu lösen? | ||
+ | * Der Browser benötigt auch Javascript. Der Click-Handler jedes Buttons soll das `set` API aufrufen und anschliessend den Inhalt des HTMLs an den Spielzustand anpassen. | ||
+ | * Wie weiss der Browser, ob er `X` oder `O` ist? | ||
+ | * Wie können mehrere Spiele betreut werden (Hinweis: schau dir die `gameid` oben an...!) | ||
+ | |||
+ | ### Client-Side Javascript | ||
+ | Der Browser benötigt auch Javascript. Der Click-Handler jedes Buttons soll das `set` API aufrufen und anschliessend den Inhalt des HTMLs an den Spielzustand anpassen. | ||
+ | |||
+ | <code javascript tictactoe_client.js> | ||
+ | class Tictactoe_Client { | ||
+ | | ||
+ | /** | ||
+ | * Fetches the game state, updates the UI, and installs click handlers. | ||
+ | */ | ||
+ | async init() { | ||
+ | let i = 0; | ||
+ | for (const button of this.view.grid.getElementsByTagName(" | ||
+ | const cell = i; | ||
+ | button.addEventListener(' | ||
+ | this.handleJsonUrl(`/ | ||
+ | }); | ||
+ | i++; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Fetches the given URL and updates the internal state from the parsed JSON. | ||
+ | | ||
+ | * Keeps polling for updates if the updated state could change remotely. | ||
+ | | ||
+ | * @param {string} url the URL to fetch that will return tictactoe JSON. | ||
+ | */ | ||
+ | async handleJsonUrl(url) { | ||
+ | let response = await fetch(url); | ||
+ | if (!response.ok) { | ||
+ | let error = await response.text(); | ||
+ | console.log(" | ||
+ | return; | ||
+ | } | ||
+ | const json = await response.json(); | ||
+ | this.updateHtml(json); | ||
+ | } | ||
+ | | ||
+ | /** Update the HTML based on JSON game state. */ | ||
+ | updateHtml(json) { | ||
+ | let i = 0; | ||
+ | |||
+ | for (const button of this.grid.getElementsByTagName(" | ||
+ | const cellText = json.grid[i]; | ||
+ | // Set the data-state attribute which drives CSS formatting. | ||
+ | button.setAttribute(' | ||
+ | // Set the text contents of the cell. | ||
+ | button.textContent = cellText; | ||
+ | i++; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ### Sicherheit, User Ids & Cookies | ||
+ | Um sicherzustellen, | ||
+ | |||
+ | <code javascript app.js> | ||
+ | import express from ' | ||
+ | import cookieParser from ' | ||
+ | import {TicTacToe, GameState} from ' | ||
+ | |||
+ | const app = express() | ||
+ | app.use(cookieParser()) | ||
+ | const port = 3000 | ||
+ | |||
+ | function getUserId(req, | ||
+ | let userid = req.cookies.userid | ||
+ | if (userid == undefined) { | ||
+ | userid = crypto.randomUUID() | ||
+ | res.cookie(' | ||
+ | } | ||
+ | return userid | ||
+ | } | ||
+ | |||
+ | app.get('/ | ||
+ | const userid = getUserId(req, | ||
+ | for (let game of Object.values(games)) { | ||
+ | if (game.isWaiting()) { | ||
+ | game.join(userid) | ||
+ | res.json(game.toJson(userid)) | ||
+ | return | ||
+ | } | ||
+ | } | ||
+ | // No waiting game found - create a new one | ||
+ | let game = new TicTacToe(nextGameId) | ||
+ | games[nextGameId] = game | ||
+ | nextGameId += 1 | ||
+ | | ||
+ | game.join(userid) | ||
+ | res.json(game.toJson(userid)) | ||
+ | }) | ||
+ | </ | ||
+ | |||
+ | ### Hinweise | ||
+ | |||
+ | * Die ganze Web-App mit JS & Node: https:// | ||
+ | * Alternative mit Python & Flask auf der Serverseite: |