## Wortkartei als REST-Server 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: {{:ef_informatik:pasted:20220522-075444.png?nolink&300}} ### 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 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 Tags versehen wird, um Struktur 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 [[gf_informatik:web:websites|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 {{:ef_informatik:pasted:20220522-075257.png?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**: * 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:pasted:20220522-081524.png?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. * **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. ### Ein Web-Server mit C# # 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: 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: Voci Trainer

Voci Trainer

Trainiere täglich!

#### 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. ### Dynamische Ressourcen 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* ([[wpde>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: 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/" [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. ### Web-Applikation 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. 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 [[gf_informatik:web:start|Grundlagenfach-Wiki]] #### Aufgabe: Web-App * Verknüpfen von Website und dynamischen Inhalten mit Javascript (TBD). ## JavaScript JavaScript einbinden: Voci Trainer

Voci Trainer

JavaScript Code: /** * 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();