.trim()Bevor wir uns mit den beiden Features Template Literal und Tagged Template beschäftigen, untersuchen wir zunächst die verschiedenen Bedeutungen des Begriffs Template.
Die folgenden drei Dinge sind erheblich unterschiedlich, obwohl sie alle Template in ihren Namen tragen und obwohl sie alle ähnlich aussehen.
Ein Text-Template ist eine Funktion von Daten zu Text. Es wird häufig in der Webentwicklung verwendet und oft über Textdateien definiert. Zum Beispiel definiert der folgende Text ein Template für die Bibliothek Handlebars.
<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{body}}
</div>
</div>Dieses Template hat zwei Platzhalter, die gefüllt werden müssen: title und body. Es wird wie folgt verwendet:
// First step: retrieve the template text, e.g. from a text file.
const tmplFunc = Handlebars.compile(TMPL_TEXT); // compile string
const data = {title: 'My page', body: 'Welcome to my page!'};
const html = tmplFunc(data);Ein Template Literal ist ähnlich wie ein String-Literal, hat aber zusätzliche Features – zum Beispiel Interpolation. Es wird durch Backticks begrenzt.
const num = 5;
assert.equal(`Count: ${num}!`, 'Count: 5!');Syntaktisch ist ein Tagged Template ein Template-Literal, dem eine Funktion (oder besser gesagt, ein Ausdruck, der zu einer Funktion ausgewertet wird) folgt. Dies führt dazu, dass die Funktion aufgerufen wird. Ihre Argumente werden aus dem Inhalt des Template-Literals abgeleitet.
const getArgs = (...args) => args;
assert.deepEqual(
getArgs`Count: ${5}!`,
[['Count: ', '!'], 5] );Beachten Sie, dass getArgs() sowohl den Text des Literals als auch die über ${} interpolierten Daten erhält.
Ein Template-Literal hat im Vergleich zu einem normalen String-Literal zwei neue Features.
Erstens unterstützt es String-Interpolation: Wenn wir einen dynamisch berechneten Wert innerhalb von ${} platzieren, wird er in einen String umgewandelt und in den von dem Literal zurückgegebenen String eingefügt.
const MAX = 100;
function doSomeWork(x) {
if (x > MAX) {
throw new Error(`At most ${MAX} allowed: ${x}!`);
}
// ···
}
assert.throws(
() => doSomeWork(101),
{message: 'At most 100 allowed: 101!'});Zweitens können Template-Literale mehrere Zeilen umfassen.
const str = `this is
a text with
multiple lines`;Template-Literale erzeugen immer Strings.
Der Ausdruck in Zeile A ist ein Tagged Template. Er ist gleichbedeutend mit dem Aufruf von tagFunc() mit den in Zeile B aufgeführten Argumenten im Array.
function tagFunc(...args) {
return args;
}
const setting = 'dark mode';
const value = true;
assert.deepEqual(
tagFunc`Setting ${setting} is ${value}!`, // (A)
[['Setting ', ' is ', '!'], 'dark mode', true] // (B)
);Die Funktion tagFunc vor dem ersten Backtick wird als Tag-Funktion bezeichnet. Ihre Argumente sind:
${} umgeben.['Setting ', ' is ', '!']'dark mode' und trueDie statischen (festen) Teile des Literals (die Template Strings) werden von den dynamischen Teilen (den Substitutions) getrennt gehalten.
Eine Tag-Funktion kann beliebige Werte zurückgeben.
Bisher haben wir nur die cooked Interpretation von Template Strings gesehen. Tag-Funktionen erhalten jedoch tatsächlich zwei Interpretationen:
Eine cooked Interpretation, bei der Backslashes eine besondere Bedeutung haben. Zum Beispiel erzeugt \t ein Tabulatorzeichen. Diese Interpretation der Template Strings wird als Array im ersten Argument gespeichert.
Eine raw Interpretation, bei der Backslashes keine besondere Bedeutung haben. Zum Beispiel erzeugt \t zwei Zeichen – einen Backslash und ein t. Diese Interpretation der Template Strings wird in der Eigenschaft .raw des ersten Arguments (eines Arrays) gespeichert.
Die Raw-Interpretation ermöglicht Raw String Literale über String.raw (später beschrieben) und ähnliche Anwendungen.
Die folgende Tag-Funktion cookedRaw verwendet beide Interpretationen:
function cookedRaw(templateStrings, ...substitutions) {
return {
cooked: Array.from(templateStrings), // copy only Array elements
raw: templateStrings.raw,
substitutions,
};
}
assert.deepEqual(
cookedRaw`\tab${'subst'}\newline\\`,
{
cooked: ['\tab', '\newline\\'],
raw: ['\\tab', '\\newline\\\\'],
substitutions: ['subst'],
});Wir können auch Unicode-Codepunkt-Escapes (\u{1F642}), Unicode-Codierungs-Escapes (\u03A9) und ASCII-Escapes (\x52) in Tagged Templates verwenden.
assert.deepEqual(
cookedRaw`\u{54}\u0065\x78t`,
{
cooked: ['Text'],
raw: ['\\u{54}\\u0065\\x78t'],
substitutions: [],
});Wenn die Syntax eines dieser Escapes nicht korrekt ist, ist der entsprechende cooked Template String undefined, während die Raw-Version immer noch unverändert ist.
assert.deepEqual(
cookedRaw`\uu\xx ${1} after`,
{
cooked: [undefined, ' after'],
raw: ['\\uu\\xx ', ' after'],
substitutions: [1],
});Falsche Escapes führen zu Syntaxfehlern in Template-Literalen und String-Literalen. Vor ES2018 führten sie auch in Tagged Templates zu Fehlern. Warum wurde das geändert? Wir können jetzt Tagged Templates für Text verwenden, der zuvor illegal war – zum Beispiel:
windowsPath`C:\uuu\xxx\111`
latex`\unicode`
Tagged Templates eignen sich hervorragend zur Unterstützung kleiner eingebetteter Sprachen (sogenannte domänenspezifische Sprachen). Wir fahren mit einigen Beispielen fort.
lit-html ist eine Templating-Bibliothek, die auf Tagged Templates basiert und von dem Frontend-Framework Polymer verwendet wird.
import {html, render} from 'lit-html';
const template = (items) => html`
<ul>
${
repeat(items,
(item) => item.id,
(item, index) => html`<li>${index}. ${item.name}</li>`
)
}
</ul>
`;repeat() ist eine benutzerdefinierte Funktion zum Schleifen. Ihr zweiter Parameter erzeugt eindeutige Schlüssel für die vom dritten Parameter zurückgegebenen Werte. Beachten Sie das verschachtelte Tagged Template, das von diesem Parameter verwendet wird.
re-template-tag ist eine einfache Bibliothek zum Komponieren von regulären Ausdrücken. Mit re getaggte Templates erzeugen reguläre Ausdrücke. Der Hauptvorteil besteht darin, dass wir reguläre Ausdrücke und einfachen Text über ${} interpolieren können (Zeile A).
const RE_YEAR = re`(?<year>[0-9]{4})`;
const RE_MONTH = re`(?<month>[0-9]{2})`;
const RE_DAY = re`(?<day>[0-9]{2})`;
const RE_DATE = re`/${RE_YEAR}-${RE_MONTH}-${RE_DAY}/u`; // (A)
const match = RE_DATE.exec('2017-01-27');
assert.equal(match.groups.year, '2017');Die Bibliothek graphql-tag ermöglicht es uns, GraphQL-Abfragen über Tagged Templates zu erstellen.
import gql from 'graphql-tag';
const query = gql`
{
user(id: 5) {
firstName
lastName
}
}
`;Zusätzlich gibt es Plugins zum Vorverkompilieren solcher Abfragen in Babel, TypeScript usw.
Raw String Literale werden über die Tag-Funktion String.raw implementiert. Sie sind String-Literale, bei denen Backslashes keine besondere Funktion haben (wie das Escapen von Zeichen usw.).
assert.equal(String.raw`\back`, '\\back');Dies hilft immer dann, wenn Daten Backslashes enthalten – zum Beispiel Strings mit regulären Ausdrücken.
const regex1 = /^\./;
const regex2 = new RegExp('^\\.');
const regex3 = new RegExp(String.raw`^\.`);Alle drei regulären Ausdrücke sind äquivalent. Bei einem normalen String-Literal müssen wir den Backslash zweimal schreiben, um ihn für dieses Literal zu escapen. Bei einem Raw String Literal müssen wir das nicht tun.
Raw String Literale sind auch nützlich für die Angabe von Windows-Dateipfaden.
const WIN_PATH = String.raw`C:\foo\bar`;
assert.equal(WIN_PATH, 'C:\\foo\\bar');Alle verbleibenden Abschnitte sind fortgeschritten.
Wenn wir mehrzeiligen Text in Template-Literale einfügen, stehen zwei Ziele im Konflikt: Einerseits sollte das Template-Literal eingerückt sein, damit es in den Quellcode passt. Andererseits sollten die Zeilen seines Inhalts in der linken Spalte beginnen.
Zum Beispiel
function div(text) {
return `
<div>
${text}
</div>
`;
}
console.log('Output:');
console.log(
div('Hello!')
// Replace spaces with mid-dots:
.replace(/ /g, '·')
// Replace \n with #\n:
.replace(/\n/g, '#\n')
);Aufgrund der Einrückung passt das Template-Literal gut in den Quellcode. Leider wird auch die Ausgabe eingerückt. Und wir wollen den Zeilenumbruch am Anfang und den Zeilenumbruch plus zwei Leerzeichen am Ende nicht.
Output:
#
····<div>#
······Hello!#
····</div>#
··Es gibt zwei Möglichkeiten, dies zu beheben: über ein Tagged Template oder durch Trimming des Ergebnisses des Template-Literals.
Die erste Lösung besteht darin, einen benutzerdefinierten Template-Tag zu verwenden, der unerwünschte Leerzeichen entfernt. Er verwendet die erste Zeile nach dem anfänglichen Zeilenumbruch, um zu bestimmen, in welcher Spalte der Text beginnt, und kürzt die Einrückung überall. Er entfernt auch den Zeilenumbruch ganz am Anfang und die Einrückung ganz am Ende. Ein solcher Template-Tag ist dedent von Desmond Brand.
import dedent from 'dedent';
function divDedented(text) {
return dedent`
<div>
${text}
</div>
`.replace(/\n/g, '#\n');
}
console.log('Output:');
console.log(divDedented('Hello!'));Diesmal ist die Ausgabe nicht eingerückt.
Output:
<div>#
Hello!#
</div>.trim()Die zweite Lösung ist schneller, aber auch schmutziger.
function divDedented(text) {
return `
<div>
${text}
</div>
`.trim().replace(/\n/g, '#\n');
}
console.log('Output:');
console.log(divDedented('Hello!'));Die String-Methode .trim() entfernt die überflüssigen Leerzeichen am Anfang und am Ende, aber der Inhalt selbst muss in der linken Spalte beginnen. Der Vorteil dieser Lösung ist, dass wir keine benutzerdefinierte Tag-Funktion benötigen. Der Nachteil ist, dass es hässlich aussieht.
Die Ausgabe ist dieselbe wie bei dedent.
Output:
<div>#
Hello!#
</div>Obwohl Template-Literale wie Text-Templates aussehen, ist nicht sofort ersichtlich, wie man sie für (Text-)Templating verwendet: Ein Text-Template erhält seine Daten aus einem Objekt, während ein Template-Literal seine Daten aus Variablen erhält. Die Lösung besteht darin, ein Template-Literal im Body einer Funktion zu verwenden, deren Parameter die Templating-Daten empfängt – zum Beispiel:
const tmpl = (data) => `Hello ${data.name}!`;
assert.equal(tmpl({name: 'Jane'}), 'Hello Jane!');Als komplexeres Beispiel möchten wir ein Array von Adressen nehmen und eine HTML-Tabelle erzeugen. Dies ist das Array:
const addresses = [
{ first: '<Jane>', last: 'Bond' },
{ first: 'Lars', last: '<Croft>' },
];Die Funktion tmpl(), die die HTML-Tabelle erzeugt, sieht wie folgt aus:
const tmpl = (addrs) => `
<table>
${addrs.map(
(addr) => `
<tr>
<td>${escapeHtml(addr.first)}</td>
<td>${escapeHtml(addr.last)}</td>
</tr>
`.trim()
).join('')}
</table>
`.trim();Dieser Code enthält zwei Templating-Funktionen:
addrs, ein Array mit Adressen, und gibt einen String mit einer Tabelle zurück.addr, ein Objekt, das eine Adresse enthält, und gibt einen String mit einer Tabellenzeile zurück. Beachten Sie .trim() am Ende, das unnötige Leerzeichen entfernt.Die erste Templating-Funktion erzeugt ihr Ergebnis, indem sie ein Tabellenelement um ein Array wickelt, das sie zu einem String zusammenfügt (Zeile 10). Dieses Array wird erzeugt, indem die zweite Templating-Funktion auf jedes Element von addrs angewendet wird (Zeile 3). Es enthält daher Strings mit Tabellenzeilen.
Die Hilfsfunktion escapeHtml() wird verwendet, um spezielle HTML-Zeichen zu escapen (Zeile 6 und Zeile 7). Ihre Implementierung ist in der nächsten Unterabschnitt gezeigt.
Rufen wir tmpl() mit den Adressen auf und protokollieren das Ergebnis:
console.log(tmpl(addresses));Die Ausgabe ist:
<table>
<tr>
<td><Jane></td>
<td>Bond</td>
</tr><tr>
<td>Lars</td>
<td><Croft></td>
</tr>
</table>Die folgende Funktion escapet einfachen Text, damit er in HTML unverändert angezeigt wird:
function escapeHtml(str) {
return str
.replace(/&/g, '&') // first!
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/`/g, '`')
;
}
assert.equal(
escapeHtml('Rock & Roll'), 'Rock & Roll');
assert.equal(
escapeHtml('<blank>'), '<blank>'); Übung: HTML-Templating
Übung mit Bonus-Herausforderung: exercises/template-literals/templating_test.mjs
Quiz
Siehe Quiz-App.