Einzelnen Beitrag anzeigen

mytbo

Registriert seit: 8. Jan 2007
472 Beiträge
 
#1

mORMot: Mustache Editor mit integriertem HTTP-Server zum Anzeigen von HTML Seiten

  Alt 30. Jul 2022, 00:41
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:
  1. ORM und DocVariant kurz vorgestellt
  2. ZIP-Datei als Datenspeicher mit verschlüsseltem Inhalt
  3. Einführung in methodenbasierte Services - Server und Client
  4. Einführung in Interface-based Services - Server und Client
Bis bald...
Thomas
Angehängte Dateien
Dateityp: zip TestMustacheEditorSource.zip (242,0 KB, 45x aufgerufen)
Dateityp: zip TestMustacheEditorExe.zip (1,95 MB, 44x aufgerufen)
  Mit Zitat antworten Zitat