ホームページ >ウェブフロントエンド >jsチュートリアル >依存関係のないテンプレート言語
このブログは、私のブルーバードブログのパート 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
function(vars) { eval(`var { ${Object.keys(vars).join(', ')} } = vars;`); out = []; out.push("Hello, "); out.push(name); out.push(""); return out.join(""); }次に、このテンプレートエンジンをmain.mjsファイルに統合して、.ejsテンプレートを使用してテンプレートをレンダリングします。
var { foo } = { foo: 1 }; // foo = 1 eval('var { bar } = { bar: 2 }'); // bar = 2
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 => ({ "<": "<", ">": ">", "&": "&", '"': """, "'": "'" })[s]) }
以上が依存関係のないテンプレート言語の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。