Action unknown: copypageplugin__copy

Wir haben bereits eine Wortkartei-App mit OO gebaut:

  • eine Klasse, die eine einzelne Übersetzung beschreibt, zum Beispiel Word.
  • eine Klasse, die eine Sammlung von Words bereitstellt, zum Beispiel Unit.
  • eine Klasse, um den Lernerfolg zu erfassen, z.B. Stats.

Ein Konsolen-Programm liest Daten im CSV-Format ein, erfragt Übersetzungen, und speichert die Resultate wieder ab.

Das ist schön und gut, aber wir möchten ja eine App bauen, die nicht nur für uns selbst zugänglich ist, sondern für jedermann über das Internet verwendet werden kann:

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 eine Webseite zugreifen. In den DevTools zeigen wir den Network Tab, wo die Folge der HTTP Requests angezeigt werden.

Ressourcen & URLs

HTTP dient dazu, Ressourcen über das Internet zu übertragen. Ressourcen sind zum Beispiel Dateien, die vom Server zum Client übertragen werden:

  • HTML (HyperText Meta Language): die Sprache, um Webseiten zu schreiben. Im wesentlichen Text, der mit <b>Tags</b> versehen wird, um <i>Struktur</i> darzustellen.
  • CSS (Cascading Style Sheets): Formatierungsregeln, um HTML ansprechend darzustellen.
  • JS (JavaScript): Programmcode, der vom Browser ausgeführt wird.
  • PNG & JPG: Bilddateien

Damit erstellen wir das Grundgerüst für unsere Web-App.

Mehr zu diesen Sprachen im Grundlagenfach Informatik 2.

Statt ganzen Dateien können wir aber als Ressourcen auch Daten übertragen, die „on-the-fly“ auf dem Server erzeugt werden. Beispielsweise können wir C#-Code schreiben, um dynamisch ein zufälliges Wort zur Übersetzung zu liefern.

Jede Ressource wird durch einen Uniform Resource Locator (URL) identifiziert. Die URL ist genau das, was in der Adresszeile des Browsers angezeigt wird. Beispiel für URLs in unserer App:

  • http://localhost:1234/website/index.html - die URL für die HTML-Datei.
  • http://localhost:1234/random - die URL, um ein zufälliges Wort zurückzugeben.

Die URL besteht aus:

  • dem Protokoll: http
  • dem Servernamen und optional einem Port: localhost:1234
  • dem Pfad der Ressource auf dem Server: /website/index.html

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:

  • 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.

Der Server antwortet mit einem 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.
  • 500 (SERVER ERROR): Ein Fehler auf dem Server ist passiert.

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 Session-Cookies aber unverzichtbar.

Wir verwenden das Grapevine-Framework, um einen einfachen Webserver zu bauen.

Wir fügen das Framework zu unserem Projekt hinzu:

dotnet add package Grapevine --version 5.0.0-rc.10

Dann starten wir einen Server, der Dateien im Ordner website als Dateien überträgt:

VociServer.cs
using Grapevine;
 
namespace server;
 
class VociServer {
    static void Main(string[] args) {
        // Start the standalone REST server.
        using (var server = RestServerBuilder.UseDefaults().Build())
        {
            // The server will scan the entire assembly for RestResource annotations
            // and add them to the served prefixes.
 
            // In addition, we want to serve static content (HTML, CSS, JS that is not modified
            // but transferred to the client as is from the "website" folder.
            string staticPath = Path.Combine(Directory.GetCurrentDirectory(), "website");
            server.ContentFolders.Add(staticPath);
            server.UseContentFolders();
 
            // Start the server.
            server.Start();
 
            Console.WriteLine("Press enter to stop the server");
            Console.ReadLine();
        }
    }
}

Wenn wir ihn starten mit dotnet run, können wir alle Dateien im website Ordner übertragen. Wenn du jetzt mehrere Main Funktionen hast, kannst du die auszuführende angeben mit

dotnet build -p:StartupObject=server.VociServer
dotnet run 

Eine einfache Website könnte beispielsweise mit folgendem HTML dargestellt werden:

index.html
<html>
    <head>
        <title>Voci Trainer</title>
    </head>
    <body>
        <h1>Voci Trainer</h1>
        <p>Trainiere <strong>täglich</strong>!</p>
    </body>
</html>

Aufgabe: Statische Webseite

  • Binde grapevine in deinen Code ein.
  • Füge einen Ordner website in den Projekt ein und füge obige HTML-Seite als Datei website/index.html hinzu.
  • Starte den Server mit dotnet run und rufe http://localhost:1234/index.html auf - die Datei sollte im Browser angezeigt werden.

Für eine Webapp wollen wir nicht nur ganze Dateien übertragen, sondern dynamisch auf eine Anfrage (also eine bestimmte URL) reagieren.

Wir müssen also Server-Code schreiben, der weiss, wie er auf bestimmte URLs reagieren soll. Dazu verwenden wir Annotations, die vom Server automatisch erkannt werden. Das zugrundeliegende Muster heisst Representational State Transfer (REST), deshalb heissen die Annotations RestResource und RestRoute.

Der folgende Code wird ausgeführt, wenn die Adresse http://localhost:1234/hello/world aufgerufen wird:

Hello.cs
using System.Net;
using Grapevine;
 
[RestResource]
public class Hello {
    // RestRoute tells the server to reply to requests of the given pattern.
    // "Get": the HTTP-method (normally either "Get" or "Post")
    // "/hello/world": the path to match.
    [RestRoute("Get", "/hello/world")]
    public async Task World(IHttpContext context) {
        await context.Response.SendResponseAsync("Hello Client");
    }
}

