JavaScript for impatient programmers (ES2022 edition)
この本のサポートをお願いします: 購入 または 寄付
(広告です。ブロックしないでください。)

21 テンプレートリテラルとタグ付きテンプレートの使用



テンプレートリテラルタグ付きテンプレートの2つの機能について詳しく説明する前に、まずテンプレートという用語の複数の意味について見ていきましょう。

21.1 用語の明確化: 「テンプレート」

以下の3つは、すべて名前にテンプレートが含まれていて、すべて似たように見えるにもかかわらず、大きく異なります。

21.2 テンプレートリテラル

テンプレートリテラルには、通常の文字列リテラルと比較して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`;

テンプレートリテラルは常に文字列を生成します。

21.3 タグ付きテンプレート

A行の式は、*タグ付きテンプレート*です。B行の配列にリストされている引数を使用して`tagFunc()`を呼び出すことと同じです。

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)
);

最初のバッククォートの前にある関数`tagFunc`は、*タグ関数*と呼ばれます。その引数は以下のとおりです。

リテラルの静的(固定)部分(テンプレート文字列)は、動的部分(置換)とは別に保持されます。

タグ関数は任意の値を返すことができます。

21.3.1 クック済みと生のテンプレート文字列 (上級)

これまでは、テンプレート文字列の*クック済み解釈*のみを見てきました。しかし、タグ関数は実際には2つの解釈を取得します。

生の解釈により、`String.raw` を介して生の文字列リテラルが可能になります(後述)および同様のアプリケーションが可能です。

次のタグ関数 `cookedRaw` は両方の解釈を使用します

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'],
  });

Unicodeコードポイントエスケープ(`\u{1F642}`)、Unicodeコードユニットエスケープ(`\u03A9}`)、およびASCIIエスケープ(`\x52}`)をタグ付きテンプレートで使用することもできます。

assert.deepEqual(
  cookedRaw`\u{54}\u0065\x78t`,
  {
    cooked: ['Text'],
    raw:    ['\\u{54}\\u0065\\x78t'],
    substitutions: [],
  });

これらのエスケープのいずれかの構文が正しくない場合、対応するクック済みテンプレート文字列は `undefined` になりますが、生のバージョンはそのままです。

assert.deepEqual(
  cookedRaw`\uu\xx ${1} after`,
  {
    cooked: [undefined, ' after'],
    raw:    ['\\uu\\xx ', ' after'],
    substitutions: [1],
  });

正しくないエスケープは、テンプレートリテラルと文字列リテラルで構文エラーを生成します。ES2018より前は、タグ付きテンプレートでもエラーが発生していました。なぜそれが変更されたのでしょうか?今では、以前は違法だったテキストにタグ付きテンプレートを使用できます。たとえば、

windowsPath`C:\uuu\xxx\111`
latex`\unicode`

21.4 タグ付きテンプレートの例 (ライブラリによる提供)

タグ付きテンプレートは、小さな埋め込み言語(いわゆる*ドメイン固有言語*)をサポートするのに最適です。いくつかの例を続けてみましょう。

21.4.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()` はループ用のカスタム関数です。2番目のパラメーターは、3番目のパラメーターによって返される値の一意のキーを生成します。そのパラメーターで使用されているネストされたタグ付きテンプレートに注意してください。

21.4.2 タグ関数ライブラリ: re-template-tag

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');
assert.equal(match.groups.year, '2017');

21.4.3 タグ関数ライブラリ: graphql-tag

ライブラリgraphql-tag を使用すると、タグ付きテンプレートを介してGraphQLクエリを作成できます。

import gql from 'graphql-tag';

const query = gql`
  {
    user(id: 5) {
      firstName
      lastName
    }
  }
  `;

さらに、Babel、TypeScriptなどでこのようなクエリをプリコンパイルするためのプラグインがあります。

21.5 生の文字列リテラル

生の文字列リテラルは、タグ関数 `String.raw` を介して実装されます。バックスラッシュが特別なことをしない(文字のエスケープなど)文字列リテラルです。

assert.equal(String.raw`\back`, '\\back');

