Ich spreche viel über Static Site Generatoren, aber immer über *die Verwendung* von Static Site Generatoren. In den meisten Fällen mag es wie eine Black Box erscheinen. Ich erstelle eine Vorlage und etwas Markdown und heraus kommt eine vollständig ausgebildete HTML-Seite. Magie!
Aber was genau *ist* ein Static Site Generator? Was passiert in dieser Black Box? Was für ein Voodoo ist das?
In diesem Beitrag möchte ich alle Teile untersuchen, aus denen ein Static Site Generator besteht. Zuerst werden wir diese im Allgemeinen diskutieren, aber dann werden wir uns einige tatsächliche Codes genauer ansehen, indem wir tief in HarpJS eintauchen. Also, setzen Sie Ihren Abenteurerhut auf und lassen Sie uns mit der Erkundung beginnen.
Warum Harp? Aus zwei Gründen. Der erste ist, dass HarpJS per Design ein sehr einfacher Static Site Generator ist. Er hat nicht viele Funktionen, die uns dazu verleiten könnten, uns in einem umfassender ausgestatteten Static Site Generator zu verirren (wie zum Beispiel Jekyll). Der zweite, viel praktischere Grund ist, dass ich JavaScript kann und Ruby nicht sehr gut.
Die Grundlagen eines Static Site Generators
Die Wahrheit ist, dass ein Static Site Generator ein ziemlich einfaches Konzept ist. Die Schlüsselzutaten für einen Static Site Generator sind typischerweise:
- Eine oder mehrere Vorlagensprachen zum Erstellen von Seiten-/Beitragsvorlagen
- Eine leichtgewichtige Auszeichnungssprache (typischerweise Markdown) zum Verfassen von Inhalten
- Eine Struktur und Auszeichnung (oft YAML) zur Bereitstellung von Konfiguration und Metadaten (z. B. „Front Matter“)
- Ein Satz von Regeln oder eine Struktur für die Organisation und Benennung von Dateien, die exportiert/kompiliert werden, Dateien, die dies nicht tun, und wie diese Dateien behandelt werden (z. B. bedeutet häufiges Voranstellen einer Datei oder eines Ordners mit einem Unterstrich, dass sie nicht in die endgültigen Site-Dateien exportiert wird oder alle Beiträge in einem Beitragsordner landen)
- Eine Möglichkeit, Vorlagen und Auszeichnungen in HTML zu kompilieren (häufig ist auch Unterstützung für CSS- oder JavaScript-Präprozessoren enthalten)
- Ein lokaler Server zum Testen.
Das ist alles. Wenn Sie denken: „Hey… das könnte ich bauen!“, dann haben Sie wahrscheinlich Recht. Die Dinge werden kompliziert, wenn man anfängt, die Funktionalität zu erweitern, wie es die meisten Static Site Generatoren tun.
Schauen wir uns also an, wie Harp damit umgeht.
Auf den Kern von Harp kommen
Werfen wir einen Blick auf die Grundlagen, wie Harp die oben beschriebenen Schlüsselzutaten handhabt. Harp bietet mehr als diese Handvoll an Funktionalität, aber der Einfachheit halber werden wir uns auf diese Punkte beschränken.
Zuerst sprechen wir über die Grundlagen von Harp.
Harp Grundlagen
Harp unterstützt Jade und EJS (für Templating) und Markdown als seine leichtgewichtige Auszeichnungssprache (für Inhalte). Beachten Sie, dass Jade jetzt Pug heißt, Harp aber in seiner Dokumentation oder seinem Code noch nicht offiziell umgestellt hat, daher bleiben wir hier bei Jade. Harp bietet auch Unterstützung für andere Präprozessoren wie Less, Sass und Stylus für CSS und CoffeeScript für JavaScript.
Standardmäßig erfordert Harp nicht viel Konfiguration oder Metadaten. Es bevorzugt Konvention vor Konfiguration. Es ermöglicht jedoch spezifische Metadaten und Konfigurationen mit JSON. Es unterscheidet sich von vielen anderen Static Site Generatoren darin, dass Dateimetadaten außerhalb der eigentlichen Datei in einer `_data.json`-Datei enthalten sind.
Obwohl es bis zu einem gewissen Grad konfigurierbar ist, hat Harp bestimmte festgelegte Richtlinien für die Strukturierung von Dateien. Zum Beispiel sind in einer typischen Anwendung die zu bedienenden Dateien im Verzeichnis `public` enthalten. Außerdem wird jede Datei oder jeder Ordner, dem ein Unterstrich vorangestellt ist, nicht bedient.
Zuletzt bietet Harp einen einfachen lokalen Webserver zum Testen mit einigen konfigurierbaren Optionen. Und natürlich kompiliert er die fertigen HTML-, CSS- und JavaScript-Dateien für die Bereitstellung.
Werfen wir einen Blick auf den Quellcode von Harp
Da ein großer Teil dessen, was einen Static Site Generator ausmacht, Regeln und Konventionen sind, dreht sich der Code hauptsächlich um das eigentliche Bereitstellen und Kompilieren. Tauchen wir ein.
Die Server-Funktion
In Harp wird Ihr Projekt normalerweise durch Ausführen von `harp server` von der Befehlszeile aus bereitgestellt. Werfen wir einen Blick auf den Code für diese Funktion.
exports.server = function(dirPath, options, callback){
var app = connect()
app.use(middleware.regProjectFinder(dirPath))
app.use(middleware.setup)
app.use(middleware.basicAuth)
app.use(middleware.underscore)
app.use(middleware.mwl)
app.use(middleware.static)
app.use(middleware.poly)
app.use(middleware.process)
app.use(middleware.fallback)
return app.listen(options.port || 9966, options.ip, function(){
app.projectPath = dirPath
callback.apply(app, arguments)
})
}
Obwohl die Funktion einfach aussieht, passiert offensichtlich eine Menge in der Middleware, die hier nicht dargestellt wird.
Der Rest dieser Funktion öffnet einen Server mit den von Ihnen angegebenen Optionen (falls vorhanden). Zu diesen Optionen gehören ein Port, eine IP-Adresse, an die gebunden werden soll, und ein Verzeichnis. Standardmäßig ist der Port 9000 (nicht 9966, wie Sie vielleicht aufgrund des Codes vermuten), das Verzeichnis ist das aktuelle (d. h. das, in dem Harp läuft) und die IP-Adresse ist `0.0.0.0`.
Die Compiler-Funktion
Bleiben wir in index.js und schauen uns als nächstes die `compile`-Funktion an.
exports.compile = function(projectPath, outputPath, callback){
/**
* Both projectPath and outputPath are optional
*/
if(!callback){
callback = outputPath
outputPath = "www"
}
if(!outputPath){
outputPath = "www"
}
/**
* Setup all the paths and collect all the data
*/
try{
outputPath = path.resolve(projectPath, outputPath)
var setup = helpers.setup(projectPath, "production")
var terra = terraform.root(setup.publicPath, setup.config.globals)
}catch(err){
return callback(err)
}
/**
* Protect the user (as much as possible) from compiling up the tree
* resulting in the project deleting its own source code.
*/
if(!helpers.willAllow(projectPath, outputPath)){
return callback({
type: "Invalid Output Path",
message: "Output path cannot be greater then one level up from project path and must be in directory starting with `_` (underscore).",
projectPath: projectPath,
outputPath: outputPath
})
}
/**
* Compile and save file
*/
var compileFile = function(file, done){
process.nextTick(function () {
terra.render(file, function(error, body){
if(error){
done(error)
}else{
if(body){
var dest = path.resolve(outputPath, terraform.helpers.outputPath(file))
fs.mkdirp(path.dirname(dest), function(err){
fs.writeFile(dest, body, done)
})
}else{
done()
}
}
})
})
}
/**
* Copy File
*
* TODO: reference ignore extensions from a terraform helper.
*/
var copyFile = function(file, done){
var ext = path.extname(file)
if(!terraform.helpers.shouldIgnore(file) && [".jade", ".ejs", ".md", ".styl", ".less", ".scss", ".sass", ".coffee"].indexOf(ext) === -1){
var localPath = path.resolve(outputPath, file)
fs.mkdirp(path.dirname(localPath), function(err){
fs.copy(path.resolve(setup.publicPath, file), localPath, done)
})
}else{
done()
}
}
/**
* Scan dir, Compile Less and Jade, Copy the others
*/
helpers.prime(outputPath, { ignore: projectPath }, function(err){
if(err) console.log(err)
helpers.ls(setup.publicPath, function(err, results){
async.each(results, compileFile, function(err){
if(err){
callback(err)
}else{
async.each(results, copyFile, function(err){
setup.config['harp_version'] = pkg.version
delete setup.config.globals
callback(null, setup.config)
})
}
})
})
})
}
Der erste Teil definiert den Ausgabepfad, wie er durch den Aufruf von `harp compile` über die Befehlszeile angegeben wird (Quellcode hier). Standardmäßig ist dies, wie Sie sehen können, `www`. Der Callback ist eine Callback-Funktion, die vom Befehlszeilen-Dienstprogramm übergeben wird und nicht konfigurierbar ist.
Der nächste Teil ruft zunächst die `setup`-Funktion im Hilfsmodul auf. Der Kürze halber werden wir nicht auf den spezifischen Code der Funktion eingehen (sehen Sie ihn sich gerne selbst an), aber im Wesentlichen liest er die Site-Konfiguration (d. h. `harp.json`).
Sie werden auch einen Aufruf zu etwas namens `terraform` bemerken. Dies wird in dieser Funktion erneut vorkommen. Terraform ist eigentlich ein separates Projekt, das von Harp benötigt wird und die Grundlage seiner Asset-Pipeline bildet. Die Asset-Pipeline ist der Ort, an dem die eigentliche Arbeit der Kompilierung und Erstellung der fertigen Site erledigt wird (wir werden uns den Terraform-Code gleich ansehen).
Der nächste Codeabschnitt versucht, wie er angibt, zu verhindern, dass Sie ein Ausgabeverzeichnis angeben, das versehentlich Ihren Quellcode überschreibt (was schlecht wäre, da Sie jegliche Arbeit seit Ihrem letzten Commit verlieren würden).
Die Funktionen `compileFile` und `copyFile` sind ziemlich selbsterklärend. Die Funktion `compileFile` verlässt sich auf Terraform, um die eigentliche Kompilierung durchzuführen. Beide Funktionen steuern die `prime`-Funktion, die eine Hilfsfunktion ( `fs` ) verwendet, um die Verzeichnisse zu durchlaufen und dabei Dateien zu kompilieren oder zu kopieren.
Terraform
Wie bereits erwähnt, erledigt Terraform die Hauptarbeit beim Kompilieren von Jade, Markdown, Sass und CoffeeScript zu HTML, CSS und JavaScript (und beim Zusammenfügen dieser Teile gemäß den Vorgaben von Harp). Terraform besteht aus einer Reihe von Dateien, die seine Prozessoren für JavaScript, CSS/Stylesheets und Vorlagen (was in diesem Fall Markdown einschließt) definieren.

