Blog ini adalah bahagian 2 blog bluebird saya. Lihat bahagian 1
Untuk klon twitter saya tanpa kebergantungan, saya telah memutuskan untuk mereka bentuk pengendali laluan saya dengan cara mereka akan mengembalikan pembolehubah dan nama templat. Ini membolehkan ujian mudah, kerana ujian saya hanya boleh membuat penegasan pada nama templat dan pembolehubah daripada memeriksa dokumen 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 } ] } }
Dalam blog ini, saya akan melaksanakan bahasa templat ini.
Bahasa templat yang saya perlukan, perlu mengeluarkan dokumen HTML dengan hanya satu set pembolehubah sebagai input. Saya mahu templat dikompilasi ke fungsi JS. Sebagai contoh, saya mahu Hello <%= name %> untuk menyusun kepada sesuatu seperti ini:
({ name }) => `Hello ${escapeHtml(name)}`;
Saya akan pergi dengan klasik <%= %> sintaks, kerana ia sangat biasa dan terkenal. Kebanyakan pembangun yang melihat sintaks ini secara intuitif akan mengetahui bahawa anda hanya boleh menulis kod biasa di sana dan output kod akan ditambahkan pada output.
Ia mesti menyokong pembolehubah dan entiti HTML auto-escape. Gelung, pernyataan if/else dan termasuk templat lain juga mesti disokong. Alangkah baiknya jika kita boleh menggunakan fungsi sewenang-wenangnya dan melakukan beberapa matematik asas;
Jadi pada asasnya, saya mahu ia dapat melaksanakan javascript sewenang-wenangnya.
Saya rasa saya baru mula menulis kod dan melihat di mana saya berakhir. Pertama, ujian.
it("simple template", () => { const fn = Template.parse("Hello, <%= name %>"); assert.equal(fn({ name: "world" }), "Hello, world"); });
Pelaksanaan paling mudah yang saya boleh fikirkan ialah menggunakan ungkapan biasa. Semua kandungan di luar <%= %> hanya akan ditambahkan pada output, dan kandungan antara akan dilaksanakan sebagai JS.
Ungkapan biasa yang digunakan ialah /(.*?)<%(.*?)%>/sg. Ungkapan biasa ini menangkap sebarang teks sehingga <% pertama yang ditemuinya menggunakan (.*?)<%. Kemudian ia menangkap sebarang teks sehingga %> menggunakan (.*?)%>. Pengubah suai s membenarkan . (titik) untuk memadankan baris baharu. Pengubah suai g membenarkan berbilang padanan.
Fungsi ganti Javascript pada rentetan membenarkan melaksanakan kod untuk setiap padanan sambil turut mengembalikan nilai gantian, "" dalam kod saya. Kerana setiap padanan digantikan dengan rentetan kosong, hanya teks selepas %> dikembalikan oleh fungsi ganti, yang saya panggil ekor.
Saya menggunakan JSON.stringify untuk mencipta rentetan literal.
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")) } };
Untuk templat dalam ujian, fungsi ini mengembalikan fungsi seperti ini:
function(vars) { eval(`var { ${Object.keys(vars).join(', ')} } = vars;`); out = []; out.push("Hello, "); out.push(name); out.push(""); return out.join(""); }
Satu lagi bahagian penting kod ini ialah pernyataan eval. Untuk membenarkan templat merujuk kepada mana-mana pembolehubah dalam vars (nama dalam contoh ini), saya perlu menjadikan sifat dalam vars tersedia sebagai pembolehubah tempatan dalam fungsi.
tidak ada cara mudah untuk menentukan pembolehubah yang mungkin semasa menyusun, jadi saya menghasilkannya semasa runtime. Satu -satunya cara yang saya tahu untuk memberikan pembolehubah tempatan sewenang -wenang pada masa runtime, adalah menggunakan 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 } ] } }
Kaedah lain ialah menggunakan pernyataan dengan, yang tidak digalakkan. Mari cuba juga.
({ name }) => `Hello ${escapeHtml(name)}`;
Fungsi yang dihasilkan berfungsi dengan sempurna. Terlalu buruk ciri ini tidak digalakkan, warisan atau ditutup, bergantung kepada siapa yang anda minta. Setakat ini pilihan saya adalah eval atau evolusi jahat. Sebaik-baiknya, saya ingin menentukan pembolehubah yang digunakan pada masa kompilasi, tetapi ini memerlukan menyusun kod JavaScript untuk menentukan pembolehubah yang digunakan.
tidak ada cara mudah untuk mendapatkan pokok sintaks abstrak dari sekeping javascript menggunakan nodej biasa.
sekarang untuk melarikan diri dari entiti HTML, menyokong jika/pernyataan lain dan tambahkan beberapa perbaikan kecil.
it("simple template", () => { const fn = Template.parse("Hello, <%= name %>"); assert.equal(fn({ name: "world" }), "Hello, world"); });
Saya juga menambah beberapa ujian lagi.
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")) } };
untuk membenarkan termasuk, saya akan menambah fungsi untuk menghuraikan semua fail templat dalam direktori. Fungsi ini akan menyimpan kamus dengan nama templat sebagai kunci dan fungsi templat mereka yang dihuraikan sebagai nilai.
function(vars) { eval(`var { ${Object.keys(vars).join(', ')} } = vars;`); out = []; out.push("Hello, "); out.push(name); out.push(""); return out.join(""); }
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(""); } }
Sekarang untuk mengintegrasikan enjin templat ini dalam fail main.mjs untuk menjadikan templat menggunakan templat .ejs.
// 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]) }
describe("Template.parse", () => { it("simple template", () => { const fn = Template.parse("Hello, <%= name %>"); assert.equal(fn({ name: "world" }), "Hello, world"); }); it("math template", () => { const fn = Template.parse("1 + 1 = <%= 1 + 1 %>"); assert.equal(fn({}), "1 + 1 = 2"); }); it("function template", () => { const fn = Template.parse("Hello <%= foo() %>"); assert.equal(fn({ foo: () => "world" }), "Hello world"); }); it("if-else template", () => { const fn = Template.parse(`Answer: <% if (answer) { %>Yes<% } else { %>No<% } %>`); assert.deepEqual( [fn({ answer: true }), fn({ answer: false })], ["Answer: Yes", "Answer: No"]); }); it("multiline template", () => { const fn = Template.parse(` Answer: <% if (answer) { %> Yes <% } else { %> No <% } %> `); assert.deepEqual( [delws(fn({ answer: true })), delws(fn({ answer: false }))], ["Answer: Yes", "Answer: No"]); }); it("escape html", () => { const fn = Template.parse("Hello, <%= name %>"); assert.equal(fn({ name: "<script> & \" '" }), "Hello, <script> & " '"); }); }); function delws(str) { return str.replace(/^\s+|\s+$/g, "").replace(/\s+/g, " "); }
export const Template = { /** * * @param {string} path Directory containing one or more template files * @returns {Promise<(template: string, vars: Record<string, any>) => string>} */ async parseDirectory(path) { /** @type {Map<string, function>} */ const templates = new Map(); const include = (templateName, vars) => { const template = templates.get(templateName); if (!template) throw new Error(`Template ${path}/${templateName} does not exist, ` + `templates found: ${Array.from(templates.keys()).join(", ")}`); return template({ ...vars, include }); }; const readDir = async (prefix) => { const innerPath = join(path, prefix); const fileNames = await promises.readdir(join(innerPath)); for (const fileName of fileNames) { const templateName = join(prefix, fileName); const filePath = join(innerPath, fileName); if (fileName.endsWith(".ejs")) { const body = await promises.readFile(filePath, { encoding: "utf-8" }); templates.set(templateName, Template.parse(body, { filePath })); } else if ((await promises.stat(filePath)).isDirectory) { await readDir(join(prefix, fileName)); } } }; await readDir(""); return include; } }
Sekarang kami sudah bersedia untuk menulis permohonan kami, yang akan diteruskan di blog seterusnya
Atas ialah kandungan terperinci Bahasa templat tanpa kebergantungan. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!