Zurück zum Blog

Folgen und abonnieren

Eine modulare Edge Side Includes Komponente für JavaScript auf Fastly Compute

Katsuyuki Omuro

Senior Software Engineer, Developer Relations, Fastly

Im Sommer 2022 arbeitete mein Teamkollege Kailan an einer Rust Crate für Fastly Compute. Dabei implementierte er eine Teilmenge der Templating-Sprache Edge Side Includes (ESI) und veröffentlichte einen Blogpost über das Projekt. Dieser Blogpost war nicht nur deshalb so interessant, weil darin eine nützliche Bibliothek veröffentlicht wurde, sondern auch, weil er die Vorteile von Compute eindrucksvoll veranschaulicht: eine programmierbare Edge mit modularen Funktionen. Und da JavaScript seit mehr als einem Jahr für alle Compute Nutzer verfügbar ist, wurde es höchste Zeit, dass wir eine ähnliche Lösung für unsere JavaScript Nutzer anbieten. Mit der ESI Bibliothek von Fastly für JavaScript, die jetzt auf npm verfügbar ist, können Sie Ihre Anwendung um eine leistungsstarke ESI Verarbeitung erweitern.

Programmierbarkeit und Modularität

Seit fast einem Jahrzehnt bietet das Fastly CDN Unterstützung für Edge Side Includes (ESI), eine Templating-Sprache, die über die Interpretation spezieller Tags in Ihrem HTML-Quelltext funktioniert. Das Tag, von dem hier die Rede ist, lautet <esi:include>. Es weist den Edge-Server an, ein anderes Dokument abzurufen und es in den Stream einzubinden.

index.html

<body>
<esi:include src="./header.html" />
<main>
Content
</main>
</body>

header.html

<header>Welcome to the web site</header>

Output

<body>
<header>Welcome to the web site</header>
<main>
Content
</main>
</body>

Als Compute auf der Bildfläche erschien, veränderte sich das Edge-Umfeld vor allem in zweierlei Hinsicht: Programmierbarkeit und Modularität.

Kurz nachdem unsere Plattform endlich einen stabilen Support für Rust bot, veröffentlichten wir eine Crate für Rust, bei der ESI und zusätzliche Programmierbarkeit implementiert wurden. Nun ließ sich mittels Code der Aufbau zusätzlicher Backend-Anfragen und der Umgang mit Antwortfragmenten konfigurieren. Man kann sogar Dokumente, die nicht vom Backend-Server stammen, mittels ESI verarbeiten. In dieser Programmierbarkeit liegt der Unterschied zum ESI Support in VCL, der auf die von uns angebotenen Standardfunktionen beschränkt war.

Gleichzeitig war dieser Ansatz hochgradig modular, sodass es Programmierern freigestellt war, diese ESI Verarbeitung für jede einzelne Anfrage durchzuführen, sie mit anderen Modulen zu kombinieren, die mit kompatiblen Datentypen arbeiten, und sie in jeder beliebigen Reihenfolge bzw. mit jeder vorgegebenen logischen Bedingung anzuwenden.

Nächstes Ziel: JavaScript

Ähnlich wie bei unserem Rust Release wollten wir, dass diese JavaScript Bibliothek programmierbar ist. Fastlys JavaScript Support umfasst seit jeher die Fetch API und die dazugehörige Streams API. Ein nützliches Feature der Streams API ist die Schnittstelle TransformStream, mit der sich Daten wie durch eine Pipeline durch ein Objekt leiten lassen, um eine Transformation durchzuführen. Diese Funktion eignet sich perfekt für ESI. Durch die Implementierung der ESI Verarbeitung über TransformStream konnten wir sie direkt in eine in JavaScript geschriebene Fastly Compute Anwendung integrieren.

So können Sie darüber streamen:

const transformedBody = resp.body.pipeThrough(esiTransformStream);

return new Response(
  transformedBody,
  {
    status: resp.status,
    headers: resp.headers,
  },
);

