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. {{ :ef_informatik:connect4-css.jpg?nolink&300}} # Web App Game Ziel dieses Blocks ist, die Grundlagen der Web-App-Programmierung zu kennen. Dazu programmieren wir Schritt für Schritt ein einfaches Spiel (//Vier gewinnt//), das im Browser (inklusive Mobiltelefon) gegeneinander gespielt werden kann. ## Weshalb eine Web-App? Das Verteilen und Verkaufen von Programmen war über Jahrzehnte aufwändig, fehleranfällig und teuer. Eine Web-App hingegen kann jede:r im hintersten Winkel der Erde programmieren und auf einem Server für 4 Milliarden Internetnutzer ((https://de.statista.com/themen/42/internet/)) bereitstellen. Eine Web-App ist sehr niederschwellig, da die Benutzer nichts installieren müssen. Eine gute Web-App läuft auf allen Plattformen und Betriebssystemen (Desktop, iOS, Android...). Eine Web-App erlaubt es, neue Funktionen zuerst mit einer kleinen Anzahl von Benutzern auszuprobieren, was das iterative Verbessern einer Anwendung ermöglicht. ## Quartalsplan Wir gehen Schritt für Schritt vor: - //UI//: User Interface mit HTML und CSS entwerfen (1 Woche). - //Server-less Game//: Logik mit Javascript in Browser implementieren (2 Wochen). - //Server-based Game mit Flask//: - HTML mit Templates vereinfachen (1 Woche). - Server-Schnittstelle mit JSON entwerfen (1 Woche). - Gameplay auf den Server verschieben (1 Woche). - Deployment # Web-Basics: HTML & CSS HTML und CSS sind die Sprachen des Web. Mit ihnen werden (fast) alle Webseiten auf eine Weise beschrieben, dass jeder Browser weiss, wie sie angezeigt werden sollen. Viele werden beidem schon begegnet sein - wem sie noch etwas fremd sind, findet hier eine gute [[https://informatik.mygymer.ch/ef2021/007.html-css/01.html-grundlagen.html|Einführung]]. Als Referenz-Werk für alle Elemente und Attribute empfehle ich [[https://wiki.selfhtml.org/|SelfHtml]]. ## Aufgabe 1a {{ :ef_informatik:connect4-basic.jpg?300}} * Erstelle ein [[https://github.com/|github]] Repository für das Projekt. * Erstelle eine neue Datei, z.B. ''connect_four.html'' mit dem HTML Inhalt für das Spiel Vier-Gewinnt. * Hinweise: * Ich empfehle, für den Raster zwei Ebenen von ''<div>'' Elementen zu verwenden und auf ein ''<table>'' zu verzichten. * Da die einzelnen Felder gedrückt werden können, bietet sich ein ''<button>'' Element an. * Standard-Vier-Gewinnt hat 6 Reihen à 7 Spalten. ## Aufgabe 1b * [[https://wiki.selfhtml.org/wiki/CSS/Tutorials/Einstieg/Stylesheets_einbinden#Einbinden_einer_externen_Datei|Binde eine CSS-Datei ein]] (z.B. ''connect_four.css''). * Experimentiere mit CSS, um das User Interface etwas ansprechender zu gestalten. Das Resultat sollte sich an ein richtiges Vier-Gewinnt Spiel anlehnen. * Teile das Repository mit deinen Lehrpersonen per Email. * Hinweise: * Benutze die DevTools in [[https://developer.chrome.com/docs/devtools/open/|Chrome]] oder [[https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide-chromium/open/|Edge]]. * In den Dev-Tools kannst du das [[https://developer.chrome.com/docs/devtools/css/|CSS live verändern]]. * Für den Anfang die wichtigsten CSS-Eigenschaften sind: * ''background-color'' * ''height'' * ''min-width'' * ''margin'' * ''border'' * ''border-radius'' * ''box-shadow'' * Mit den CSS-Funktionen ''[[https://wiki.selfhtml.org/wiki/CSS/Funktionen/calc()|calc()]]'' und ''[[https://wiki.selfhtml.org/wiki/CSS/Funktionen/var()|var()]]'' erleichterst du dir das Leben... ## Aufgabe 1c * Erreichst du für deine Webseite die folgenden Eigenschaften? * Sie hat ein [[https://wiki.selfhtml.org/wiki/Grafik/Favicon|Favicon]]. * Sie sieht auf einem Mobiltelefon gut aus. * Hinweis: [[https://developer.chrome.com/docs/devtools/device-mode/|Simuliere ein Mobilgerät]] mit DevTools. * Sie passt sich an die Grösse des Fensters (des Mobilgeräts) an. * Hinweis: Das Schlüsselwort heisst [[https://wiki.selfhtml.org/wiki/CSS/Tutorials/Einstieg/Media_Queries|Media Queries]] ## Musterlösung 1 Mein Code als Zip-Archiv hier. Wichtig: es gibt nicht nur eine Möglichkeit, und meine Lösung stellt nur eine von vielen dar. Ich fand zum Beispiel die relativen Grössenangaben mit [[https://wiki.selfhtml.org/wiki/CSS/Wertetypen/Zahlen,_Ma%C3%9Fe_und_Ma%C3%9Feinheiten/Viewportabmessungen|vw und vh]] eine gute Alternative zu Viewport-Queries. {{ :ef_informatik:ksr_ef_viergewinnt-css_and_html.zip |}} # Javascript Die meisten Browser können Javascript (oder ECMAScript) ausführen, um Webseiten dynamisch zu gestalten. Die Syntax ist ähnlich wie `C++`, C# oder Java, am bestem am Beispiel: <file ecmascript connect_four.js> let game = new ConnectFourModel(); /** * Returns the parent div element containing the Connect-Four grid. * * @param {Element} parent */ function findFourConnectUi(parent) { return parent.getElementById("grid"); } const view = new ConnectFourView(findFourConnectUi(document)); view.connectToGame(game); </file> * Variablen durch ''let'' deklariert, Konstanten durch ''const''. * Ausdrücke werden durch ein Semikolon beendet. * Auf den [[https://de.wikipedia.org/wiki/Document_Object_Model|DOM]] (also, das HTML) kann über die globale ''document'' Variable zugegriffen werden. * Ein gesuchtes HTML-Element kann z.B. über den Tag-Namen (''getElementsByName()'') oder eine Id (''getElementById()'') gefunden werden. * Javascript ist nicht typisiert - Variablen und Funktionsparameter haben keinen deklarierten Typ. Um Fehler zu vermeiden kann der Typ im Kommentar angegeben werden: ''@param {Element} parent''. <file ecmascript view.js> /** * The UI of a game of connect-four. */ class ConnectFourView { /** * Creates a new view that will update the given button grid and winner area. * * @param {Element} grid */ constructor(grid) { this.grid = grid; } } </file> * Klassen kapseln Zustand und Funktion (in Methoden) und können einen Constructor haben. * Felder (''this.grid'') werden durch Zuweisung erzeugt und müssen nicht deklariert werden. * Grundsätzlich sind alle Members (Felder und Methoden) öffentlich. <file ecmascript view.js> // ... in class ConnectFourView /** * Connects this view to the given game. * * @param {ConnectFourModel} game */ connectToGame(game) { let index = 0; for (let button of this.grid.getElementsByTagName("button")) { // Use a constant value that will be captured in the // event listener. Use modulo operator to compute the column // from the button index. const column = index % game.width; button.addEventListener("click", () => { game.insertPiece(column); this.fillHtml(game); }); index++; } } /** * Updates the view (button elements) to match the game state. * * @param {ConnectFourModel} game */ fillHtml(game) { // Modify the HTML DOM to match the game state, using the following properties: // element.setAttribute(<attribute>, "value") // element.innerHTML = "content" // element.classList.add() } </file> * Reaktion auf Benutzerverhalten durch //Callbacks//, die mit ''addEventListener'' an ein Signal gebunden wird (im obigen Beispiel wird eine Funktion an den ''click'' Event des Button Elements gebunden). * Um Probleme mit ''this'' zu vermeiden, empfehle ich, für Callbacks sogenannte [[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions|Arrow-Functions]] zu verwenden. ## Aufgabe 2a * [[https://wiki.selfhtml.org/wiki/JavaScript/Tutorials/DOM/Einbindung_in_HTML#JavaScript-Dateien_in_HTML_referenzieren|Binde]] eine Javascript-Datei ''connect_four.js'' in die HTML-Datei ein. * Schreibe Javascript-Code mit folgenden Eigenschaften: * Wird auf einer der Buttons gedrückt, wird in der entsprechenden Spalte ein Stein des momentanen Spielers eingefügt. * Gewinner werden identifiziert. * Hinweise: * Verwende die obigen Code-Schnipsel als Startgerüst, wenn der Anfang schwierig ist. * [[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference|Javascript-Referenz]] bei Mozilla ## Aufgabe 2b Wenn das alles zu einfach ist, oder du Lust auf mehr hast: * Schaffst du es, dasselbe Spiel auf zwei separaten Teilen der Seite darzustellen? * Tipp: Separiere das Modell des Spiels (''model'') von der Darstellung (''view''). * Das Muster (//en.// pattern) dahinter heisst //[[https://de.wikipedia.org/wiki/Model_View_Controller|Model-View-Controller]]// und ist in vielen Variationen in der Informatik weit verbreitet. * Wenn du zusätzlich zu Model und View noch den //Controller// separierst, wäre es möglich zum Beispiel eine Read-only-View darzustellen, die sich nicht klicken lässt... ## Aufgabe 2c Als Vorbereitung für den nächsten Schritt: * Füge noch eine weitere Indirektion hinzu: der View soll statt eines Javascript models nur ein JSON-Objekt erhalten, um seine Darstellung zu ändern. * JSON wird die Sprache sein, die wir vom Web-Server an die App ausliefern werden. * Hinweis: [[https://developer.mozilla.org/de/docs/Learn/JavaScript/Objects/JSON|JSON Referenz]] ## Musterlösung 2 Wie immer gilt: es gibt unendlich viele korrekte Lösungen. {{ :ef_informatik:ksr_ef_viergewinnt-js_game.zip |}} # HTTP Das HyperText Transfer Protocol wird benützt, um Webseiten (und CSS und JS und Bilder...) zwischen einem Client (z.B. dem Browser) und einem Server auszutauschen. 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. ## Request & Response {{ :ef_informatik:http_404.jpg?nolink&500|}} Jeder Request verlangt eine bestimmte //Ressource// 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: * **If-modified-since**: <timestamp> * Das Dokument nur zurückgeben, wenn es seit dem Zeitstempel verändert wurde. * Sonst antwortet der Server mit einem 304, um Caching zu ermöglichen, so dass nicht immer die ganze Seite übertragen werden muss. * **Referer**: Welche Seite oder Host hat die Anfrage ausgelöst. * wird dazu verwendet, das Surfverhalten von Nutzern zu analysieren * **User-agent**: * Enthält Informationen zum Browser des Benutzers. * Kann zum Beispiel verwendet werden, um ein Mobiltelefon von einem Desktop-Computer zu unterscheiden. {{ :ef_informatik:http_200.jpg?nolink&500|}} Der Server antwortet mit einem //[[https://de.wikipedia.org/wiki/HTTP-Statuscode|Response Code]]// und zusätzlichen Informationen, zum Beispiel der angeforderten Ressource. Die wichtigsten Codes sind: * **200** (OK): Anfrage ist erfolgreich, die Ressource ist im Response Body enthalten. * **304** (UNMODIFIED): Anfrage ist in Ordnung, die Ressource ist unverändert und wird darum nicht zurückgegeben (Nur bei GET, nicht bei POST) * **404** (NOT FOUND): Die Ressource wurde nicht gefunden. ## Cookies ## 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. # Web-App mit Flask Bislang haben wir den Inhalt unserer App lokal von unserem Gerät geladen. Das hat ein paar Nachteile: * Nicht jedermann darauf Zugriff, die App ist also noch sehr begrenzt nutzbar. * Der Inhalt ist statisch, d.h. wir können das HTML nicht je nach Benutzeraktion frei zusammenstellen. * Den Inhalt von connect_four.html könnten wir viel kompakter Darstellen, wenn wir Schleifen benützen könnten. * Es gibt keinen globalen Zustand, der zwischen mehreren Benutzern geteilt werden könnte. Nix Multiplayer. Um diese Problem zu lösen, werden wir die App auf einen Web-Server verschieben. * Install Flask mit ''pip3 install flask''. * Use the [[https://flask.palletsprojects.com/en/2.0.x/quickstart/|quick start guide]] to help you with the following tasks. * Transfer existing code to web app: * Move static files to ''/static''. * Create ''app.py'' in root folder to create the simple-most web app. <code python app.py> from flask import Flask app = Flask(__name__) @app.route("/") def hello_world(): return "<p>Hello, World!</p>" </code> * Turn HTML into a Jinja2 template. * Create a connect-four game model in python. * Our App will communicate with the Server through JSON endpoints * see an [[https://de.wikipedia.org/wiki/Representational_State_Transfer#Beispiel|example of a REST endpoint]] * we need endpoints for * fetching gamestate * inserting a piece * tricky part: identify the user using [[https://flask.palletsprojects.com/en/2.0.x/quickstart/#sessions|sessions]] * Instead of holding game state on the client side, fetch it from the server. Weitere Ideen: * Long polling für den Game-State * Websockets (nicht direkt unterstützt in Flask, aber in [[https://flask-socketio.readthedocs.io/|Flask-SocketIO]] oder [[https://pgjones.gitlab.io/quart/|Quart]]) # Javascript 2 ## Fetch Requests werden im klassischen Web für eine HTML-Ressource und die darin eingebundenen Ressourcen (Stylesheets, Bilder, Javascript) ausgelöst. Sie können aber auch vom Javascript-Code angefordert werden. Dieser Mechanismus ([[https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch|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. ## Asynchronous code 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. ### 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. Mit Promises kann ich eine Sequenz von voneinander abhängigen Aufgaben zusammenstellen, ohne dass diese voneinander wissen müssen. Dafür nutzt man die Methoden ''then()'' und ''catch()'', die beide als Argument eine Funktion haben, die erst ausgeführt wird, wenn die Promise fullfilled (bei ''then'') oder rejected (bei ''catch'') wird: <code javascript> class ConnectFourView { updateUi(json) { // Updates the view using json.cells } fetchUpdateFromServer() { fetchPromise = fetch('/gamestate'); jsonPromise = fetchPromise.then(response => response.json()); updatePromise = jsonPromise.then(json => updateUi(json)); updatePromise.catch(error => console.log(error)); } } </code> Was geht in ''fetchUpdateFromServer'' vor? * Fetch beginnt den gegebenen URL zu laden und gibt eine Promise zurück, die in Zukunft erfüllt werden wird. * Ist ''fetch'' erfolgreich, wird eine ''[[https://developer.mozilla.org/en-US/docs/Web/API/Response|Response]]'' zurückgegeben. * Mit ''then'' hängen wir der Promise eine weitere Funktion an, die erst ausgeführt wird, wenn die erste erfolgreich war. * Die Funktion erhält als Argument das Resultat der vorangehenden Promise, also die ''Response'' * ''Response.json()'' gibt eine Promise zurück auf den geparsten JSON-Inhalt der Response. * Ist die zweite Promise erfolgreich, passen wir die View anhand des JSON-Inhalts an. * Die letzte Promise erhält einen Failure-Handler (''catch''). Schlägt eine der vorgelagerten Promises fehl, wird auch die letzte Promise rejected, der Fehler wird in der Konsole ausgegeben. Merke: * Wenn die Funktion ''fetchUpdateFromServer'' fertig ausgeführt wird, ist das Update noch lange nicht erfolgt. Die Promise-Kette wird nebenläufig ausgeführt, die Seite "gefriert" nicht. Das Ganze lässt sich mit Chaining auch kompakter darstellen: <code javascript> fetchUpdateFromServer() { fetch('/gamestate') .then(response => response.json()) .then(json => updateUi(json)) .catch(error => console.log(error)); } </code> ### Async Functions Promises sind grossartig, aber der Code ist etwas gewöhnungsbedürftig. Seit 2017 kann dasselbe eleganter geschrieben werden: <code javascript> async fetchUpdateFromServer() { try { const response = await fetch('/gamestate'); const json = await response.json(); updateUi(json)); } catch (error) { console.log(error); } } </code> 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. * Fehler können mit dem gewohnten ''try-catch'' gefangen werden. * read up on ''async'' and ''await'' [[https://developers.google.com/web/fundamentals/primers/async-functions|here]] * [[https://web.dev/promises/|more on Promises]] {{ :ef_informatik:javascript_async_fetch_promises.png?nolink&600 | }} ef_informatik/web_app.txt Zuletzt geändert: 2021-12-15 13:41von hof