これは、データにバックスラッシュが含まれている場合に役立ちます。たとえば、正規表現を含む文字列です。

const regex1 = /^\./;
const regex2 = new RegExp('^\\.');
const regex3 = new RegExp(String.raw`^\.`);

3つの正規表現はすべて同等です。通常の文字列リテラルでは、バックスラッシュを2回記述して、そのリテラルのためにエスケープする必要があります。生の文字列リテラルでは、それを行う必要はありません。

生の文字列リテラルは、Windowsファイル名パスを指定する場合にも役立ちます。

const WIN_PATH = String.raw`C:\foo\bar`;
assert.equal(WIN_PATH, 'C:\\foo\\bar');

21.6 (上級)

残りのセクションはすべて上級です。

21.7 複数行のテンプレートリテラルとインデント

テンプレートリテラルに複数行のテキストを配置する場合、2つの目標が競合します。一方では、テンプレートリテラルはソースコード内に収まるようにインデントする必要があります。一方、その内容の行は左端の列から始まる必要があります。

例えば

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')
);

インデントにより、テンプレートリテラルはソースコードによく適合します。残念ながら、出力もインデントされています。そして、先頭の改行と末尾の改行と2つのスペースは必要ありません。

Output:
#
····<div>#
······Hello!#
····</div>#
··

これを修正するには、タグ付きテンプレートを使用するか、テンプレートリテラルの結果をトリミングするかの2つの方法があります。

21.7.1 修正: デデントのためのテンプレートタグ

最初の修正は、不要な空白を削除するカスタムテンプレートタグを使用することです。最初の改行後の最初の行を使用して、テキストが開始される列を決定し、すべての場所のインデントを短縮します。また、先頭の改行と末尾のインデントを削除します。そのようなテンプレートタグの1つは、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>

21.7.2 修正: `.trim()`

2番目の修正はより高速ですが、よりダーティです。

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>

21.8 テンプレートリテラルによるシンプルなテンプレート

テンプレートリテラルはテキストテンプレートのように見えますが、(テキスト)テンプレートにどのように使用するかをすぐに理解することはできません。テキストテンプレートはオブジェクトからデータを取得しますが、テンプレートリテラルは変数からデータを取得します。解決策は、テンプレートデータを受け取る関数の本文でテンプレートリテラルを使用することです。たとえば、

const tmpl = (data) => `Hello ${data.name}!`;
assert.equal(tmpl({name: 'Jane'}), 'Hello Jane!');

21.8.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();

このコードには、2つのテンプレート関数があります。

最初のテンプレート関数は、テーブル要素を文字列に結合する配列で囲むことによって結果を生成します(10行目)。その配列は、2番目のテンプレート関数を `addrs` の各要素にマッピングすることによって生成されます(3行目)。したがって、テーブル行を含む文字列が含まれています。

ヘルパー関数 `escapeHtml()` は、特別なHTML文字をエスケープするために使用されます(6行目と7行目)。その実装は、次のサブセクションに示されています。

アドレスを使用して `tmpl()` を呼び出し、結果をログに記録してみましょう。

console.log(tmpl(addresses));

出力は次のとおりです。

<table>
  <tr>
        <td>&lt;Jane&gt;</td>
        <td>Bond</td>
      </tr><tr>
        <td>Lars</td>
        <td>&lt;Croft&gt;</td>
      </tr>
</table>

21.8.2 シンプルなHTMLエスケープ

次の関数は、プレーンテキストをエスケープして、HTMLでそのまま表示されるようにします。

function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;') // first!
    .replace(/>/g, '&gt;')
    .replace(/</g, '&lt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')
    .replace(/`/g, '&#96;')
    ;
}
assert.equal(
  escapeHtml('Rock & Roll'), 'Rock &amp; Roll');
assert.equal(
  escapeHtml('<blank>'), '&lt;blank&gt;');

  演習: HTMLテンプレート

ボーナスチャレンジ付きの演習: `exercises/template-literals/templating_test.mjs`

  クイズ

クイズアプリをご覧ください。