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 } ] } }
In diesem Blog werde ich diese Vorlagensprache implementieren.
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)}`;
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.
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"); });
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")) } };
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(""); }
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 } ] } }
Eine andere Methode besteht darin, das entmutigte Withement zu verwenden. Versuchen wir es trotzdem.
({ name }) => `Hello ${escapeHtml(name)}`;
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"); });
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")) } };
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.
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(""); } }
nun, um diese Template -Engine in die main.mjs -Datei zu integrieren, um die Vorlage mit den .ejs -Vorlagen zu rendern.
// 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; } }
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!