bake.js

Hierbei handelt es sich um eine Art portabler JavaScript-Werkzeugkette. Programme können modular geschrieben und über bake "gebacken", also zu einer kompakten Datei in optimierter Form zusammengefasst werden. Die Quell-Dateien sind in zwei Arten von Abschnitten unterteilt: Präprozessor und Ergebnis bzw. Back-Zeit und Lauf-Zeit. Die Unterteilung ist von Prinzip und Umsetzung her mit rtjscomp identisch!

Motivation

Um im WWW die Inkompatiblität zwischen Netzdurchsuchern zu überbrücken, werden in der Regel Frameworks verwendet. Dies sind große und komplexe JavaScript-Bibliotheken, die auf Besucher-Seite ausgeführt werden. Es werden also jedem Netzdurchsucher die gleichen Daten geschickt und das Programm erkennt erst im jeweiligen Netzdurchsucher, worin es sich befindet und was der Netzdurchsucher unterstützt und was nicht. Das ist eine riesige unnötige Belastung für das Netzwerk und für die Besucher-Rechner. Ich möchte dies umgehen, indem jedem Benutzer ein auf sein Netzdurchsucher hin optimiertes Programm geschickt wird. Er erhält also nur eine Datei, die gerade ausreicht, um auf gerade dieser Plattform das Ziel zu erreichen. Das bedeutet zwar einen Mehraufwand auf Anbieter-Seite, aber der lässt sich vernachlässigen, da es in der Praxis nur wenige verschiedene Ziele zum selben Zeitpunkt gibt und Ergebnisse zwischen-gespeichert werden können.

Funktionsweise

Ein Präprozessor sammelt zuerst den zu backenen Teig ein und verarbeitet ihn grob. Der gesamte Teig wird zusammengefasst und dann optional (und standartmäßig) durch Googles Closure-Compiler gegeben. Der davon ausgegebene, optimierte Text wird dann noch etwas weiter gestutzt und schließlich ausgegeben.

Beispiel-Programm "Demonstration"

Dieses Programm diente mir bisher als einfacher Test, ob der Bäcker funktioniert. An ihm lassen sich einige Grundfunktionalitäten erklären.

Quell-Datei Demonstration.cjs

#!/usr/bin/env bake
<?

bake_global_name='window';
bake_output_html_enabled=true;
bake_output_ending='html';
//bake_gcc_enable=false;

?><?$bake_output_html_head?><title>Demonstration mit bake.js <?=bake_version?></title><?$?><?

include('test');

?>

var alert=__pseudo__window['alert'];

alert('Hallo!');
alert('<?=test?>');
alert('<?=456?>');

Quell-Datei test.ijs

<?
vars.test=123;
?>

Ablauf

Zum Backen wird (in einer POSIX-Umgebung) die cjs-Datei als Programm aufgerufen. Die erste Zeile bewirkt dann, dass die Datei mit bake geöffnet wird.

Die cjs-Datei beginnt gleich am Anfang mit Back-Anweisungen. Die Zeichen <? eröffnen einen solchen Bereich. Der nachfolgende JavaScript-Text bis ?> wird vom Bäcker ausgeführt. Es werden mehreren globalen Variablen Werte zugewiesen. Globale Variablen sind im vars-Objekt enthalten und können beim Aufrufen des Bäckers als Programm-Parameter übergeben werden. Das vars-Objekt muss aber nur bei der erstmaligen Deklaration einer Variablen angegeben werden.

Die globalen Variablen, die mit bake_ beginnen, gehören zum Bäcker. Über sie lassen sich Umgebungsdetails verarbeiten oder manipulieren.

Eine solche Variable ist auch bake_output_html_head. Ist das Ziel eine HTML-Datei, so kann hier angegeben werden, was im head-Bereich landen soll. Die <?$...?>-Anweisung dient dazu, den nachfolgenden Text um zu leiten, also in einer bestimmten Variable zu speichern. Die Versionsnummer wird dann wieder dynamisch mit <?=...?> eingefügt und die Umleitung des Text-Stroms mit <?$?> wieder aufgehoben. Momentan wird HTML-Text im Gegensatz zu JavaScript nicht optimiert und deswegen habe ich diesen hier auch so kompakt geschrieben.

Danach wird include aufgerufen. Diese Funktion sucht nach einer passenden Datei und bindet sie hier und jetzt ein. Der Inhalt von test.ijs wird also praktisch an dieser Stelle eingefügt. Dies hat den großen Vorteil, dass sich größere Programme unkompliziert über beliebig viele Dateien verteilen lassen.

In der ijs-Datei wird nur die globale Variable test auf 123 gesetzt.

Danach (wieder in der cjs-Datei) wird der Back-Bereich verlassen. Die nachfolgenden Anweisungen sind die eigentlichen Anweisungen, die sich im späteren Brot wiederfinden lassen. Es wird dreimal die alert-Funktion aufgerufen, also im Netzdurchsucher-Fenster ein Meldungsfenster angezeigt. Da für dieses Beispiel das base-Modul bewusst weg-gelassen wird, muss alert hier manuell definiert werden. Das erste Fenster gibt den fest eingeschriebenen Text "Hallo!" aus. Das zweite die Back-Variable test, das dritte den berechneten Wert 456.

Ergebnis-Datei Demonstration.html

<!doctype html><html><head><title>Demonstration mit bake.js 0.48</title></head><body><script>var a=window.alert;a("Hallo!");a("123");a("456")</script></body></html>

Beispiel-Programm "Blokker"

Den bake-Vorgänger seng der 4. Generation habe ich auch besonders für ein Schulprojekt erschaffen, in dessen Rahmen ich eine viel bessere Variante meines alten Spiele-Klassikers Blokker schaffen wollte. Das Projekt konnte ich nicht ganz fertigstellen, aber das schon erreichte habe ich hier veröffentlicht. Ich bekam auch nur sehr wenige Punkte für das Projekt, da ich zuvor ein spielbares Spiel in Ausschau stellte. Die gelieferte Menüstruktur alleine wäre sicherlich zu wenig gewesen und die ganze Struktur im Hintergrund (an der ich natürlich mit großem Abstand am meisten arbeitete) wollte ich nicht zum Teil der Abgabe machen, um zu verhindern, dass ich die Rechte an Teilen meines wichtigsten Projektes verliere. Ich habe versucht, meine Arbeit gut zu präsentieren, aber (wie erwartet) hat das an dem Ergebnis auf Papier wenig geändert. Ich bereue natürlich, nicht etwas anspruchsloseres gemacht zu haben (was erwiesenermaßen wunderbar funktioniert hätte), aber jetzt weiß ich, dass sich die Arbeit dennoch gelohnt hat.

Vorab zu beachten gilt, dass dieses Programm mit einem frühen Vorgänger von bake gebacken worde. Seitdem hat sich schon viel bei bake geändert. Dennoch beachte man, wie sehr das Benutzer-Erlebnis eher Adobe Flash ähnelt, als einfacher HTML. Die Menü-Größe wird dynamisch an der Fenstergröße angepasst. Programmierfehler sind mir keine bekannt.

Das Programm "Blokker" habe ich universell, also Plattform-agnostisch geschrieben. Dann habe ich den Bäcker für zwei unterschiedliche Plattformen backen lassen:

Öffnet man eine der Dateien in einer ungeeigneten Plattform, wird es sicherlich Fehler verschiedener Art geben. Der gewöhnte Benutzer erwartet zwar, dass er die Datei irgendwo einfach öffnen kann und sie sich genau gleich verhalten würde. Doch das stimmt nur für das ursprünglich angegebene Programm (Teig), nicht aber für die jeweils generierten Ergebnisdateien. Lässt sich eine Ergebnis-Datei auch in einem anderen Ziel problemlos ausführen, so ist dies ein mehr oder weniger großer Zufall.

Beispielsweise habe ich für beide hier angebotenen Varianten als Eingabe ausschließlich Tastatur und eine Maus angegeben. Bedient man diese Varianten mit einem Berührungs-empfindlichen Bildschirm, so könnte unerwartetes Verhalten auftreten.

Ziele lassen sich sehr vielfältig angeben. Sie lassen sich so streng wie hier angeben, sodass die Umgebungen, in denen sie sich ausführen lassen, zahlenmäßig stark begrenzt sind. Es gibt auch ein Umgebungsprofil, das mit fast allen funktioniert, dafür aber weniger effiziente Ergebnisse erzeugt. Schön wäre auch eine automatische Erkennung zur Laufzeit, aber das wäre mir hiermit zu aufwändig. Ich habe schon ein Konzept, wie das funktionieren könnte, aber sehe noch keinen Grund diesen Aufwand auf mich zu nehmen. Auch dann wären die Dateien größer als Maß-Anfertigungen, aber dafür wird beim Programmstart automatisch erkannt, ob eine Maus bzw. ob eine Berührungs-Eingabe angeschlossen ist. So lässt sich auf Wunsch ein maximal offenes Ziel angeben, das zwar eine viel größere Datei ausgibt und wahrscheinlich minimal langsamer ist, dafür aber in jedem Netzdurchsucher funktioniert. Damit wäre auch das klassische Anwendungs-Gebiet der Netz-Frameworks erschlossen.

Der Bäcker ist bei weitem nicht auf einfaches HTML begrenzt. Er kann auch für Node.js oder Windows Script Host backen. Dafür gibt es eine einheitliche Konsolen-Ausgabe-Schnittstelle. Zur Feier des Tages hier noch eine weitere Variante: MSHTA (Debug). In dieser Variante werden zusätzlich technische Details angezeigt. Es hat mich keinerlei zusätzlichen Aufwand gekostet, diese weitere Variante zu generieren. Ich habe nur beim Backen den Diagnose-Modus aktiviert.

Dass sich Programme einheitlich für die verschiedensten Ziele generieren lassen, hat neben verschiedenen Netzdurchsuchern noch einen riesigen Vorteil. Beispielsweise lässt sich Blokker auch für Node.js generieren, um so einen Anbieter für vernetzte Spiele zu erhalten. Und ich könnte relativ einfach den Bäcker in rtjscomp integrieren, aber das hier noch zu erklären, würde den Rahmen der Vorstellungskraft sprengen.