Die Transformation, die wir „EsiTransformStream“ nennen, wird direkt auf den Stream angewendet, wodurch Speicher- und Performance-Probleme vermieden werden. Dies bietet folgende Vorteile:

  • Es ist nicht notwendig, die gesamte Upstream-Antwort zu buffern, bevor Sie die Transformation anwenden.

  • Die Upstream-Antwort wird vom Transformator so schnell wie möglich erfasst und ESI Tags werden verarbeitet, sobald sie im Datenstream auftauchen. Nachdem der Transformator die Verarbeitung der einzelnen ESI Tags abgeschlossen hat, werden die Ergebnisse sofort für nachgelagerte Prozesse freigegeben, damit möglichst wenig gebuffert werden muss. Auf diese Weise kann der Client das erste Byte des gestreamten Ergebnisses empfangen, sobald es verfügbar ist, ohne auf seine vollständige Verarbeitung warten zu müssen.

Außerdem ist EsiTransformStream modular aufgebaut, sodass Sie es einfach parallel zu anderen Tools nutzen können. Sie können zum Beispiel andere Transformationen (darunter auch Komprimierung) auf Antworten anwenden und Antworten durch eine beliebige Anzahl von Transformations-Streams leiten, da es sich bei ESI um eine Standardschnittstelle handelt.  Wenn Sie möchten, können Sie ESI sogar nur für bestimmte Anfragen oder Antworten aktivieren, etwa anhand von Anfrage-Headern, Pfaden oder dem Inhaltstyp der Antwort.

So instanziieren Sie EsiTransformStream:

const esiTransformStream = new EsiTransformStream(req.url, req.headers, {
  fetch(input, init) {
    return fetch(input, { ...init, backend: 'origin_0' });
  }
});

Nehmen Sie eine URL und ein Headers-Objekt und gegebenenfalls einige Optionen für einen dritten Parameter. Wie bereits beschrieben, besteht die Hauptfunktion von ESI darin, zusätzliche Templates herunterzuladen, um sie in den resultierenden Stream aufzunehmen. Taucht dabei das Tag <esi:include> auf, wird Fetch als zugrundeliegender Mechanismus verwendet, um ein Template abzurufen, und der Hauptzweck der erwähnten Parameter ist die Konfiguration dieser Fetch-Aufrufe:

  • Die URL dient der Auflösung relativer Pfade im Quellcode von <esi:include>-Tags.

  • Die Header kommen bei zusätzlichen Anfragen zum Abruf der Templates zum Einsatz.

  • Das optionale Konfigurationsobjekt kann verwendet werden, um das Verhalten des durchgeführten Abrufs zu verändern und andere nutzerdefinierte Verhaltensweisen wie die Verarbeitung des abgerufenen Templates und eine individuelle Fehlerbehebung anzuwenden.

Im einfachsten Fall verwenden Sie nur den Fetch-Wert des Konfigurationsobjekts. Wenn Sie diesen nicht angeben, wird stattdessen die globale Fetch-Funktion verwendet. In Compute müssen Sie jedoch ein Backend für Fetch angeben, das beim Einfügen eines Templates verwendet wird (es sei denn, Sie verwenden die Funktion für dynamische Backends). Im obigen Beispiel-Snippet wird das Backend mit dem Namen origin_0 zugewiesen, bevor der globale Fetch aufgerufen wird.

Mit diesem einfachen Setup können Sie ESI Tags, die von einem Backend ausgeliefert werden, mithilfe einer Compute Anwendung verarbeiten. Probieren Sie es aus, indem Sie folgenden umfassenden Beispielcode ausführen:

Unterstützung für ESI Funktionen

Diese Implementierung bietet mehr ESI Funktionen als andere, die wir in der Vergangenheit zur Verfügung gestellt haben.

Fehlerbehebung

Es kann vorkommen, dass eine Datei, auf die ein <esi:include>-Tag verweist, nicht abgerufen werden kann, weil sie nicht existiert oder einen Serverfehler verursacht. In solchen Fällen können Sie den Fehler ignorieren, indem Sie das Attribut onerror="continue" hinzufügen.