Innerhalb jedes dieser Ordner befindet sich ein `processors`-Ordner, der den Code für jeden spezifischen Prozessor enthält, den Terraform (d. h. Harp) unterstützt. Zum Beispiel befinden sich im Vorlagenordner Dateien, die die Grundlage für die Kompilierung von EJS-, Jade- und Markdown-Dateien bilden.

Ich werde nicht auf den Code für jeden einzelnen eingehen, aber größtenteils verlassen sie sich auf externe npm-Module, die den unterstützten Prozessor handhaben. Zum Beispiel hängt die Markdown-Unterstützung von Marked ab.
Die Kernlogik von Terraform ist in seiner `render`-Funktion enthalten.
/**
* Render
*
* This is the main method to to render a view. This function is
* responsible to for figuring out the layout to use and sets the
* `current` object.
*
*/
render: function(filePath, locals, callback){
// get rid of leading slash (windows)
filePath = filePath.replace(/^\\/g, '')
// locals are optional
if(!callback){
callback = locals
locals = {}
}
/**
* We ignore files that start with underscore
*/
if(helpers.shouldIgnore(filePath)) return callback(null, null)
/**
* If template file we need to set current and other locals
*/
if(helpers.isTemplate(filePath)) {
/**
* Current
*/
locals._ = lodash
locals.current = helpers.getCurrent(filePath)
/**
* Layout Priority:
*
* 1. passed into partial() function.
* 2. in `_data.json` file.
* 3. default layout.
* 4. no layout
*/
// 1. check for layout passed in
if(!locals.hasOwnProperty('layout')){
// 2. _data.json layout
// TODO: Change this lookup relative to path.
var templateLocals = helpers.walkData(locals.current.path, data)
if(templateLocals && templateLocals.hasOwnProperty('layout')){
if(templateLocals['layout'] === false){
locals['layout'] = null
} else if(templateLocals['layout'] !== true){
// relative path
var dirname = path.dirname(filePath)
var layoutPriorityList = helpers.buildPriorityList(path.join(dirname, templateLocals['layout'] || ""))
// absolute path (fallback)
layoutPriorityList.push(templateLocals['layout'])
// return first existing file
// TODO: Throw error if null
locals['layout'] = helpers.findFirstFile(root, layoutPriorityList)
}
}
// 3. default _layout file
if(!locals.hasOwnProperty('layout')){
locals['layout'] = helpers.findDefaultLayout(root, filePath)
}
// 4. no layout (do nothing)
}
/**
* TODO: understand again why we are doing this.
*/
try{
var error = null
var output = template(root, templateObject).partial(filePath, locals)
}catch(e){
var error = e
var output = null
}finally{
callback(error, output)
}
}else if(helpers.isStylesheet(filePath)){
stylesheet(root, filePath, callback)
}else if(helpers.isJavaScript(filePath)){
javascript(root, filePath, callback)
}else{
callback(null, null)
}
}
(Wenn Sie all diesen Code genau gelesen hätten, hätten Sie wahrscheinlich TODOs, Tippfehler und sogar einen lustigen Kommentar wie „verstehe noch einmal, warum wir das tun“ bemerkt. Das ist das echte Leben beim Programmieren!)
Der Großteil des Codes in der `render`-Funktion beschäftigt sich mit der Handhabung von Vorlagen. Dinge wie CoffeeScript und Sass rendern grundsätzlich eins-zu-eins. Zum Beispiel wird `style.scss` zu `style.css` gerendert. Selbst wenn es Includes gibt, wird das vom Renderer behandelt. Das Ende der `render`-Funktion beschäftigt sich mit diesen Arten von Dateien.
Layouts in Harp hingegen sind verschachtelt in verschiedenen Arten, die sogar von der Konfiguration abhängen können. Zum Beispiel kann `about.md` innerhalb des Standard-`_layout.jade` gerendert werden (wo genau, wird durch die Verwendung von `!= yield` in diesem Layout bestimmt). `_layout.jade` kann jedoch mehrere andere Layouts innerhalb seiner selbst über die Partial-Unterstützung in Harp einschließen.
Partials sind eine Möglichkeit, eine Vorlage in mehrere Dateien aufzuteilen. Sie sind besonders nützlich für die Wiederverwendung von Code. Zum Beispiel könnte ich den Site-Header in ein Partial legen. Partials sind wichtig, um Layouts in einem Static Site Generator wartbar zu machen, aber sie fügen der Logik der Vorlagenkompilierung auch eine beträchtliche Komplexität hinzu. Diese Komplexität wird in der `partial`-Funktion des Vorlagenprozessors gehandhabt.
Schließlich könnten Sie das Standardlayout überschreiben, indem Sie ein bestimmtes Layout oder gar kein Layout für eine bestimmte Datei innerhalb der Konfigurationsdatei `_data.json` angeben. All diese Szenarien werden (und sogar nummeriert) innerhalb der Logik der `render`-Funktion gehandhabt.
Das ist doch nicht so kompliziert, oder?
Um dies verdaulich zu machen, habe ich eine Menge zusätzlicher Details ausgelassen. Im Kern funktionieren alle Static Site Generatoren, die ich je benutzt habe (und ich habe ein paar benutzt), ähnlich: Ein Satz von Regeln, Konventionen und Konfigurationen, der durch Compiler für die verschiedenen unterstützten Auszeichnungen läuft. Vielleicht gibt es deshalb eine lächerliche Anzahl von Static Site Generatoren.
Das gesagt, ich würde meinen eigenen nicht bauen wollen!
Mein Bericht & Buch
Wenn Sie daran interessiert sind, wie man Websites mit einem Static Site Generator erstellt, habe ich einen Bericht verfasst und ein Buch mitautorisiert, das Sie vielleicht interessieren könnte. Mein Bericht, einfach mit Static Site Generators betitelt, ist kostenlos und versucht, die Geschichte, die Landschaft und die Grundlagen von Static Site Generatoren darzulegen.

