ホームページ >ウェブフロントエンド >jsチュートリアル >依存関係のないテンプレート言語

依存関係のないテンプレート言語

Patricia Arquette
Patricia Arquetteオリジナル
2025-01-27 20:32:13492ブラウズ

Template language without dependencies

テンプレート言語

このブログは、私のブルーバードブログのパート 2 です。パート 1 を参照

依存関係のない私の Twitter クローンでは、変数とテンプレート名を返すようにルート ハンドラーを設計することにしました。これにより、テストでは HTML ドキュメントを検査するのではなく、テンプレート名と変数のアサーションのみを実行できるため、テストが簡単になります。

// request
{
  method: "GET",
  path: "/profile/1234",
  cookies: { "user-id": 54 },
}

// response
{
  status: 200,
  template: "public-profile-show",
  variables: {
    user: {
      id: 54,
      name: "John Doe",
    },
    posts: [
      { id: 55412, message: "Have you seen the new iThing?",
        createdAt: 1699788972 }
    ]
  }
}

このブログでは、このテンプレート言語を実装していきます。

テンプレート言語の設計

私が必要とするテンプレート言語は、一連の変数のみを入力として使用して HTML ドキュメントを出力する必要があります。テンプレートを JS 関数にコンパイルしたいと考えています。たとえば、Hello <%= name %> が必要です。次のようにコンパイルします:

({ name }) => `Hello ${escapeHtml(name)}`;

私は古典的な <%= %> を使用します。構文は非常に一般的でよく知られているためです。この構文を見たほとんどの開発者は、そこに通常のコードを記述するだけで、そのコードの出力が出力に追加されることが直感的にわかるでしょう。

変数と自動エスケープ HTML エンティティをサポートする必要があります。ループ、if/else ステートメント、および他のテンプレートもサポートする必要があります。任意の関数を呼び出して基本的な計算を実行できれば便利です;

基本的には、任意の JavaScript を実行できるようにしたいのです。

実装

コードを書き始めて、最終的にどこに辿り着くのかを確認したところだと思います。まずはテストです。

it("simple template", () => {
  const fn = Template.parse("Hello, <%= name %>");
  assert.equal(fn({ name: "world" }), "Hello, world");
});

私が思いつく最も簡単な実装は、正規表現を使用することです。 の外側にあるすべてのコンテンツは出力に追加されるだけで、その間のコンテンツは JS として実行されます。

使用される正規表現は /(.*?)/sg です。この正規表現は、(.*?) になるまでテキストをキャプチャします。 (.*?)%> を使用します。 s 修飾子を使用すると、 . (ドット) は改行と一致します。 g 修飾子を使用すると、複数の一致が可能になります。

文字列に対する Javascript の replace 関数を使用すると、一致するすべてのコードを実行しながら、コード内の置換値 "" を返すことができます。すべての一致は空の文字列に置き換えられるため、最後の %> の後のテキストのみが空の文字列に置き換えられます。これは、末尾と呼ばれる replace 関数によって返されます。

JSON.stringify を使用して文字列リテラルを作成します。

const Template = {
  parse(template) {
    let body = [
      "eval(`var { ${Object.keys(vars).join(', ')} } = vars;`);",
      `out = [];`
    ];
    const tail = template.replace(/(.*?)<%(.*?)%>/sg, (_, content, code) => {
      body.push(`out.push(${JSON.stringify(content)});`);

      if (code.startsWith("="))
        body.push(`out.push(${code.substr(1)});`);

      return "";
    });

    body.push(`out.push(${JSON.stringify(tail)});`);
    body.push(`return out.join("");`);

    return new Function('vars', body.join("\n"))
  }
};

テストのテンプレートの場合、この関数は次のような関数を返します:

function(vars) {
  eval(`var { ${Object.keys(vars).join(', ')} } = vars;`);
  out = [];
  out.push("Hello, ");
  out.push(name);
  out.push("");
  return out.join("");
}

このコードのもう 1 つの注目すべき部分は、eval ステートメントです。テンプレートが vars 内の任意の変数 (この例では name) を参照できるようにするには、vars 内のプロパティを関数のローカル変数として使用できるようにする必要があります。