<esi:include src="/templates/header.html" onerror="continue" />

Wenn der Fehler durch /templates/header.html verursacht wird, ignoriert das ESI Verarbeitungstool den Fehler und ersetzt das gesamte <esi:include>-Tag durch einen leeren String.

Sie können auch eine strukturiertere Fehlerbehebung vornehmen, indem Sie einen <esi:try>-Block verwenden, der wie folgt aussieht:

<esi:try>
  <esi:attempt>
    Main header
    <esi:include src="/templates/header.html" />
  </esi:attempt>
  <esi:except>
    Alternative header
  </esi:except>
</esi:try>

Das ESI Verarbeitungstool führt zunächst den Inhalt von <esi:attempt> aus. Wenn ein <esi:include>-Tag einen Fehler verursacht, führt das ESI Verarbeitungstool den Inhalt von <esi:except> aus.

Es ist wichtig zu beachten, dass der gesamte Block <esi:try> durch den gesamten Block <esi:attempt> ersetzt wird, wenn er erfolgreich verarbeitet wurde, oder durch den Block <esi:except>, wenn ein Fehler auftritt. Wenn im obigen Beispiel /templates/header.html einen Fehler verursacht, dann führt dies auch dazu, dass der Text „Main header“ nicht im Output enthalten ist, sondern nur der Text „Alternative header“. Einzelheiten finden Sie in der ESI Language Specification.

Bedingte Ausführung

ESI ermöglicht auch eine bedingte Ausführung, indem es Laufzeitprüfungen für Variablen durchführt. Beispiel einer solchen Prüfung:

<esi:choose> 
    <esi:when test="$(HTTP_COOKIE{group})=='admin'"> 
        <esi:include src="/header/admin.html" /> 
    </esi:when> 
    <esi:when test="$(HTTP_COOKIE{group})=='basic'">
        <esi:include src="/header/basic.html" />
    </esi:when> 
    <esi:otherwise> 
        <esi:include src="/header/anonymous.html" /> 
    </esi:otherwise>
</esi:choose>

Wenn das Verarbeitungstool auf einen <esi:choose>-Block stößt, durchläuft es die esi:when Blöcke und prüft die Ausdrücke, die in ihren Testattributen festgelegt sind. Das Verarbeitungstool führt den ersten <esi:when>-Block aus, bei dem das Ergebnis der Ausdrucksprüfung positiv ausfällt. Wenn das Prüfergebnis bei keinem der Ausdrücke positiv ausfällt, wird der <esi:otherwise>-Block optional ausgeführt. In diesem Fall wird der gesamte esi:choose Block durch den ausgeführten Block <esi:when> oder <esi:otherwise> ersetzt.

Das Verarbeitungstool stellt eine begrenzte Anzahl an Variablen zur Verfügung, die hauptsächlich auf Anfrage-Cookies beruhen. Im obigen Beispiel wird ein HTTP-Cookie namens „group“ auf seinen Wert hin geprüft. Unsere Implementierung basiert auf der ESI Language Specification, in der Sie weitere Informationen erhalten.

Unterstützte Tags und Features

Unsere Implementierung unterstützt folgende Tags aus der ESI Language Specification:

  • esi:include
  • esi:comment
  • esi:remove
  • esi:try / esi:attempt / esi:except
  • esi:choose / esi:when / esi:otherwise
  • esi:vars

Das in der ESI Language Specification als optional ausgewiesene Tag <esi:inline> ist in unserer Implementierung nicht enthalten.

ESI Variablen werden in den Attributen von ESI Tags und ESI Ausdrücke im Testattribut <esi:when> unterstützt. Außerdem wird der Kommentar <!--esi ...--> unterstützt.

Unzählige Möglichkeiten durch nutzerdefiniertes Verhalten

