なぜ Liquid に自作タグを1枚だけ足したのか ― テンプレートエンジンの拡げ方
前回は、共通パーツを配る {% partial "名前" %} という 1 つのタグを、使う側から見ました(ヘッダーは 1 か所だけ ― 共通パーツを全ページに配る partial タグ)。今回はその裏側、設計の話です。@nano-cms/core はテンプレートに Liquid を採用していますが、ほとんど素のまま使い、自作のタグはこの partial 1 枚だけ足しています。なぜ 1 枚だけなのか、そして「タグを足す」を成り立たせるために押さえている 2 つの接ぎ目を見ていきます。
Liquid を選び、ほとんど素のまま使う
テンプレートには Liquid(LiquidJS 実装)を使っています。{% if %} による分岐、{% for %} による繰り返し、{% assign %} による変数、{{ title | default: "無題" }} のようなフィルタ ― ページを組み立てるのに要る制御構文は、最初から揃っています。これらは自分では作りません。標準にあるものを自作で足し直すのは、増やさなくていい面積を増やすだけだからです。
Liquid を選んだ理由のひとつは、テンプレートを書く人が必ずしもエンジニアとは限らないことです。Liquid のタグは「任意のプログラムを実行できる」ものではなく、決められたタグの範囲で組み立てる仕組みなので、テンプレートに危険なコードが紛れ込みにくい。共通パーツやページのテンプレートを画面から編集する運用とも相性が良いわけです。
もう 1 点、engine は寛容な設定で動かしています。未定義の変数やフィルタに出会っても、そこで止めずに空として流す設定です。テンプレートのささいな書き間違いや、たまたま欠けたデータ 1 つでページ全体が真っ白になる ― 公開面でそれが起きるのは避けたい。厳密に倒すより、欠けても出し続ける側に倒しています。
標準で書けない 1 点 ― 「別の場所の中身を、名前で読み込む」
では、なぜ partial だけは自作したのか。共通パーツの読み込み ― 「site-header という名前で登録された、版管理された別のデータを、いまの文脈に応じた版で読み込んで差し込む」 ― これは素の Liquid の標準タグには無い動きだからです。単にファイルを取り込むだけなら近い標準機能もありますが、前回見たように、ここで欲しいのは「データベース上の共通パーツを、サイトごとに区別し、本番では公開版・プレビューでは下書きや予約版に切り替えて読む」という、このサイトのデータの持ち方に踏み込んだ振る舞いです。
そして「なぜ自作タグがこの 1 個で足りるのか」も同じ理由です。やりたいことを機能ごとにタグへ分けると、タグは際限なく増えます。そうではなく「名前で別の中身を読み込む」という 1 つの能力に集約し、値渡し・プレビュー時の差し替え・予約版の先取り・サイトごとのスコープを、すべて partial という 1 つのタグの内側に畳み込みました。読む側のテンプレートに現れる自作タグは 1 種類で済み、覚えることも 1 つになります。
拡張の口は「engine にタグを登録する」1 本だけ
自作タグをエンジンに足す方法も、1 本の口に絞っています。タグの名前と、その振る舞いを書いたクラスを渡して登録する、という関数です。
// 名前と、振る舞いを書いたクラスを engine に登録する
registerLiquidTag("partial", PartialTag);
この方式の効きどころは、CMS の中核が個々のタグの中身を知らないままでいられることです。中核は「タグを登録する口」だけを用意し、どんなタグを足すかは外側(このサイト側)で決めて差し込みます。partial も、この口を通して外から登録された 1 枚であって、中核に直接埋め込まれたものではありません。将来べつのタグが要るサイトが出てきても、中核を書き換えずにそのサイト側で足せます。タグの中身そのものをどう書くかは次回(Liquid に自作タグを足す ― partial タグで見る作り方)に譲り、ここでは「足す口は 1 本」という形だけ押さえておきます。
接ぎ目その 1 ― 登録する順番に制約がある
タグを足せるとして、次は「いつ登録するか」です。ここに順序の制約があります。
起動時、CMS はいくつかの初期化を順に行います。設定を注入し、自作タグを登録し、それからサイトのモジュール(このサイトではブログ)を登録します。問題は、最後のモジュール登録が、その内側でサイト設定のテンプレートを先に一度描画する経路を持っていることです。もしこの描画の時点で partial がまだエンジンに登録されていないと、「そんなタグは無い」と判断されて描画が落ちます。
そのため、自作タグの登録はモジュール登録より必ず前に置きます。起動コードのその箇所には、なぜこの順でなければならないかが理由つきで書き残してあります。順序は「なんとなくこの並び」ではなく、後段の描画が前段の登録に依存している、という構造から決まっています。
接ぎ目その 2 ― 開発中、放っておくとタグ登録が消える
もう 1 つの接ぎ目は、開発のしやすさと引き換えに出てくる問題です。コードを保存するたびに変更を即反映する仕組み(ホットリロード)で開発していると、テンプレートエンジンの実体が作り直されることがあります。作り直されたエンジンには、起動時に登録したはずの自作タグが載っていません。つまり、開発中にうっかりタグだけ未登録の状態に戻ってしまいます。
これを避けるため、登録した内容の控えをプロセス全体で 1 つだけ持つようにしています。エンジンが作り直されても、次にテンプレートを描画する直前にこの控えを見て、必要なら登録し直します。一度登録済みなら何もしないので、毎回の描画が重くなることもありません。本番ではエンジンが作り直されることはないため、この復元は主に開発時の保険として働きます。
控えを「プロセス全体で 1 つ」に保つのは見た目より厄介です。本番のビルドではコードがいくつかの束に分割され、同じモジュールでも複数の実体が同居しうるからです。そこで控えの置き場には、どの束から見ても同じ 1 か所に解決される共有のキーを使い、登録の控えが束ごとにばらけないようにしています。
タグ 1 つの失敗を、ページ全体に波及させない
最後に、自作タグを足すなら避けて通れないのが「タグが描画中にエラーを起こしたらどうなるか」です。共通パーツの読み込み中に何かが失敗したとき、それでページ全体が真っ白になっては困ります。
そこで、タグの描画はエラーを内側で受け止める形で包んでいます。あるタグの描画が失敗しても、そのタグの範囲が出ないだけで、ページの残りは描画を続けます。失敗はその場で握りつぶすのではなくログに残し、管理画面からも後で確認できるようにしています。公開面では「一部が欠けても、ページは出し続ける」側に倒し、起きた失敗は運用側で拾える、という分担です。
標準の Liquid はそのまま使い、足したのは「名前で別の中身を読み込む」という 1 点だけ。値渡し・プレビュー・予約・サイトスコープはその 1 つのタグの内側に畳み込みました。拡張の口は「engine にタグを登録する」1 本に絞り、中核はタグの中身を知りません。そのうえで、登録の順序・登録が消えない仕掛け・タグ単体の失敗の隔離、という 3 つの足場を用意しています。
素の Liquid に足したものは、結局のところ「名前で読み込む」タグ 1 枚と、それを安全に成り立たせる足場でした。では、そのタグの中身は具体的にどう書かれているのか。次回は、この partial タグ自身を題材に、Liquid の自作タグを 1 つ作る手順を追います ― Liquid に自作タグを足す ― partial タグで見る作り方。