コンパイル中に可能な変数を決定する簡単な方法はないので、実行時にそれらを生成します。実行時に任意のローカル変数を割り当てるために私が知っている唯一の方法は、evalを使用することです。

// request
{
  method: "GET",
  path: "/profile/1234",
  cookies: { "user-id": 54 },
}

// response
{
  status: 200,
  template: "public-profile-show",
  variables: {
    user: {
      id: 54,
      name: "John Doe",
    },
    posts: [
      { id: 55412, message: "Have you seen the new iThing?",
        createdAt: 1699788972 }
    ]
  }
}
別の方法は、スタイトメントを使用することです。これは落胆しています。とにかく試してみましょう。


({ name }) => `Hello ${escapeHtml(name)}`;
生成された関数は完全に機能します。残念なことに、この機能は、あなたが尋ねる人に応じて、落胆、遺産、または非推奨です。これまでのところ、私の選択肢は邪悪な評価または非推奨です。理想的には、コンパイル時に使用される変数を決定したいのですが、これには、使用される変数を決定するためにJavaScriptコードをコンパイルする必要があります。

Plain Nodejsを使用してJavaScriptの一部の抽象的な構文ツリーを取得する簡単な方法はありません。

HTMLエンティティを逃れ、If/elseステートメントをサポートし、マイナーな修正を追加してください。

また、さらにいくつかのテストを追加しました。


it("simple template", () => {
  const fn = Template.parse("Hello, <%= name %>");
  assert.equal(fn({ name: "world" }), "Hello, world");
});
含めることを許可するには、ディレクトリ内のすべてのテンプレートファイルを解析する関数を追加します。この関数は、テンプレート名のある辞書をキーとして保持し、それらの解析されたテンプレートは値として機能します。

src/template.mjs

const Template = {
  parse(template) {
    let body = [
      "eval(`var { ${Object.keys(vars).join(', ')} } = vars;`);",
      `out = [];`
    ];
    const tail = template.replace(/(.*?)<%(.*?)%>/sg, (_, content, code) => {
      body.push(`out.push(${JSON.stringify(content)});`);

      if (code.startsWith("="))
        body.push(`out.push(${code.substr(1)});`);

      return "";
    });

    body.push(`out.push(${JSON.stringify(tail)});`);
    body.push(`return out.join("");`);

    return new Function('vars', body.join("\n"))
  }
};
テスト/テン​​プレート/**。ejs

test/template.test.mjs

function(vars) {
  eval(`var { ${Object.keys(vars).join(', ')} } = vars;`);
  out = [];
  out.push("Hello, ");
  out.push(name);
  out.push("");
  return out.join("");
}
次に、このテンプレートエンジンをmain.mjsファイルに統合して、.ejsテンプレートを使用してテンプレートをレンダリングします。

テンプレート/home.ejs

var { foo } = { foo: 1 };
// foo = 1
eval('var { bar } = { bar: 2 }');
// bar = 2

src/main.mjs

function(vars) {
  with (vars) {
    out = [];
    out.push("Hello, ");
    out.push(name);
    out.push("");
    return out.join("");
  }
}

次のブログで継続するアプリケーションの作成を開始する準備が整いました

// Template.parse
let body = [
  "eval(`var { ${Object.keys(vars).join(', ')} } = vars;`);",
  `out = [];`
];
const tail = template.replace(/(.*?)<%(.*?)%>/sg, (_, content, code) => {
  if (content)
    body.push(`out.push(${JSON.stringify(content)});`);

  if (code.startsWith("="))
    body.push(`out.push(escapeHtml(${code.substr(1)}));`);
  else if (code.startsWith("-"))
    body.push(`out.push(${code.substr(1)});`);
  else
    body.push(code);

  return "";
});

if (tail.length > 0) body.push(`out.push(${JSON.stringify(tail)});`);

body.push(`return out.join("");`);
body = body.join("\n");

const fn = new Function('vars', body);
return (vars) => fn({ ...vars, ...Template.locals });

// Template.locals
locals: {
  escapeHtml: (str) => `${str}`.replace(/[<>&"']/g, s =>
    ({ "<": "<", ">": ">", "&": "&amp;", '"': "&quot;", "'": "&#39;" })[s])
}

以上が依存関係のないテンプレート言語の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。