Der Funktionsumfang ist zwar schon beeindruckend genug, aber das wirklich Aufregende an der Programmierbarkeit ist, dass sie noch viele weitere Möglichkeiten bietet. Die Bereitstellung von Templates ist der wichtigste Nutzen von ESI, aber das ist bei weitem nicht alles, was diese Programmiersprache leisten kann. Hier ein Beispiel:

Nehmen wir an, Sie haben in Ihr Dokument einen Zeitstempel integriert, der als relative Datumsangabe angezeigt werden soll, z. B. „vor 2 Tagen“. Dies lässt sich auf verschiedene Weise bewerkstelligen, aber um die beste Performance zu gewährleisten und das Ganze möglichst arbeitsspeichereffizient zu gestalten, wäre es gut, wenn Sie in Ihren Streams die Funktion „Suchen/Ersetzen“ verwenden könnten. Dies lässt sich besonders gut bewerkstelligen, indem Sie eine entsprechende ESI Bibliothek programmieren.

Sie können zum Beispiel Zeitstempel festlegen, die in Ihrem Backend-Dokument mit einem speziellen ESI Tag im folgenden Format kodiert werden:

<esi:include src="/__since/<unix-timestamp>" />

Dieses Snippet kann zum Beispiel für 0:00 Uhr Pazifischer Zeit am 1. Januar 2024 stehen:

<h1>This document was written <esi:include src="/__since/1704034800" />.</h1>

Richten Sie nun EsiTransformStream so ein, dass er ein synthetisches Ersatzdokument liefert, sobald er folgendes URL-Muster erkennt:

const esiTransformStream = new EsiTransformStream(req.url, req.headers, {
    fetch(input, init) {
      const url = new URL(input instanceof Request ? input.url : String(input));

      // If custom URL pattern, return synthetic Response
      const sinceMatch = /^\/__since\/(\d+)$/.exec(url.pathname);
      if (sinceMatch != null) {
        const timestampString = sinceMatch[1];
        const timestamp = parseInt(timestampString, 10);
        return new Response(
          toRelativeTimeSince(timestamp), // This function builds a relative date
          {
            headers: { 'Content-Type': 'text/plain', },
            status: 200,
          }
        );
      }

      // For all other esi:include tags, add the backend and call the global fetch
      return fetch(input, { ...init, backend: 'origin_0' });
    }
  });

Wenn das Verarbeitungstool auf das obige Beispiel-Snippet stößt, wird das Ergebnis Folgendem ähneln (je nachdem, wie viele Tage in der Zukunft Sie den Code ausführen):

<h1>This document was written 22d ago.</h1>

Da das Backend-Dokument von Fastly gecacht werden kann, können zukünftige Anfragen aus einem Cache Hit bedient werden, während bei der Verarbeitung weiterhin die aktualisierte relative Zeitangabe ausgegeben wird.

In folgendem Fiddle können Sie sich das Ganze live ansehen:

Überzeugen Sie sich selbst!

@fastly/esi ist jetzt auf npm verfügbar und kann zu jedem JavaScript Programm hinzugefügt werden. Nutzen Sie es auf der Edge in Ihren Fastly Compute Programmen oder sogar außerhalb von Compute, sofern Ihre Umgebung die Fetch API unterstützt. Den vollständigen Quellcode finden Sie auf GitHub.

Klonen Sie zunächst eines der obigen Fiddles und probieren Sie sie mit Ihren eigenen Origin-Servern aus, bevor Sie überhaupt einen Fastly Account erstellen. Wenn Sie bereit sind, Ihren Service in unserem globalen Netzwerk zu veröffentlichen, können Sie sich für eine kostenlose Testversion von Compute registrieren und sofort mit der ESI Bibliothek auf npm loslegen.

Compute sorgt für eine programmierbare und modulare Edge, auf der Sie die für Sie am besten geeigneten Lösungen auswählen und kombinieren oder sogar eigene Lösungen erstellen können. Aber nicht nur wir können solche Module für Compute anbieten. Sämtliche Nutzer können zum Ökosystem beitragen und davon profitieren. Lassen Sie uns gerne auch im Fastly Community Forum wissen, was Sie so entwickeln!