HTML5Experts.jp

テンプレートエンジン不要?JavaScriptで文字列処理を簡潔にするTemplate literal

これまでのJavaScriptでは、複雑な文字列処理はテンプレートエンジンを使うことが一般的でした。しかしECMAScript 2015(ECMAScript 6)では、パワフルなTemplate literalが標準で利用できるようになりました。この新たに追加されたTemplate literalについて、概要とサンプルコードを紹介します。

これまでの文字列組み立て

ES6で追加されたTemplate literalを使うと、文字列をより柔軟に、シンプルに組み立てることができます。例えば、これまでのJavaScriptでは、文字列を組み立てるために、例えば以下のような方法を取る必要がありました。

var name = 'john';
var country = 'japan';

var str1 = 'Hello! My name is ' + name + '. I live in ' + country + '.';

var str2 = [ 'Hello! My name is ', name, '. I live in ', country, '.' ].join('');

console.log(str1); // "Hello! My name is john. I live in japan." console.log(str2); // "Hello! My name is john. I live in japan."

文字列の断片を複数作り、+concatでつなぎ合わせたり、配列に突っ込んでjoinしたりなどといった方法です。単純なテキスト結合であればことたりますが、ある程度複雑な処理を行う場合、可読性の欠如や、複数行に渡る文字列を簡潔に表現できないというような点が問題になってきます。

Template literalを使った文字組み立て

Template literalを使えば、上記の文字組み立てを、以下のように書くことができます。

var name = 'john';
var country = 'japan';

var str1 = Hello! My name is ${name}. I live in ${country}.;

console.log(str1); // "Hello! My name is john. I live in japan."

``で囲まれた部分がTemplate literalです。この中で${name}${country}という部分がありますが、ここはそれぞれ、"john""japan"と、変数の内容に置き換わります。このように、Template literal内の${}部分は、その内容が評価された結果に置き換えられ、最終的な処理結果が文字列として変数str1に格納されます。

式の展開

${}の中には、変数だけでなく、式を入れることができます。例えば、以下の例を見て下さい。

var str1 = 今月: ${(new Date).getMonth()+1}月;

console.log(str1); // "今月: 10月"

上記例では、Dateを利用し、現在の月を表示させています。(new Date).getMonth()+1の結果は現在の月で、例えばこの原稿執筆時では10となりますが、このような計算結果を、あらかじめ変数に入れておかずとも、${}内で展開させることも可能です。

JavaScriptを用いて複雑なテキストの組み立て処理を行いたい場合、Handlebarsやmustacheといった、文字列処理を抽象化したテンプレートエンジンが利用されてきました。

このようなライブラリを利用すれば、今解説したような、テンプレート的な文字列処理を行えるのですが、そういった処理がある程度、素のJavaScriptでできるようになります。

タグ付テンプレート

Template literalにはタグという機能(Tagged template strings)があります。タグを利用すると、テンプレートの処理内容を、functionで具体的に指定することができます。その仕組みを理解するため、まずは単純な例を見てみます。

var marginVal = '30px';
var paddingVal = '40px';

function tag(strings, ...values) { console.log(strings); // ["marginは", "で、paddingは", "です"] console.log(values); // ["30px", "40px"] return 'returned strings!'; };

var str1 = tag marginは${marginVal}で、paddingは${paddingVal}です;

console.log(str1); // "returned strings!"

ここで使われているTemplate literalの頭には、tagという文字があります。これがタグの指定で、tagというfunctionで、テンプレートの処理内容を定義していることを示します。タグとして指定されたfunctionは、テンプレート内の文字列、変数を受け取ることができます。そして、このfunctionの返す値が、Template literalの処理結果となります。

もし、このTemplate literalにタグが指定されていない場合、コンソールに表示される結果は「marginは30pxで、paddingは40pxです」となりますが、タグが指定されているため、そのようにはなりません。

以降は、この例で登場しているfunction、tagを眺めながらご確認下さい。

タグとして指定されたfunctionは、第一引数に、テンプレート内で使われている文字列が、登場する順に格納された配列を受け取ります。この場合ですと、処理する対象は「marginは${marginVal}で、paddingは${paddingVal}です」で、この中で使われている文字列は、以下の3つです。

これらが配列になって渡ってくるのが第一引数。第二引数以降で、これら文字列の間に挟まっている${}で展開される式の結果が渡されます。ここでは、以下の値をvaluesとして受け取っています。(※1)

そして最終的にfunctionが返した値が、テンプレートの処理結果となります。この場合ですと、"returned strings!"という文字列を返しているので、str1に入るのは"returned strings!"という文字列となり、これがコンソールに表示される結果となります。

こんなふうにして、受け取った値を好きに加工してくださいというのがタグ付きテンプレートです。この例では、最終的に返した値が、受け取った値を一切使っていませんが、ひとまずのタグの仕組みの理解のためそうしています。

ちなみに、以下例のように、${}${}の間に何も文字がない場合、空文字が渡されます。いきなり${}から始まる場合はその前に空文字が、${}で終わる場合はその後に空文字があるものとして扱われます。

var val1 = 'value1';
var val2 = 'value2';

function tag(strings, ...values) { console.log(strings, values); };

var str1 = tag ${val1}; // ["", ""], ["value1"] var str1 = tag ${val1}${val2}; // ["", "", ""], ["value1", "value2"]

※1: ...valuesと引数に書かれていますが、これは、残りの引数を配列にまとめて受け取ることのできる、ES6のRest parametersという機能を利用しています。

タグを利用して大文字に変換する例

このタグを利用し、テンプレート内に埋め込まれた文字列を大文字にしてみたのが、以下の例です。

var charsToUpper = function(strings, ...values) {
  var res = '';
  for(var i=0, l=strings.length; i<l; i+=1) {
    res += strings[i];
    if(i < values.length) {
      res += values[i].toUpperCase();
    }
  }
  return res;
};

var name = 'john'; var country = 'japan';

var str1 = charsToUpper Hello! My name is ${name}. I live in ${country}.; var str2 = charsToUpper ${name} ${country};

console.log(str1); // "Hello! My name is JOHN. I live in JAPAN." console.log(str2); // "JOHN JAPAN"

テンプレート内で利用されている変数namecountryにはそれぞれ、"john""japan"と、小文字の文字列が入っていますが、テンプレートの処理結果は、"Hello! My name is JOHN. I live in JAPAN."と、大文字に置き換わっています。タグとして指定されたcharsToUpperの内容を見ると、登場する順に文字列と変数の内容を結合し、その中で、${}で埋め込まれた文字列に関しては、.toUpperCase()で大文字にしているのが確認できます。

ブログ記事用のHTMLを展開する例

最後に、ちょっと長いですが、ブログ記事のようなものをHTMLとして出力するようなケースを想定した例を紹介します。

var data = {
  title: '<探検>!ECMAScript',
  author: '佐藤鈴木&田中太郎',
  tags: [ 'HTML', 'JavaScript', 'CSS' ],
  links: [
    { 
      title: 'Mozilla Developer Network',
      href: 'https://developer.mozilla.org/ja/'
    },
    {
      title: 'Web Hypertext Application Technology Working Group',
      href: 'https://whatwg.org/'
    }
  ],
  entryBody: '<p>This is a blog post!</p>'
};

// HTMLの特殊文字を実体参照にするタグ function htmlEscape(strings, ...values) { var handleString = function(str) { return str.replace(/&/g, '&amp;') .replace(/>/g, '&gt;') .replace(/</g, '&lt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#039;') .replace(/`/g, '&#096;'); }; var res = ''; for(var i=0, l=strings.length; i<l; i+=1) { res += handleString(strings[i]); if(i < values.length) { res += handleString(values[i]); } } return res; };

var html = &lt;section class=&quot;entry&quot;&gt; &lt;header&gt; &lt;h1&gt;${htmlEscape${data.title}}&lt;/h1&gt; &lt;div class=&quot;author&quot;&gt;Author: ${htmlEscape${data.author}}&lt;/div&gt; &lt;ul class=&quot;tags&quot;&gt; ${data.tags.map(function(tag) { return<li>${tag}</li>}).join('\n')} &lt;/ul&gt; &lt;/header&gt; ${data.entryBody} &lt;hr&gt; &lt;dl&gt; &lt;dt&gt;関連リンク&lt;/dt&gt; ${data.links.map(function(link) { return<dd><a href="${link.href}">${link.title}</dd>}).join('\n')} &lt;/dl&gt; &lt;/section&gt;;

console.log(html);

このコードで出力されるのは、以下の文字列です。(見やすさのためにインデントを調節しています)

<section class="entry">
  <header>
    <h1>&lt;探検&gt;!ECMAScript</h1>
    <div class="author">Author: 佐藤鈴木&amp;田中太郎</div>
    <ul class="tags">
      <li>HTML</li>
      <li>JavaScript</li>
      <li>CSS</li>
    </ul>
  </header>
  <p>This is a blog post!</p>
  <hr>
  <dl>
    <dt>関連リンク</dt>
    <dd><a href="https://developer.mozilla.org/ja/">Mozilla Developer Network</dd>
    <dd><a href="https://whatwg.org/">Web Hypertext Application Technology Working Group</dd>
  </dl>
</section>

このサンプルでは、コードの頭で用意しているオブジェクトdataの内容を、テンプレートに展開しています。

その処理内容の中で、タグhtmlEscapeを使っています。このfunctionは、受け取った値の中に含まれるHTMLの特殊文字を実体参照に変換し、ひとつに結合して返します。ここでは、タイトル部分と著者名部分に使われており、出力結果には“、&amp;がそれぞれ実体参照に置き換わっているのが確認できます。

このほか、タグやリンクからリストを作っている箇所では、${}の中でmap(※2)を使い、配列をli要素の繰り返しに変換しています。単純な処理であれば、このようにテンプレート内で何らかの処理を行わせてしまってもよいですし、ある程度複雑な処理であれば、別途functionとして分け、${}内でそのfunctionを呼び出すというような方法を取るのもよさそうです(※3)。

※2: Array.prototype.mapは、配列の各要素それぞれに対して、渡されたfunctionを実行し、その結果から新しい配列を作る機能です。

※3: ES6で追加されたArrow functionを使えば、このテンプレート内でmapが使用されている箇所を、以下のように簡潔に書くことができます。

${data.tags.map(tag => <li>${tag}</li>).join('\n')}

${data.links.map(link => <dd><a href="${link.href}">${link.title}</dd>).join('\n')}

ブラウザ対応状況

以下のサイトのES6対応テーブルを見ると、Template literal (Template strings)は、IEを除く最新版のモダンブラウザ(Microsoft Edgeは可)で利用可能なようです。各ブラウザの対応はかなり進んでいると言えそうですが、bable等のトランスパイラを利用しない場合、IE11、iOS8のSafariではまだ対応していないというあたりが、現実的に利用可能かを判断するボーダーラインとなりそうです。

まとめ

以上、Template literalについて、その概要を解説しました。Template literalを用いれば、文字列の結合処理をとても簡潔に書くことができるようになります。複雑なテンプレート処理の場合ははやり、テンプレートエンジンのような仕組みを利用したほうが良いケースが多いように筆者は感じますが、ちょっとした文字列処理の際には、基本、Template literalを使っていくようになるのではないでしょうか。

Template literalについてより詳しく知りたい場合、以下が参考になります。