Das Buch, das ich mit Raymond Camden mitautorisiert habe, heißt Working with Static Sites und ist als Frühausgabe erhältlich, sollte aber bald auch gedruckt verfügbar sein.
Wenn man die Website wegnimmt, ist es im Grunde nicht unähnlich zu einem Report-Generator. Er wird bei einem Ereignis (möglicherweise periodisch, da dies ein sehr grundlegendes Ereignis ist) ausgeführt, holt aus einer Datenquelle und präsentiert etwas.
Was mich im Moment sehr interessiert (obwohl ich noch keinen Kunden habe, der bereit ist, diesen Weg mit mir zu gehen) ist die Anwendung der Prinzipien von statischen Frontends mit dynamischen Backends auf Systeme. Die einfache Option ist das Spinnen einer statischen Version von einer dynamisch bedienten Website, das habe ich oft gemacht, aber die wahre Stärke liegt darin, die Kosten pro Besucher und sogar die Serverkosten senken zu können, indem man weniger auf dem Server über nicht geschäftskritische Aufgaben nachdenkt.
Tolle Lektüre; vielleicht etwas spezifisch in einigen Details.
Ich weiß nicht, Leute, aber all diese Generatoren sehen für mich wie eine Goldberg-Maschine aus. Man muss tonnenweise Plugins, Bibliotheken installieren, dies kompilieren, Pfade einrichten, eine neue Markup-Sprache lernen und verwenden, und am Ende hat man eine einfache Website, die man auch ohne all diese Werkzeuge in der gleichen Zeit oder sogar schneller schreiben könnte. Ich weiß, ich weiß, das ist „Harder, Better, Faster, Stronger“, aber ich vermisse die Zeiten, in denen man für das Schreiben einer Website nur einen Code-Editor, Kenntnisse in HTML, CSS und etwas JS oder PHP brauchte, ohne all diese Zusätze.
Wie auch immer, toller Artikel. Danke
Hallo Mark. Als Neuling (ca. 18 Monate) im Web-Dev-Bereich habe ich mir viele Generatoren, Frameworks usw. angesehen und sehe immer noch keinen Vorteil. Für mich ist das alles nur „noch eine Sache zum Lernen“. Ich habe immer noch nicht DEN Generator/Framework für mich gefunden. Ich scheine mit „nur“ HTML, CSS, JS bestens zurechtzukommen. Hoffentlich finde ich einen Arbeitgeber, der das auch so sieht.
Danke Brian für diesen tiefen Einblick in Harps Kernfunktionen, dieser Beitrag ist jetzt auch auf Französisch verfügbar (https://jamstatic.fr/2017/02/09/y-a-quoi-dans-un-generateur-de-site-statique/), wenn Sie den Link zur Übersetzung hinzufügen möchten.
@Mark @Jared Heute muss man kontinuierlich liefern können.
Die Kombination von Git-Versionierung und automatisch getesteten Builds (durchgeführt von Static Site Generatoren) ermöglicht ein sicheres und kontinuierliches Ausliefern. Dies gilt möglicherweise nicht für jedes Projekt.
Matthias Billman, der CEO von Netlify, spricht über den aktuellen Web-Frontend-Stack und warum er sich entwickelt hat: https://vimeo.com/163522126