Es ist auch möglich, statt eines exakten Pfads ein Muster anzugeben und die passenden Segmente auszulesen. Achtung: in URLs werden Sonderzeichen und Leerschläge speziell codiert, wir müssen die Codierung wieder rückgängig machen:

    // Matches any path of the pattern "/greetings/<anything>"
    [RestRoute("Get", "/greetings/{name}")]
    public async Task Greetings(IHttpContext context) {
        // Extract the name from the URL path.
        // UrlDecode ensures that spaces and special characters are in the right format.
        string name = WebUtility.UrlDecode(context.Request.PathParameters["name"]);
        await context.Response.SendResponseAsync($"Greetings, {name}!");
    }

Aufgabe: Dynamischer Webserver

  • Füge eine neue Klasse wie die obige zu deinem Code hinzu.
  • Teste, ob du den Webserver auf http://localhost:1234/hello/world erreichst, indem du die Adresse mit dem Browser öffnest.
    • Was passiert, wenn du http://localhost:1234/hello/mars aufrufst?
  • Füge eine Route mit Muster und Parametern ein, z.B. http://localhost:1234/greetings/Mr.%20Bond.
  • Sicherheit: überlege dir, wie ein Angreifer Unfug treiben könnte mit unserem Code.

Nun kennen wir die Grundbausteine einer Webapp. Wir möchten nun unseren Voci-Trainer als Web-Schnittstelle zur Verfügung stellen. Verwende zum Start den untenstehenden Code und verbinde ihn mit deinem Voci-Code, um ein zufälliges Wort zurückzugeben.

VociRoutes.cs
using System.Net;
using Microsoft.Extensions.DependencyInjection;
using Grapevine;
 
/**
 * ServiceLifetime.Singleton tells the server to keep the same
 * resource around for the entire time the application runs, instead
 * of creating a fresh resource for every request.
 */
[RestResource]
[ResourceLifetime(ServiceLifetime.Singleton)]
public class VociRoutes {
    // TODO: load the unit from CSV
    private Unit unit;
 
    [RestRoute("Get", "/random")]
    public async Task Random(IHttpContext context) {
        // TODO: Choose a random word from the unit and return in the request.
    }
}

Aufgabe: Web-Schnittstelle für Voci-App

  • Starte mit dem obigen Code und gib ein zufälliges Wort zurück.
  • Teste, ob du den Webserver auf http://localhost:1234/random erreichst. Was passiert beim Reload (Ctrl+R)?
  • Nächster Schritt: baue eine Route ein, um vom Client einen Übersetzungsversuch einzulesen, z.B. auf den Pfad /submit/{word}/{translation}.
    • Die Logik solltest du eigentlich direkt vom bisherigen Konsolenprogramm übernehmen können.

Aufgabe: Statische Webseite

  • Baue eine einfache Webseite mit HTML & CSS für die Voci-App (more details coming).
  • Lerninhalte zu HTML & CSS im Grundlagenfach-Wiki

Aufgabe: Web-App

  • Verknüpfen von Website und dynamischen Inhalten mit Javascript (TBD).

JavaScript einbinden:

index.html
<html>
    <head>
        <title>Voci Trainer</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="voci.css" />
    </head>
    <body>
        <h1>Voci Trainer</h1>
        <div class="words">
            <span id="original"></span>
            <input type="text" id="translation" name="translation" onkeyup="keyup(this, event);">
            <div class="buttons">
                <button type="button" id="submit" onclick="submit();">Eingabe</button>
            </div>
        </div>
        <div class="status">
            <p id="reply"></p>
            <p id="stats"></p>
        </div>
    </body>
    <script src="voci.js" async></script>
</html>

JavaScript Code:

voci.js
/**
 * Function called when the user clicks the submit button or presses Enter.
 */
async function submit() {
    // Get the original and translated words, send them to the server.
    original = document.getElementById("original").innerHTML;
    translation = document.getElementById("translation").value;
    uri = `/submit/${original}/${translation}`;
    let response = await fetch(uri);
 
    let response_text =  await response.text();
    if (!response.ok) {
        // something unexpected happened...
        console.log(`Error: ${response_text} in response to '${uri}'`);
        return;
    }
    // Update the status text.
    document.getElementById("reply").innerText = response_text;
 
    // And fetch the next word.
    next();
}
 
/**
 * Update the stats.
 */
async function updateStats() {
    uri = '/stats';
    let response = await fetch(uri);
    let response_text =  await response.text();
    document.getElementById("stats").innerText = response_text;
}
 
/**
 * Fetches a new word to translate. Called automatically after submit(), and initially for the first
 * word.
 */
async function next() {
    // Ensure the stats are correctly displayed.
    updateStats();
 
    uri = `/random`;
    let response = await fetch(uri);
    let response_text =  await response.text();
    if (!response.ok) {
        console.log(`Error: ${response_text} in response to '${uri}'`);
        return;
    }
    document.getElementById("original").innerText = response_text;
    document.getElementById("translation").value = "";
}
 
/** Called when a key is released within input. */
async function keyup(input, event) {
    var code = event.code;
    if (code == "Enter") {
        submit();
    }
}
 
// Initial call to fetch the first word.
next();
  • ef_informatik/voci_rest.txt
  • Zuletzt geändert: 2022-05-31 13:16
  • von sca