JavaScript Compute 向け Edge Side Includes モジュールコンポーネント
2022年の夏、チームメイトの Kailan は Fastly Compute 向けに Rust クレート の構築に取り組んだ際、テンプレート化のためのマークアップ言語である Edge Side Includes (ESI) のサブセットを実装し、それについてブログ記事を執筆しました。有用なライブラリを公開したという理由だけではなく、モジュール機能により Compute で プログラマブルなエッジを実現できる好例を示した点において、この記事は大きな意味を持っていました。そして Compute で JavaScript のサポートが一般公開されてから1年以上たった今、JavaScript ユーザー向けに同様のソリューションを提供したいと私たちは考えました。すでに npm で公開されている Fastly の JavaScript 向け ESI ライブラリを利用することで、パワフルな ESI 処理をアプリケーションに追加できます。
プログラマビリティとモジュール性
約10年にわたり Fastly の CDN は Edge Side Includes (ESI) をサポートしてきました。ESI は HTML ソースにある特殊なタグを解釈することで機能する、テンプレート化のためのマークアップ言語です。別のドキュメントをフェッチし、ストリームにインラインするようエッジサーバーに指示する <esi:include>
タグを使用します。
index.html |
---|
|
header.html |
---|
|
出力 |
---|
|
Compute がリリースされた際、「プログラマビリティ」と「モジュール性」の2点においてエッジ環境が変化しました。
Rust に対する Fastly プラットフォームのサポートが安定した後、すぐに私たちは ESI を実装する Rust のクレートを公開し、プログラマビリティを高めました。その結果、コードを使って追加のバックエンドリクエストを構築する方法やレスポンスのフラグメントを処理する方法を設定できるようになりました。バックエンドサーバーから取得していないドキュメントに ESI 処理を実行することも可能です。このようなプログラマビリティは、Fastly が提供する決まった機能セットに限られていた VCL での ESI サポートとは一線を画しています。
同時に、このアプローチはモジュール性が高く、プログラマーはこのような ESI 処理をリクエストごとに処理することも、互換性のある種類のデータを扱う別のモジュールと ESI 処理を組み合わせ、任意の順序または特定のロジック条件に基づいて適用することもできます。
次なるターゲット : JavaScript
Rust のクレートの場合と同様に、この JavaScript ライブラリもプログラマブルにしたいと私たちは考えました。Fastly の JavaScript のサポートでは一貫して Fetch API とその仲間である Streams API を積極的に受け入れてきました。Streams API の便利な機能のひとつに TransformStream インターフェイスがあり、これによりオブジェクトを介してデータがパイプ処理され、完璧な変換を ESI で実行できます。TransformStream を導入するために ESI プロセッサーを実装することで、JavaScript で書かれた Fastly Compute のアプリケーションにこれを簡単に組み込むことが可能になりした。
以下はこれを介してデータをストリームする方法を示しています。
const transformedBody = resp.body.pipeThrough(esiTransformStream);
return new Response( transformedBody, { status: resp.status, headers: resp.headers, },);
「EsiTransformStream」と呼ばれる変換が直接ストリームに適用され、メモリやパフォーマンスに関する懸念を緩和できます。その結果、以下のメリットが得られます。
変換を実行する前にアップストリームレスポンス全体をバッファする必要がなくなります。
トランスフォーマーがアップストリームレスポンスを素 早く取り込み、ストリームに現れる ESI タグを処理します。トランスフォーマーが各 ESI タグの処理を終えると、結果がすぐにダウンストリームにリリースされるので、バッファを最小限に抑えることができるのです。これにより、結果がストリームされるとクライアントはすぐに最初のバイトを受信することができ、全体が処理されるまで待つ必要がなくなります。
また、これはモジュラー型の設計であるため、EsiTransformStream は数あるうちの利用可能なツールのひとつにすぎません。例えば、圧縮など他の変換をレスポンスに適用し、レスポンスを任意の数の変換ストリームを通じてパイプ処理することができます。これは完全に標準的なインターフェイスだからです。 さらに、リクエストヘッダーやパス、レスポンスのコンテンツタイプなどに基づいて特定のリクエストやレスポンスに対してのみ条件的に ESI を有効にすることも可能です。
以下は EsiTransformStream をインスタンス化する方法を示しています。
const esiTransformStream = new EsiTransformStream(req.url, req.headers, { fetch(input, init) { return fetch(input, { ...init, backend: 'origin_0' }); }});
コンストラクタは URL とヘッダーオブジェクトを取得し、3つ目のパラメータとして何らかのオブジェクトを追加で取得することもできます。前述のように、ESI の主な機能は結果のストリームに含める追加のテンプレートをダウンロードすることにあります。<esi:include>
のタグが、テンプレートを取得する基盤となるメカニズムとして fetch を使用する場合、以下のパラメーターの目的は、このような fetch の呼び出しを設定することにあります。
URL は
<esi:include>
タグの src で相対パスを解決す るのに使用されます。ヘッダーはテンプレートをフェッチする追加リクエストを実行する際に使用されます。
任意で追加の設定オブジェクトを使用して fetch の動作をオーバーライドしたり、フェッチしたテンプレートの処理方法やカスタムエラー処理など、別のカスタム動作を適用したりできます。
最もシンプルなケースでは、単に設定オブジェクトの fetch 値のみを使用することもできます。fetch 値が提供されない場合、グローバル fetch 関数が代わりに使用されますが、Compute では、テンプレートを含める場合 (動的バックエンド機能を使用しない限り)、使用する fetch のバックエンドを特定するために fetch 値が必要になります。上記のスニペット例では、global fetch が呼び出される前に origin_0
という名前のバックエンドが割り当てられます。
以上です!このシンプルなセットアップにより、ESI タグに対応するバックエンドとこれらのタグを処理する Compute アプリケーションを用意できます。以下は実行できる完全なサンプルです。
ESI 機能のサポート
この実装は、過去に私たちが公開したものよりも多くの ESI 機能を提供します。
エラー処理
<esi:include>
タグによって参照されるファイルが、存在しないか、またはサーバーエラーによりフェッチに失敗することがあります。このような場合、onerror="continue"
の属性を追加することで、エラーを無視できます。
<esi:include src="/templates/header.html" onerror="continue" />
/templates/header.html
がエラーを引き起こす場合、ESI プロセッサーは何事もなかったかのようにエラーを無視し、<esi:include>
タグを空の文字列に置き換えます。
また、以下のような <esi:try>
ブロックを使用し、より構造化されたエラー処理を行うことも可能です。
<esi:try> <esi:attempt> Main header <esi:include src="/templates/header.html" /> </esi:attempt> <esi:except> Alternative header </esi:except></esi:try>
ESI プロセッサーはまず、<esi:attempt>
のコンテンツを実行します。<esi:include>
タグによってエラーが生じると、ESI プロセッサーは <esi:except>
のコンテンツを実行します。
成功した場合、<esi:try>
ブロック全体が <esi:attempt>
ブロック全体に置き換えられ、エラーが発生した場合、<esi:except>
ブロック全体に置き換えられるようにすることが重要なポイントです。上記の例では、/templates/header.html
によってエラーが発生すると、これにより「Main header」のテキストが出力に表示されず、「Alternative header」のテキストのみが含まれます。詳しくは、ESI 言語仕様をご覧ください。
条件を付ける
ESI では、変数にランタイムチェックを実施することによる、条件付きの実行も可能です。以下は、そのようなチェックの例です。
<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>
プロセッサーが <esi:choose>
ブロ ックに遭遇すると、esi:when ブロックをざっと調べ、テスト属性にある式のセットを確認します。プロセッサーは、式の値が true と判断される最初の <esi:when>
ブロックを実行します。どの式も値が true と判断されない場合、オプションで <esi:otherwise>
ブロックが実行されます (提供されている場合)。esi:choose ブロック全体が、実行する <esi:when>
または <esi:otherwise>
ブロックのいずれかに完全に置き換えられます。
プロセッサーは、主にリクエスト Cookie に基づいて、限られた変数のセットを利用可能にします。上記の例では、HTTP Cookie の「group」の値が確認されます。私たちの実装は、ESI 言語仕様に基づいているので、詳細についてはこちらをご覧ください。
サポートされているタグと機能
この実装では ESI 言語仕様の以下のタグがサポートされています。
esi:include
esi:comment
esi:remove
esi:try / esi:attempt / esi:except
esi:choose / esi:when / esi:otherwise
esi:vars
同仕様でオプションとして定義されている <esi:inline>
タグは、この実装には含まれません。
ESI Variables は ESI タグの属性でサポートされ、ESI Expressions は <esi:when>
のテスト属性でサポートされています。また、<!--esi ...-->
コメントもサポートされています。