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