Rumah > hujung hadapan web > tutorial js > Bahasa templat tanpa kebergantungan

Bahasa templat tanpa kebergantungan

Patricia Arquette
Lepaskan: 2025-01-27 20:32:13
asal
459 orang telah melayarinya

Template language without dependencies

Bahasa Templat

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 }
    ]
  }
}
Salin selepas log masuk
Salin selepas log masuk

Dalam blog ini, saya akan melaksanakan bahasa templat ini.

Mereka bentuk bahasa templat

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)}`;
Salin selepas log masuk
Salin selepas log masuk

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.

Pelaksanaannya

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");
});
Salin selepas log masuk
Salin selepas log masuk

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"))
  }
};
Salin selepas log masuk
Salin selepas log masuk

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("");
}
Salin selepas log masuk
Salin selepas log masuk

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 }
    ]
  }
}
Salin selepas log masuk
Salin selepas log masuk

Kaedah lain ialah menggunakan pernyataan dengan, yang tidak digalakkan. Mari cuba juga.

({ name }) => `Hello ${escapeHtml(name)}`;
Salin selepas log masuk
Salin selepas log masuk

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");
});
Salin selepas log masuk
Salin selepas log masuk

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"))
  }
};
Salin selepas log masuk
Salin selepas log masuk

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.

src/template.mjs

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

ujian/templat/**. ejs

var { foo } = { foo: 1 };
// foo = 1
eval('var { bar } = { bar: 2 }');
// bar = 2
Salin selepas log masuk

ujian/template.test.mjs

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

Sekarang untuk mengintegrasikan enjin templat ini dalam fail main.mjs untuk menjadikan templat menggunakan templat .ejs.

templat/home.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 =>
    ({ "<": "<", ">": ">", "&": "&amp;", '"': "&quot;", "'": "&#39;" })[s])
}
Salin selepas log masuk

src/main.mjs

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> &amp; &quot; &#39;");
  });
});

function delws(str) {
  return str.replace(/^\s+|\s+$/g, "").replace(/\s+/g, " ");
}
Salin selepas log masuk
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;
  }
}
Salin selepas log masuk

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!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Artikel terbaru oleh pengarang
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan