17.使用模板字面值和标签模板

在我们深入研究两个特性*模板字面值* 和标签模板 之前,让我们首先检查术语模板 的多重含义。

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 行)应用于 Array addrs的每个元素。
  • 生成的(字符串)数组通过数组方法.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