Seite anzeigenÄltere VersionenLinks hierherCopy this pageFold/unfold allNach oben Diese Seite ist nicht editierbar. Du kannst den Quelltext sehen, jedoch nicht verändern. Kontaktiere den Administrator, wenn du glaubst, dass hier ein Fehler vorliegt. ## TicTacToe mit Server Mit unserem Wissen über Web-APIs und JSON wollen wir nun die TicTacToe Web-App weiterentwickeln, so dass zwei Spieler:innen übers Web gegeneinander spielen können. 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://nodejs.org/|nodejs]] und [[https://expressjs.com/|express]]. * Der Server muss geeignete Web-APIs zur Verfügung stellen: * `/game/<gameid>`: gibt den Zustand des Games mit Id `<gameid>` im JSON-Format zurück. * `/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. * `/set/<gameid>/<cellid>`: Setzt im Game mit Id `<gameid>` die Zelle `<cellid>` (muss im Bereich von 0-8 sein) an. 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://nodejs.org/en/download/prebuilt-installer * 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: ` "type": "module"` * 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, und welche APIs wir bereitstellen: <code javascript app.js> import express from 'express' const app = express() const port = 3000 // Hello World. app.get('/hello', (req, res) => { res.send('Hello World!') }) // Listen on the given port app.listen(port, () => { console.log(`Example app listening on port ${port}`) }) </code> Mit dem Kommandozeilenaufruf `node app.js` wird der Node-Server gestartet. Auf der Adresse http://localhost:3000/hello können wir auf die oben definierte API zugreifen. 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('/hello/:name', (req, res) => { let name = req.params['name'] res.send(`Hello ${name}!``) }) </code> ### 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 '/tictactoe.html' will be served from // 'static/tictactoe.html' app.use(express.static('static')) </code> Ausprobieren: Node neu starten und den URL http://localhost:3000/tictactoe.html laden - unser HTML + CSS sollte korrekt angezeigt werden! ### 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, damit der Server eine Vielzahl von Spielen parallell betreuen kann. 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: "ended", // "waiting", "playing", "ended" grid: [ // the nine game grid cells "X", " ", " ", "X", "O", " ", "X", "O", " " ], winner: "X", // the player who won, if there is one. player: "X", // the user's color next: "O" // the color of the player next in turn } </code> In `app.js` erstellen wir ein Spiel, z.B. mit `let game = TicTacToe()`. Damit das funktioniert, müssen wir alle Funktionen oder Klassen in `tictactoe.js` mit `export` markieren und in `app.js` importieren: <code javascript tictactoe.js> export class TicTacToe { ... } </code> <code javascript app.js> import {TicTacToe} from './tictactoe.js' let the_game = new TicTacToe(); app.get('/game/:gameid', (req, res) => { return the_game.toJson(); }) app.get('/set/:gameid/:cell', (req, res) => { the_game.set(req.params['cell']) res.json(the_game.toJson()) }) </code> Nun sollte unser Server ein einziges Game betreiben. Der Zustand sollte unter http://localhost:3000/game/0 zurückgeliefert werden. Ein Spielzug wird über das `set` API gemacht, z.B. sollte das mittlere Feld mit http://localhost:3000/set/0/4 gesetzt werden können. Kannst du (ganz ohne Grafik, nur mit den obigen URLs) ein Spiel spielen? 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 { game_id = 0; // TODO set when joining game /** * Fetches the game state, updates the UI, and installs click handlers. */ async init() { let i = 0; for (const button of this.view.grid.getElementsByTagName("button")) { const cell = i; button.addEventListener('click', () => { this.handleJsonUrl(`/set/${this.game_id}/${cell}`); }); 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("Error: " + error); 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("button")) { const cellText = json.grid[i]; // Set the data-state attribute which drives CSS formatting. button.setAttribute('data-state', cellText); // Set the text contents of the cell. button.textContent = cellText; i++; } } } </code> ### Sicherheit, User Ids & Cookies Um sicherzustellen, dass jede:r Benutzer:in nur in einem Spiel ist, und kein böswilliger Akteur einen falschen Spielzug ausführen kann, wollen wir eine Benutzer-Id für jede Verbindung erstellen. Dafür nützen wir Cookies. Das Cookie wird gesetzt wenn zum ersten Mal einem Spiel beigetreten wird: <code javascript app.js> import express from 'express' import cookieParser from 'cookie-parser' import {TicTacToe, GameState} from './tictactoe.js' const app = express() app.use(cookieParser()) const port = 3000 function getUserId(req, res) { let userid = req.cookies.userid if (userid == undefined) { userid = crypto.randomUUID() res.cookie('userid', userid) } return userid } app.get('/join', (req, res) => { const userid = getUserId(req, res) 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)) }) </code> ### Hinweise * Die ganze Web-App mit JS & Node: https://github.com/tkilla77/ksr_tictactoe/tree/server_side_js * Alternative mit Python & Flask auf der Serverseite: https://github.com/tkilla77/ksr_tictactoe/tree/security ksk_ef/web/apps/server.txt Zuletzt geändert: 2024-08-11 11:20von hof