Inhaltsverzeichnis

ASCII���Screenshot

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 1) 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:

  1. UI: User Interface mit HTML und CSS entwerfen (1 Woche).
  2. Server-less Game: Logik mit Javascript in Browser implementieren (2 Wochen).
  3. Server-based Game mit Flask:
    1. HTML mit Templates vereinfachen (1 Woche).
    2. Server-Schnittstelle mit JSON entwerfen (1 Woche).
    3. Gameplay auf den Server verschieben (1 Woche).
    4. 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 Einführung. Als Referenz-Werk für alle Elemente und Attribute empfehle ich SelfHtml.

Aufgabe 1a

ASCII���Screenshot

Aufgabe 1b

Aufgabe 1c

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 vw und vh eine gute Alternative zu Viewport-Queries. 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:

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);
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;
    }
}
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()
    }

Aufgabe 2a

Aufgabe 2b

Wenn das alles zu einfach ist, oder du Lust auf mehr hast:

Aufgabe 2c

Als Vorbereitung für den nächsten Schritt:

Musterlösung 2

Wie immer gilt: es gibt unendlich viele korrekte Lösungen. 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

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:

Der Server antwortet mit einem Response Code und zusätzlichen Informationen, zum Beispiel der angeforderten Ressource. Die wichtigsten Codes sind:

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:

Um diese Problem zu lösen, werden wir die App auf einen Web-Server verschieben.

app.py
from flask import Flask
 
app = Flask(__name__)
 
@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

Weitere Ideen:

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 (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:

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));
  }
}

Was geht in fetchUpdateFromServer vor?

Merke:

Das Ganze lässt sich mit Chaining auch kompakter darstellen:

  fetchUpdateFromServer() {
    fetch('/gamestate')
      .then(response => response.json())
      .then(json => updateUi(json))
      .catch(error => console.log(error));
  }

Async Functions

Promises sind grossartig, aber der Code ist etwas gewöhnungsbedürftig. Seit 2017 kann dasselbe eleganter geschrieben werden:

  async fetchUpdateFromServer() {
    try {
      const response = await fetch('/gamestate');
      const json = await response.json();
      updateUi(json));
    } catch (error) {
      console.log(error);
    }
  }

Merke: