在我们深入研究两个特性*模板字面值* 和标签模板 之前,让我们首先检查术语模板 的多重含义。
19.1 消歧:“模板”
尽管所有名称中都有*模板* 并且所有这些模板看起来都相似,但以下三件事情有很大不同:
- 网页模板 是从数据到文本的函数。它经常用于 Web 开发,通常通过文本文件定义。例如,以下文本定义了库 Handlebars 的模板:
<div class="entry"> <h1>{{title}}</h1> <div class="body"> {{body}} </div> </div>
- 模板字面值 是具有更多功能的字符串字面值,例如插值。它由反引号分隔:
const num = 5; assert.equal(`Count: ${num}!`, 'Count: 5!');
- 标签模板 是一个函数,后跟一个模板字面值。它导致调用该函数,并将模板字面值的内容作为参数提供给它。
请注意,const getArgs = (...args) => args; assert.deepEqual( getArgs`Count: ${5}!`, [['Count: ', '!'], 5] );
getArgs()
接收字面值的文本和通过${}
插入的数据。
19.2 模板字面值
与普通字符串字面值相比,模板字面值有两个主要好处。
首先,它们支持字符串插值 :如果将表达式放在${
和}
中,则可以插入表达式:
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!'});
其次,模板字面值可以跨越多行:
const str = `this is
a text with
multiple lines`;
模板字面值总是产生字符串。
19.3 标签模板
行 A 中的表达式是标签模板 :
const first = 'Lisa';
const last = 'Simpson';
const result = tagFunction`Hello ${first} ${last}!`; // A
最后一行相当于:
const result = tagFunction(['Hello ', ' ', '!'], first, last);
tagFunction
的参数是:
- 模板字符串(第一个参数):一个包含插值(
${...}
)周围文本片段的数组。- 在示例中:
['Hello ', ' ', '!']
- 在示例中:
- 替换值(剩余参数):插值。
- 在示例中:
'Lisa'
和'Simpson'
- 在示例中:
字面值的静态(固定)部分(模板字符串)与动态部分(替换)分开。
tagFunction
可以返回任意值,并接受模板字符串的两个视图作为输入(只有熟视图显示在上一个示例中):
- 熟视图 ,其中:
\t
变为制表符\\
变为一个反斜杠
- 原始视图 ,其中:
\t
变为斜线后跟t
\\
变为两个反斜杠
原始视图通过String.raw
(稍后描述)和类似的应用提供原始字符串字面值。
标签模板非常适合支持小型嵌入式语言(所谓的领域特定语言 )。我们将继续举几个例子。
19.3.1 标签函数库:lit-html
lit-html 是一个基于标签模板的模板库,由前端框架 Polymer 使用:
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()
是用于循环的自定义函数。它的第二个参数为第 3 个参数返回的值生成唯一键。请注意该参数使用的嵌套标签模板。
19.3.2 标签函数库:re-template-tag
re-template-tag 是一个用于编写正则表达式的简单库。带有re
标签的模板会生成正则表达式。主要的好处是你可以通过${}
插入正则表达式和纯文本(参见RE_DATE
):
import {re} from 're-template-tag';
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`;
const match = RE_DATE.exec('2017-01-27');
assert.equal(match.groups.year, '2017');
19.3.3 标签函数库:graphql-tag
graphql-tag 库 允许您通过标签模板创建 GraphQL 查询:
import gql from 'graphql-tag';
const query = gql`
{
user(id: 5) {
firstName
lastName
}
}
`;
此外,还有用于在 Babel,TypeScript 等中预编译此类查询的插件。
19.4 原始字符串字面值
原始字符串字面值通过标签函数String.raw
实现。它们是一个字符串字面值,其中反斜杠不做任何特殊操作(例如转义字符等):
assert.equal(String.raw`\back`, '\\back');
一个有帮助的例子是带有正则表达式的字符串:
const regex1 = /^\./;
const regex2 = new RegExp('^\\.');
const regex3 = new RegExp(String.raw`^\.`);
所有三个正则表达式都是等价的,您可以看到使用字符串字面值,您必须编写两次反斜杠才能为该字面值转义它。使用原始字符串字面值,您不必这样做。
原始字符串字面值有用的另一个示例是 Windows 路径:
const WIN_PATH = String.raw`C:\foo\bar`;
assert.equal(WIN_PATH, 'C:\\foo\\bar');
19.5 高级
所有剩余部分都是高级的
19.6 多行模板字面值和缩进
如果将多行文本放在模板字面值中,则会出现两个目标冲突:一方面,文本应缩进来适合源代码。另一方面,它的行应该从最左边的列开始。例如:
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>#
··
有两种方法可以解决这个问题:通过标签模板或修剪模板字面值的结果。
19.6.1 修复:用于去缩进的模板标签
第一个修复是使用自定义模板标签来删除不需要的空格。它使用初始换行符后面的第一行来确定文本从哪一列开始,并去掉各处的缩进。它还删除了最开始的换行符和最后的缩进。这样的模板标签之一是 Desmond Brand 的dedent
:
import dedent from 'dedent';
function divDedented(text) {
return dedent`
<div>
${text}
</div>
`;
}
console.log('Output:');
console.log(divDedented('Hello!'));
这次,输出没有缩进:
Output:
<div>
Hello!
</div>
19.6.2 修复:.trim()
第二个修复更快,但也更脏:
function divDedented(text) {
return `
<div>
${text}
</div>
`.trim();
}
console.log('Output:');
console.log(divDedented('Hello!'));
字符串方法.trim()
在开头和结尾删除多余的空格,但内容本身必须从最左边的列开始。此解决方案的优点是不需要自定义标签函数。缺点是它看起来很难看。
输出看起来与dedent
一样(但是,最后没有换行符):
Output:
<div>
Hello!
</div>
19.7 通过模板字面值进行简单的模板化
虽然模板字面值看起来像 Web 模板,但是如何将它们用于(Web)模板并不是很明显:Web 模板从对象获取其数据,而模板字面值从变量获取其数据。解决方案是在函数体中使用模板字面值,其参数接收模板数据。例如:
const tmpl = (data) => `Hello ${data.name}!`;
assert.equal(tmpl({name: 'Jane'}), 'Hello Jane!');
19.7.1 一个更复杂的例子
作为一个更复杂的例子,我们想要一个地址数组并生成一个 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();
tmpl()
采取以下步骤:
<table>
内的文本是通过单个地址(第 4 行)的嵌套模板函数生成的。注意它最后如何使用字符串方法.trim()
来删除不必要的空格。- 嵌套模板函数通过数组方法
.map()
(第 3 行)应用于 Arrayaddrs
的每个元素。 - 生成的(字符串)数组通过数组方法
.join()
(第 10 行)转换为字符串。 - 辅助函数
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>
19.7.2 简单的 HTML 转义
function escapeHtml(str) {
return str
.replace(/&/g, '&') // first!
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/`/g, '`')
;
}
练习:HTML 模板
有奖练习挑战:exercises/template-literals/templating_test.js