.trim()
テンプレートリテラルとタグ付きテンプレートの2つの機能について詳しく説明する前に、まずテンプレートという用語の複数の意味について見ていきましょう。
以下の3つは、すべて名前にテンプレートが含まれていて、すべて似たように見えるにもかかわらず、大きく異なります。
テキストテンプレートは、データからテキストへの関数です。Web開発でよく使用され、多くの場合、テキストファイルで定義されます。たとえば、次のテキストは、ライブラリHandlebarsのテンプレートを定義しています。
<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{body}}</div>
</div>
このテンプレートには、2つの空白(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()
は、リテラルのテキストと${}
を介して補間されたデータの両方を受け取ることに注意してください。
テンプレートリテラルには、通常の文字列リテラルと比較して2つの新しい機能があります。
まず、文字列補間をサポートしています: ${}
内に動的に計算された値を配置すると、文字列に変換され、リテラルによって返される文字列に挿入されます。
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`は、*タグ関数*と呼ばれます。その引数は以下のとおりです。
リテラルの静的(固定)部分(テンプレート文字列)は、動的部分(置換)とは別に保持されます。
タグ関数は任意の値を返すことができます。
これまでは、テンプレート文字列の*クック済み解釈*のみを見てきました。しかし、タグ関数は実際には2つの解釈を取得します。
*クック済み解釈*では、バックスラッシュは特別な意味を持ちます。たとえば、`\t` はタブ文字を生成します。テンプレート文字列のこの解釈は、最初の引数に配列として格納されます。
*生の解釈*では、バックスラッシュは特別な意味を持ちません。たとえば、`\t` は2つの文字(バックスラッシュと `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()` はループ用のカスタム関数です。2番目のパラメーターは、3番目のパラメーターによって返される値の一意のキーを生成します。そのパラメーターで使用されているネストされたタグ付きテンプレートに注意してください。
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`^\.`);
3つの正規表現はすべて同等です。通常の文字列リテラルでは、バックスラッシュを2回記述して、そのリテラルのためにエスケープする必要があります。生の文字列リテラルでは、それを行う必要はありません。
生の文字列リテラルは、Windowsファイル名パスを指定する場合にも役立ちます。
const WIN_PATH = String.raw`C:\foo\bar`;
.equal(WIN_PATH, 'C:\\foo\\bar'); assert
残りのセクションはすべて上級です。
テンプレートリテラルに複数行のテキストを配置する場合、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つの方法があります。
最初の修正は、不要な空白を削除するカスタムテンプレートタグを使用することです。最初の改行後の最初の行を使用して、テキストが開始される列を決定し、すべての場所のインデントを短縮します。また、先頭の改行と末尾のインデントを削除します。そのようなテンプレートタグの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>
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>
テンプレートリテラルはテキストテンプレートのように見えますが、(テキスト)テンプレートにどのように使用するかをすぐに理解することはできません。テキストテンプレートはオブジェクトからデータを取得しますが、テンプレートリテラルは変数からデータを取得します。解決策は、テンプレートデータを受け取る関数の本文でテンプレートリテラルを使用することです。たとえば、
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();
このコードには、2つのテンプレート関数があります。
最初のテンプレート関数は、テーブル要素を文字列に結合する配列で囲むことによって結果を生成します(10行目)。その配列は、2番目のテンプレート関数を `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`
クイズ
クイズアプリをご覧ください。