Was macht einen Static Site Generator wirklich aus?

Avatar of Brian Rinaldi
Brian Rinaldi am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

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 Details für diese Standardwerte finden Sie in der Quellcode der Befehlszeilenanwendung.

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.