.trim()
在深入探讨*模板字面量*和*标签模板*这两个特性之前,让我们先来看看*模板*一词的多重含义。
以下三件事尽管名称中都带有*模板*,而且看起来都很相似,但它们之间存在着显著差异
*文本模板*是从数据到文本的函数。它经常用于 Web 开发中,并且通常通过文本文件定义。例如,以下文本定义了库 Handlebars 的模板
<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{body}}</div>
</div>
此模板有两个要填写的空白:title
和 body
。它的用法如下
// 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);
*模板字面量*类似于字符串字面量,但具有一些额外的特性——例如,插值。它由反引号分隔
const num = 5;
.equal(`Count: ${num}!`, 'Count: 5!'); assert
从语法上讲,*标签模板*是跟在函数(或者更确切地说,是求值为函数的表达式)后面的模板字面量。这会导致调用该函数。它的参数是从模板字面量的内容派生出来的。
const getArgs = (...args) => args;
.deepEqual(
assertgetArgs`Count: ${5}!`,
'Count: ', '!'], 5] ); [[
请注意,getArgs()
同时接收字面量的文本和通过 ${}
插值的数据。
与普通字符串字面量相比,模板字面量有两个新特性。
首先,它支持*字符串插值*:如果我们将一个动态计算的值放在 ${}
中,它将被转换为字符串并插入到字面量返回的字符串中。
const MAX = 100;
function doSomeWork(x) {
if (x > MAX) {
throw new Error(`At most ${MAX} allowed: ${x}!`);
}// ···
}.throws(
assert=> doSomeWork(101),
() message: 'At most 100 allowed: 101!'}); {
其次,模板字面量可以跨越多行
const str = `this is
a text with
multiple lines`;
模板字面量始终生成字符串。
A 行中的表达式是一个*标签模板*。它等效于使用 B 行中数组中列出的参数调用 tagFunc()
。
function tagFunc(...args) {
return args;
}
const setting = 'dark mode';
const value = true;
.deepEqual(
asserttagFunc`Setting ${setting} is ${value}!`, // (A)
'Setting ', ' is ', '!'], 'dark mode', true] // (B)
[[; )
第一个反引号之前的函数 tagFunc
被称为*标签函数*。它的参数是
${}
周围文本片段的数组。['Setting ', ' is ', '!']
'dark mode'
和 true
字面量的静态(固定)部分(模板字符串)与动态部分(替换)分开保存。
标签函数可以返回任意值。
到目前为止,我们只看到了模板字符串的*已处理解释*。但标签函数实际上有两种解释
*已处理解释*,其中反斜杠具有特殊含义。例如,\t
生成一个制表符。模板字符串的这种解释存储在第一个参数中的数组中。
*原始解释*,其中反斜杠没有特殊含义。例如,\t
生成两个字符——一个反斜杠和一个 t
。模板字符串的这种解释存储在第一个参数(数组)的属性 .raw
中。
原始解释可以通过 String.raw
实现原始字符串字面量 (稍后描述) 和类似的应用。
以下标签函数 cookedRaw
同时使用了两种解释
function cookedRaw(templateStrings, ...substitutions) {
return {
cooked: Array.from(templateStrings), // copy only Array elements
raw: templateStrings.raw,
,
substitutions;
}
}.deepEqual(
assertcookedRaw`\tab${'subst'}\newline\\`,
{cooked: ['\tab', '\newline\\'],
raw: ['\\tab', '\\newline\\\\'],
substitutions: ['subst'],
; })
我们还可以在标签模板中使用 Unicode 代码点转义符(\u{1F642}
)、Unicode 代码单元转义符(\u03A9
)和 ASCII 转义符(\x52
)
.deepEqual(
assertcookedRaw`\u{54}\u0065\x78t`,
{cooked: ['Text'],
raw: ['\\u{54}\\u0065\\x78t'],
substitutions: [],
; })
如果其中一个转义符的语法不正确,则相应的已处理模板字符串为 undefined
,而原始版本仍然是逐字的
.deepEqual(
assertcookedRaw`\uu\xx ${1} after`,
{cooked: [undefined, ' after'],
raw: ['\\uu\\xx ', ' after'],
substitutions: [1],
; })
不正确的转义符会在模板字面量和字符串字面量中产生语法错误。在 ES2018 之前,它们甚至会在标签模板中产生错误。为什么要改变呢?我们现在可以使用标签模板来处理以前非法的文本——例如
windowsPath`C:\uuu\xxx\111`
latex`\unicode`
标签模板非常适合支持小型嵌入式语言(所谓的*领域特定语言*)。我们将继续举几个例子。
lit-html 是一个基于标签模板的模板库,由 前端框架 Polymer 使用
import {html, render} from 'lit-html';
const template = (items) => html`
<ul>
${
repeat(items,
=> item.id,
(item) , index) => html`<li>${index}. ${item.name}</li>`
(item
)}
</ul>
`;
repeat()
是一个用于循环的自定义函数。它的第二个参数为第三个参数返回的值生成唯一的键。请注意该参数使用的嵌套标签模板。
re-template-tag 是一个用于组合正则表达式的简单库。用 re
标记的模板会生成正则表达式。主要好处是我们可以通过 ${}
插值正则表达式和纯文本(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');
.equal(match.groups.year, '2017'); assert
库 graphql-tag 允许我们通过标签模板创建 GraphQL 查询
import gql from 'graphql-tag';
const query = gql`
{
user(id: 5) {
firstName
lastName
}
}
`;
此外,还有一些插件可以在 Babel、TypeScript 等中预编译此类查询。
原始字符串字面量是通过标签函数 String.raw
实现的。它们是字符串字面量,其中反斜杠不做任何特殊处理(例如转义字符等)
.equal(String.raw`\back`, '\\back'); assert
每当数据包含反斜杠时,这都会有所帮助——例如,包含正则表达式的字符串
const regex1 = /^\./;
const regex2 = new RegExp('^\\.');
const regex3 = new RegExp(String.raw`^\.`);
所有三个正则表达式都是等效的。使用普通的字符串字面量,我们必须将反斜杠写两次,以便为该字面量转义它。使用原始字符串字面量,我们不必这样做。
原始字符串字面量也可用于指定 Windows 文件名路径
const WIN_PATH = String.raw`C:\foo\bar`;
.equal(WIN_PATH, 'C:\\foo\\bar'); assert
所有剩余部分都是高级内容
如果我们将多行文本放入模板字面量中,则有两个目标会发生冲突:一方面,应缩进模板字面量以使其适合源代码。另一方面,其内容的行应从最左侧的列开始。
例如
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')
; )
由于缩进,模板字面量很好地融入了源代码。唉,输出也被缩进了。我们不希望开头有回车符,结尾有回车符加两个空格。
Output:
#<div>#
····
······Hello!#</div>#
···· ··
有两种方法可以解决这个问题:通过标签模板或修剪模板字面量的结果。
第一个解决方法是使用自定义模板标签来删除不需要的空格。它使用初始换行符后的第一行来确定文本从哪一列开始,并在所有地方缩短缩进。它还会删除开头处的换行符和结尾处的缩进。一个这样的模板标签是 Desmond Brand 的 dedent
import dedent from 'dedent';
function divDedented(text) {
return dedent`
<div>
${text}
</div>
`.replace(/\n/g, '#\n');
}console.log('Output:');
console.log(divDedented('Hello!'));
这一次,输出没有缩进
Output:<div>#
Hello!#</div>
.trim()
第二个解决方法更快,但也更脏乱
function divDedented(text) {
return `
<div>
${text}
</div>
`.trim().replace(/\n/g, '#\n');
}console.log('Output:');
console.log(divDedented('Hello!'));
字符串方法 .trim()
会删除开头和结尾处多余的空格,但内容本身必须从最左侧的列开始。这种解决方案的优点是我们不需要自定义标签函数。缺点是它看起来很丑陋。
输出与使用 dedent
时相同
Output:<div>#
Hello!#</div>
虽然模板字面量看起来像文本模板,但如何将它们用于(文本)模板化并不一目了然:文本模板从对象获取数据,而模板字面量从变量获取数据。解决方案是在函数体中使用模板字面量,该函数的参数接收模板数据——例如
const tmpl = (data) => `Hello ${data.name}!`;
.equal(tmpl({name: 'Jane'}), 'Hello Jane!'); assert
作为一个更复杂的示例,我们想获取一个地址数组并生成一个 HTML 表格。这就是数组
const addresses = [
first: '<Jane>', last: 'Bond' },
{ first: 'Lars', last: '<Croft>' },
{ ; ]
生成 HTML 表格的函数 tmpl()
如下所示
const tmpl = (addrs) => `
<table>
${addrs.map(
(addr) => `
<tr>
<td>${escapeHtml(addr.first)}</td>
<td>${escapeHtml(addr.last)}</td>
</tr>
`.trim()
).join('')}
</table>
`.trim();
此代码包含两个模板函数
addrs
(一个包含地址的数组),并返回一个包含表格的字符串。addr
(一个包含地址的对象),并返回一个包含表格行的字符串。请注意结尾处的 .trim()
,它会删除不必要的空格。第一个模板函数通过将表格元素包裹在连接成字符串的数组周围来生成其结果(第 10 行)。该数组是通过将第二个模板函数映射到 addrs
的每个元素生成的(第 3 行)。因此,它包含带有表格行的字符串。
辅助函数 escapeHtml()
用于转义特殊的 HTML 字符(第 6 行和第 7 行)。它的实现显示在下一小节中。
让我们使用地址调用 tmpl()
并记录结果
console.log(tmpl(addresses));
输出为
<table>
<tr>
<td><Jane></td>
<td>Bond</td>
</tr><tr>
<td>Lars</td>
<td><Croft></td>
</tr>
</table>
以下函数会转义纯文本,以便在 HTML 中逐字显示
function escapeHtml(str) {
return str
.replace(/&/g, '&') // first!
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/`/g, '`')
;
}.equal(
assertescapeHtml('Rock & Roll'), 'Rock & Roll');
.equal(
assertescapeHtml('<blank>'), '<blank>');
练习:HTML 模板化
带奖励挑战的练习:exercises/template-literals/templating_test.mjs
测验
参见测验应用程序。