Heim > Web-Frontend > js-Tutorial > Vorlagensprache ohne Abhängigkeiten

Vorlagensprache ohne Abhängigkeiten

Patricia Arquette
Freigeben: 2025-01-27 20:32:13
Original
459 Leute haben es durchsucht

Template language without dependencies

Die Vorlagensprache

Dieser Blog ist Teil 2 meines Bluebird-Blogs. Siehe Teil 1

Für meinen Twitter-Klon ohne Abhängigkeiten habe ich beschlossen, meine Routenhandler so zu gestalten, dass sie Variablen und einen Vorlagennamen zurückgeben. Dies ermöglicht ein einfaches Testen, da meine Tests lediglich Aussagen zum Vorlagennamen und zu Variablen machen können, anstatt ein HTML-Dokument zu überprüfen.

// 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 }
    ]
  }
}
Nach dem Login kopieren
Nach dem Login kopieren

In diesem Blog werde ich diese Vorlagensprache implementieren.

Entwerfen der Vorlagensprache

Die Vorlagensprache, die ich benötige, muss HTML-Dokumente mit nur einer Reihe von Variablen als Eingabe ausgeben. Ich möchte, dass eine Vorlage zu einer JS-Funktion kompiliert wird. Ich möchte zum Beispiel Hallo <%= Name %> um etwas wie folgt zu kompilieren:

({ name }) => `Hello ${escapeHtml(name)}`;
Nach dem Login kopieren
Nach dem Login kopieren

Ich wähle ein klassisches <%= %> Syntax, weil sie unglaublich verbreitet und bekannt ist. Die meisten Entwickler, die diese Syntax sehen, werden intuitiv wissen, dass Sie dort einfach regulären Code schreiben können und die Ausgabe des Codes zur Ausgabe hinzugefügt wird.

Es muss Variablen und automatische Escape-HTML-Entitäten unterstützen. Schleifen, if/else-Anweisungen und das Einbinden anderer Vorlagen müssen ebenfalls unterstützt werden. Es wäre schön, wenn wir beliebige Funktionen aufrufen und einige grundlegende Berechnungen durchführen könnten;

Im Grunde möchte ich, dass es beliebiges Javascript ausführen kann.

Die Umsetzung

Ich schätze, ich fange einfach an, Code zu schreiben und schaue, wo ich am Ende lande. Zuerst ein Test.

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

Die einfachste Implementierung, die ich mir vorstellen kann, ist die Verwendung eines regulären Ausdrucks. Alle Inhalte außerhalb von <%= %> wird einfach zur Ausgabe hinzugefügt und der Inhalt dazwischen wird als JS ausgeführt.

Der verwendete reguläre Ausdruck ist /(.*?)<%(.*?)%>/sg. Dieser reguläre Ausdruck erfasst jeden Text bis zum ersten <%, den er mit (.*?)<% findet. Anschließend wird jeglicher Text erfasst, bis %> mit (.*?)%>. Der s-Modifikator ermöglicht die . (Punkt) passend zu Zeilenumbrüchen. Der g-Modifikator ermöglicht mehrere Übereinstimmungen.

Die Ersetzungsfunktion von Javascript für Zeichenfolgen ermöglicht die Ausführung von Code für jede Übereinstimmung und gibt gleichzeitig einen Ersatzwert zurück, „“ in meinem Code. Da jede Übereinstimmung durch eine leere Zeichenfolge ersetzt wird, wird nur der Text nach dem letzten %> wird von der Ersetzungsfunktion zurückgegeben, die ich Tail nenne.

Ich verwende JSON.stringify, um ein String-Literal zu erstellen.

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"))
  }
};
Nach dem Login kopieren
Nach dem Login kopieren

Für die Vorlage im Test gibt diese Funktion eine Funktion wie diese zurück:

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

Ein weiterer bemerkenswerter Teil dieses Codes ist die eval-Anweisung. Damit die Vorlage auf jede Variable in vars (Name in diesem Beispiel) verweisen kann, muss ich die Eigenschaften in vars als lokale Variablen in der Funktion verfügbar machen.

Es gibt keine einfache Möglichkeit, die möglichen Variablen beim Kompilieren zu bestimmen. Daher generiere ich sie zur Laufzeit. Ich weiß nur, dass ich zur Laufzeit willkürliche lokale Variablen zugewiesen werde, die eval.
verwenden

// 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 }
    ]
  }
}
Nach dem Login kopieren
Nach dem Login kopieren

Eine andere Methode besteht darin, das entmutigte Withement zu verwenden. Versuchen wir es trotzdem.

({ name }) => `Hello ${escapeHtml(name)}`;
Nach dem Login kopieren
Nach dem Login kopieren

Die generierte Funktion funktioniert perfekt. Schade, dass das Feature entmutigt, Vermächtnis oder veraltet ist, je nachdem, wen Sie fragen. Bisher sind meine Optionen böse bewertet oder veraltet. Im Idealfall möchte ich die zur Kompilierungszeit verwendeten Variablen bestimmen. Dies erfordert jedoch das Kompilieren des JavaScript-Codes, um die verwendeten Variablen zu bestimmen.

Es gibt keine einfache Möglichkeit, einen abstrakten Syntaxbaum eines Stücks JavaScript mit einfachem Knotenjs zu erhalten.

Um HTML -Entitäten zu entkommen, unterstützen Sie, ob/sonst Anweisungen hinzugefügt werden und einige kleinere Korrekturen hinzufügen.

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

Ich habe auch einige weitere Tests hinzugefügt.

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"))
  }
};
Nach dem Login kopieren
Nach dem Login kopieren

Um einzubeziehen, füge ich eine Funktion hinzu, um alle Vorlagendateien in einem Verzeichnis zu analysieren. Diese Funktion führt ein Wörterbuch mit Vorlagennamen als Schlüssel und deren Parsen -Vorlagenfunktionen als Werte.

src/template.mjs

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

Test/Vorlagen/**. EJS

var { foo } = { foo: 1 };
// foo = 1
eval('var { bar } = { bar: 2 }');
// bar = 2
Nach dem Login kopieren

test/template.test.mjs

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

nun, um diese Template -Engine in die main.mjs -Datei zu integrieren, um die Vorlage mit den .ejs -Vorlagen zu rendern.

Vorlagen/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])
}
Nach dem Login kopieren

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, " ");
}
Nach dem Login kopieren
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;
  }
}
Nach dem Login kopieren

Jetzt sind wir bereit, unsere Bewerbung zu schreiben, die im nächsten Blog fortgesetzt wird

Das obige ist der detaillierte Inhalt vonVorlagensprache ohne Abhängigkeiten. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Artikel des Autors
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage