Im fünften Artikel der mORMot Vorstellungsreihe werden zwei Komponenten der Bibliothek behandelt. Das eine sind die HTTP-Server Klassen, das andere die Mustache Template-Engine.
Mustache wird als Logic-less bezeichnet, mit der Begründung: "
We call it logic-less because there are no if statements, else clauses, or for loops". Es funktioniert durch die Expandierung von Tags, die Daten - keine, einzelne oder ganze Listen - in einer Vorlage repräsentieren. Mächtig ist dieses Werkzeug bei Erstellung von
HTML für WebApps.
Die enthaltenen Verweise zur Dokumentation verlinken zur aktuell verfügbaren
mORMot1 Hilfe. Für das Beispiel wird
mORMot2 verwendet. Die Namen für Klassen und Funktionen können sich leicht unterscheiden. Im Anhang befindet sich der Sourcecode und das ausführbare Programm.
Disclaimer: Der Sourcecode ist weder getestet noch optimiert. Er sollte mit Delphi ab Version 10.4 funktionieren. Die Benutzung der zur Verfügung gestellten Materialien erfolgt auf eigene Gefahr.
Die Beispiel-Anwendung ist ein einfacher Editor. Je nach Compiler-Direktive wird die Komponente TMemo oder TSynEdit verwendet. Die Eingabe wird mit Hilfe des mORMot Mustache Renderers aufbereitet und über den integrierten HTTP-Server als
HTML Seite im Edge-Browser ausgegeben. Mit dem Beispiel Quelltext für das Programm bekommt man:
- Einen integrierten HTTP-Server für HTTP/1.0, HTTP/1.1, HTTPS mit selbstsignierten oder eigenem Zertifikat.
- Den Edge-Webbrowser für die Anzeige von HTML Seiten mit abdockbaren Stand-alone Fenster.
- Das direkte Linken von Images, CSS, JavaScript aus ZIP-Dateien in die HTML Seite.
- Einen einfachen Mustache Editor mit Autovervollständigung für gängige HTML Tags.
Vorwissen
In mORMot sind HTTP-Server und REST-Server getrennt, zwei Konzepte, die jedes für sich eigenständig genutzt werden können. Für die Implementierung eines HTTP-Server stehen die Klassen
THttpServer,
THttpAsyncServer und
THttpApiServer zur Verfügung. Beim
THttpServer handelt es sich um einen Socket-basierten Server. Er verwendet einen Thread-Pool für kurzlebige HTTP/1.0 Anfragen und einen Thread pro Verbindung bei HTTP/1.1. Der
THttpAsyncServer ist vollständig ereignisgesteuert und in der Lage, Tausende von gleichzeitigen Verbindungen zu bewältigen. Ausschließlich unter Windows steht der
THttpApiServer auf Basis des http.sys Modules zur Verfügung. Dieses Modul kommt auch im IIS und .NET zum Einsatz. Die Socket-basierten Server unterstützen TLS nativ. Wenn vorhanden, wird OpenSSL verwendet, andernfalls die SChannel-
API von Windows. Wer sich für mehr interessiert, dem sind die beiden Artikel
New Async HTTP/WebSocket Server on mORMot 2 und
Native TLS Support for mORMot 2 REST or WebSockets Servers empfohlen.
HTTP-Server
Für unser Beispiel wird die Klasse
THttpServer verwendet. Der Betriebsmodus des Servers kann mit Kommandozeilenschalter oder per Auswahldialog bestimmt werden. Zur Auswahl stehen: HTTP/1.0, HTTP/1.1, HTTPS mit selbstsignierten Zertifikat oder, wenn vorhanden, mit eigenem Zertifikat. Der Schalter für die Kommandozeile ist
-sm mit den Optionen
hsmHttp10|
hsmHttp11|
hsmHttpsSelf.
Die Rückruffunktion, in der alle Prozesse auf höherer Ebene zusammenkommen, ist der Event
OnRequest. Der Prototyp ist in der
Unit mormot.net.http wie folgt definiert:
Delphi-Quellcode:
type
TOnHttpServerRequest = function(Ctxt: THttpServerRequestAbstract): cardinal of object;
Über die Klasse THttpServerRequest werden Eingabeparameter übernommen und die Ausgabeargumente, wie zum Beispiel Header, Body und Statuscode, mit Inhalt befüllt, die dann als Antwort gesendet werden. Konkret, man nimmt die Eingabe-
URI und zerlegt sie in ihre Teile. Hier verwenden wir Pfad (path) und Abfrage (
query). Im Gegensatz zu den REST-Servern,
Methoden- oder
Interface-based, gibt es kein automatisches Routing und muss selbst erledigt werden. Die Selektoren im Beispiel sind "
INDEX.HTML" und "
.ZIP/". Mehr Spezialisierung ist nicht notwendig. Der Rest wird einer Standard Funktion zugeführt.
Delphi-Quellcode:
function TFileServer.DoRequest(pmCtxt: THttpServerRequestAbstract): Cardinal;
var
urlPath: RawUtf8;
urlQuery: RawUtf8;
begin
if not SplitUrlSchemePathAndQuery(pmCtxt.Url, urlPath, urlQuery) then Exit(HTTP_BADREQUEST); //=>
if IdemPChar(PUtf8Char(Pointer(urlPath)), DEFAULTHTML_FILENAME) then
Result := DoDefaultFileRequest(pmCtxt, urlPath)
else if PosI(ZIPFILE_SEPERATOR, urlPath) > 0 then
Result := DoZipFileRequest(pmCtxt, urlPath)
else
Result := DoFileRequest(pmCtxt, urlPath);
end;
Standard deshalb, weil in den
THttp*Server Klassen ein optimiertes Verfahren für die Übertragung von Dateien implementiert ist. Der Mechanismus aktiviert sich, wenn
OutContentType die Konstante
STATICFILE_CONTENT_TYPE zugewiesen wird. Des Weiteren wird an
OutContent der Dateinamen übergeben und zum Abschluss der passende
OutCustomHeaders gewählt.
Delphi-Quellcode:
function TFileServer.DoFileRequest(pmCtxt: THttpServerRequestAbstract; const pmcUriPath: RawUtf8): Cardinal;
var
fileName: TFileName;
begin
Utf8ToFileName(pmcUriPath, fileName);
if CheckFileName(fileName, [fnvFileName], @fileName) then
begin
fileName := MakePath([Executable.ProgramFilePath, ASSETS_FOLDER, fileName]);
pmCtxt.OutContent := StringToUtf8(fileName);
pmCtxt.OutContentType := STATICFILE_CONTENT_TYPE;
pmCtxt.OutCustomHeaders := STATICFILE_CONTENT_TYPE_HEADER + #13#10 + GetMimeContentTypeHeader('', fileName);
Result := HTTP_SUCCESS;
end
else
Result := HTTP_NOTFOUND;
end;
Im Artikel wird der
Query-Teil einer
URI nicht benutzt. Trotzdem möchte ich an einem Beispiel zeigen, wie er sich in seine einzelnen Teile zerlegen lässt. Es werden einige Low-Level-Funktionen genutzt, die für diese Aufgabe in der Bibliothek vorhanden sind. In REST-Servern steht über die Klasse
TRestServerUriContext höher spezialisierte Funktionen zur Verfügung. Die Reihenfolge der Parameter ist beliebig. Besonders zu beachten ist nur die
letzte UrlDecode* Funktion (ganz genau hinsehen!) mit der Übergabe der Adresse.
Delphi-Quellcode:
const
PARAMS: RawUtf8 = 'u=Text&i=10&d=8.8';
var
p: PUtf8Char;
u: RawUtf8;
i: Integer;
d: Double;
begin
p := PUtf8Char(Pointer(PARAMS));
if UrlDecodeNeedParameters(p, 'U,I,D') then
begin
repeat
UrlDecodeValue(p, 'U=', u);
UrlDecodeDouble(p, 'D=', d);
UrlDecodeInteger(p, 'I=', i, @p);
until p = Nil;
ShowMessage(Utf8ToString(FormatUtf8('String: %'#13#10'Interger: %'#13#10'Double: %', [u, i, d])));
end;
Die Instanziierung eines HTTP-Servers ist mit wenigen Zeilen erledigt:
Delphi-Quellcode:
FHttpServer := THttpServer.Create({Port=} '8080', Nil, Nil, '', {ServerThreadPoolCount=} 2, {KeepAliveTimeOut=} 0);
FHttpServer.WaitStarted(WAIT_SECONDS);
Mehr ist nicht zeigbar, das war es schon. Ein HTTP/1.0 Server der auf Port 8080 mit maximal 2 Threads läuft. Zeigte ich, wie einfach TLS zu aktivieren wäre, könnten Nutzer anderer Bibliothek
. Dieser Verantwortung möchte ich mich nicht stellen.
Mustache Template-Engine
Die Template-Sprache besteht nur aus wenigen Elementen, es sind
Variables,
Sections und
Partials. Alle Mustache Tags werden in doppelte geschweifte Klammern
{{...}} eingeschlossen. Eine Variable FirstName wird somit wie folgt kodiert:
{{FirstName}}. Eine Sektion repräsentiert einen geladenen Kontext. In der Praxis ein JSON Objekt oder Array. Eine Sektion vom Typ "if ..." wird mit
{{#...}}, ein "if not ..." mit
{{^...}} eingeleitet. Sektionen sind zwingend mit
{{/...}} zu beenden. Ist der Kontext eine Liste, wird diese vom ersten bis letzten Element durchlaufen und angewandt. Die Sektion ist auch der Root für die Variablen. Beispiel: Eine Liste mit Personendaten (Objekte) soll durchlaufen und der Name (Variable) jeder Person ausgegeben werden. Lösung:
{{#Persons}}{{Name}}<br>{{/Persons}}. Außerhalb einer Sektion muss für Variablen der volle Pfad angeben werden. Für den Kontext Person:
{{Person.Name}}. Partials entsprechen Pascal Include-Dateien. Alles Wichtige ist hier nachzulesen:
Mustache template engine. Grau ist alle Theorie - so sieht es in der Praxis aus:
Code:
<!-- Hero is loaded from the Partials folder -->
{{>Hero}}
{{#Person}}
<!-- CSS class h3 is defined in Bootstrap -->
<p class="h3">Person data</p>
<ul>
<li>Name: {{FirstName}} {{LastName}}</li>
<!-- CurrToText is a self-written Expression Helper to format money output -->
<li>Income: {{CurrToText Income}}</li>
</ul>
{{/Person}}
{{^Person}}
<p>If this is displayed, the Person data object was not found.</p>
{{/Person}}
Cheers {{Person.FirstName}}
Die Sprache lässt sich mit eigenen Funktionen (Expression Helpers) erweitern. Diese werden in einer Liste vom Typ
TSynMustacheHelpers registriert und dem Prozessor übergeben. Ihr Aufbau ist wie folgt:
Delphi-Quellcode:
procedure TFileServer.CurrToText(const pmcValue: Variant; out pmoResult: Variant);
begin
pmoResult := CurrToStrF(pmcValue, ffCurrency, 2);
end;
Mit diesem Vorwissen ausgestattet, jetzt zur konkreten Implementierung in der Anwendung. Für die Anzeige im Edge-Webbrowser bauen wird die
HTML Seite zusammen. Der Teil für den Mustache Renderer umfasst nur wenige Zeilen:
Delphi-Quellcode:
var
writer: TTextWriter;
contextData: TDocVariantData;
begin
...
PrepareMustachePartials;
PrepareMustacheContextData(pmcProjectData.MustacheDataFiles, contextData);
writer.AddString(TSynMustache.Parse(pmcProjectData.Mustache).Render(Variant(contextData), FMustachePartials, FMustacheHelpers));
Das Geniale an einem DocVariant ist, dass es eine beliebig komplexe Datenstruktur aus Objekt(en) und/oder Arrays, oder aus Kombinationen von beiden sein kann. Nur der zur Verfügung stehende Arbeitsspeicher ist die Grenze. Die Zusammenstellung der Kontext-Daten ist ein anschauliches Beispiel für die einfache Handhabung:
Delphi-Quellcode:
procedure TFileServer.PrepareMustacheContextData(const pmDataFiles: TRawUtf8DynArray; out pmoContextData: TDocVariantData);
var
context: TDocVariantData;
contextName: RawUtf8;
begin
pmoContextData.Init;
var assetsFolder: TFileName := MakePath([Executable.ProgramFilePath, ASSETS_FOLDER], True);
var contextFiles: TFileNameDynArray := FileNames(assetsFolder, CONTEXTFILE_SEARCHMASK, [ffoExcludesDir]);
for var i: Integer := 0 to High(contextFiles) do
begin
if FindPropName(pmDataFiles, StringToUtf8(contextFiles[i])) < 0 then Continue; //->
context.InitJsonFromFile(assetsFolder + contextFiles[i], JSON_FAST_FLOAT);
contextName := StringToUtf8(GetFileNameWithoutExt(contextFiles[i]));
pmoContextData.AddValue(contextName, Variant(context));
context.Clear;
end;
end;
Am Ende sind alle JSON Daten in einen DocVariant geladen und werden als Paket an den Mustache Renderer übergeben. WOW.
Der Mustache Editor: kurz und bündig- Für die Anzeige wird der Edge-Webbrowser verwendet. Mit der Funktionstaste F5 wird die Anzeige aktualisiert. Mit F6 werden die Projektdaten gespeichert. Wurde ein Projektname vergeben, wird sofort gespeichert.
- Images, CSS, JavaScript, JSON oder ZIP-Dateien werden nur aus dem Assets Verzeichnis geladen. Die Ordnerstruktur/Verzeichnistiefe in einer ZIP-Datei ist nicht beschränkt.
- Partials für den Mustache Renderer können nur aus dem Partials Verzeichnis geladen werden. Partials entsprechen Pascal Include-Dateien.
- Die Bootstrap Dateien bootstrap.min.css und bootstrap.bundle.min.js werden immer hinzugefügt.
- Externe Dateien werden in Reihenfolge der Aufführung geladen. Möglich ist, die Anordnung der Bootstrap Dateien durch Eingabe von *.css oder *.js in der Reihung zu bestimmen.
- JavaScript Dateien werden mit Option defer geladen. Dieses Verhalten lässt sich beeinflussen. Durch Voranstellung eines ! direkt am Dateinamen wird sofort geladen. Bei Voranstellung eines ? wird mit der Option async geladen.
- Der Root für geladene JSON Daten ist der Dateiname. Das JSON Objekt aus der Datei Person.json wird mit Person.FirstName angesprochen.
- Die Autovervollständigung für den Mustache Editor deckt gängige HTML Tags ab. Die Definitionen sind in der Datei HtmlAutoComplete.dci abgelegt.
- Das Fenster des Browsers lässt sich abdocken und als Stand-alone Ansicht platzieren.
- Alles Weitere ist dem Spieltrieb der Nutzer überlassen...
Zusammenfassung
Der heutige Artikel hat erste Blicke auf einen zentralen Baustein, die Phalanx der HTTP-Server, der mORMot Bibliothek geworfen. Die enorme Leistungsfähigkeit dieser Server Klassen sind das Fundament für die Mächtigkeit der nachlaufenden RestServer. Als Werkzeug eröffnet die Mustache Template Engine, mit einer einfach zu erlernenden Sprache, die Möglichkeit, eine Vorlage und JSON Daten zur Aufbereitung zu verbinden. Sie ist Basis der
Unit mormot.rest.mvc, die auf Grundlage des Model-View-Controller Patterns die Erstellung von Web-Apps ermöglicht.
mORMot ist gut dokumentiert. Die Hilfe umfasst mehr als 2500 Seiten. Davon enthalten die ersten ca. 650 Seiten einen sehr lesenswerten allgemeinen Teil, der Rest ist
API Dokumentation. mORMot muss nicht in der
IDE installierten werden! Es reicht aus, die entsprechenden Bibliothekspfade einzufügen. Es stehen viele Beispiele und ein freundliches
Forum zur Verfügung. Wenn mehr Interesse an mORMot besteht, kann ich auch andere Teile in ähnlicher Weise kurz vorstellen.
In der mORMot Reihe bisher veröffentlicht:
- ORM und DocVariant kurz vorgestellt
- ZIP-Datei als Datenspeicher mit verschlüsseltem Inhalt
- Einführung in methodenbasierte Services - Server und Client
- Einführung in Interface-based Services - Server und Client
Bis bald...
Thomas