Quell-Datei client.html5.cjs

Achtung! Diese Dateien sind alt und wurden für seng geschrieben, welches noch einen anderen, primitiveren Präprozessor (noch kein JavaScript) verwendet hat. Die Dateien gelten also nur dem Überblick. Unterschiede habe ich unterstrichen.

//Blokker-Spielerprogramm

//:debug=false
//:env=0

//:graph_canvas_performance_watch=false
//:graph_canvas_performance_subpixel_simulation=false
//:graph_input_cursor_mode=2
/** @const */var preset_blokker_mode=0;
//!include base

Quell-Datei client.hta.cjs

//Blokker-Spielerprogramm

//:debug=false
//:env=4

//:graph_canvas_performance_watch=false
//:graph_canvas_performance_subpixel_simulation=false
//:graph_input_cursor_mode=2
/** @const */var preset_blokker_mode=0;
//!include base

env kann folgende Werte haben: 0: Node.js; 1: HTML automatisch; 2: HTML5; 3: MSHTML; 4: MSHTA; 5: HTML4

Man sieht am Dateinamen client schon, dass diese Dateien die oben verknüpfte Benutzer-Variante generieren. Blokker wird über preset_blokker_mode mitgeteilt, ob es die Anbieter- oder Benutzer-Variante ist. In server.node.cjs stünde also etwa preset_blokker_mode=1.

Quell-Datei client.hta.debug.cjs

Zur Hilfe des Entwicklers gibt es einen sogenannten Kontrollmodus, bei dem eine erweiterte Kontrolle möglich ist. Um ihn zu aktivieren, wird debug auf true gesetzt.

//Blokker-Spielerprogramm

//:debug=true
//:env=4

//:graph_canvas_performance_watch=false
//:graph_canvas_performance_subpixel_simulation=false
//:graph_input_cursor_mode=2
/** @const */var preset_blokker_mode=0;
//!include base

Zum Kontrollmodus habe ich damals auch gleich eine Anleitung verfasst:

Ist der Kontrollmodus aktiviert, so können auf dem Bildschirm interne Informationen angezeigt werden und über Tastenkombinationen Befehle übergeben werden. Außerdem wird auf Fehler geprüft und im Falle gemeldet und abgebrochen.

Aufbau der Informationsanzeige:
Frame: Nummer des Einzelbildes, in Hexadezimal
Duration: Anzeigedauer des letzten Einzelbildes
Time: Laufzeit des Programms in Millisekunden, in Hexadezimal
FPS: Erreichte Einzelbilder pro Sekunde (Bildfrequenz)
Objects: Anzahl an im Bild gezeichneten Primitiven (Rechteck, Polygon, Linie)
Subpixel: Gibt an, ob die Subpixel-Simulation aktiviert ist oder nicht
Cursors: Gibt die Anzahl an verfügbaren Mauszeigern und den Erkennungsmodus an
Keys: Listet aktuell gedrückte Tasten auf

Danach folgt der hintere Ausschnitt des Logbuchs, soweit aktiviert.

Tastenkombinationen:
Wenn der Kontrollmodus aktiv ist, können, solange die AltGr-Taste gedrückt gehalten wird, über andere Tasten Befehle eingeben werden:

ESC: Programm beenden
F1: Informationsanzeige an/aus
F5: Programm neu starten
P: Programm pausieren
S: Subpixel-Simulation an/aus
T: Zeitfaktor festlegen

Technischer Hintergrund

Wie bisher fast alle meine Programme, ist auch bake.js komplett in JavaScript geschrieben. Das lässt die Namensendung aber schon vermuten. Und wie die meisten meiner rein Text-basierten Programme wird bake.js wiederum mit Node.js ausgeführt. Im Programm-Verzeichnis befindet sich ein Modul-Verzeichnis, in dem alle nutzbaren Module enthalten sind. Im Teig kann mit der require-Anweisung ein Modul eingebunden werden. Diese Methode hat nichts mit der zu tun, die Node.js liefert. Das Zusammenfügen der Dateien schafft der Bäcker von selbst. Zum Optimieren wird jedoch das Paket closure-compiler benötigt.

Bezugsmöglichkeiten

Wer bis hierher gelesen hat, würde es wohl gern selbst mal ausprobieren. Tatsächlich habe ich eine Funktions-tüchtige bake-Installation. Das ganze ist aber noch nicht annähernd so weit, dass ich es mit gutem Gewissen veröffentlichen könnte. Also gilt es, sich vorerst zu gedulden. Ansonsten kann man mich gern anschreiben und weitere Fragen direkt stellen.

Und weiter?

bake.js ist nur ein Mittel zum Zweck für mich, genau wie auch das Geschwister-Projekt rtjscomp. Ich möchte in bake eine eigene, grafische Programmier-Technik realisieren. Zunächst sollen die darin deklarierten Programme in bake interpretiert ausgeführt werden. Dann möchte ich darin einen eigenen Kompilierer realisieren, wahrscheinlich wird LLVM- oder Rust-Text erzeugt.