Webコンポーネント(4):テンプレート要素&HTMLインポート

今回はWebコンポーネント連載の4回目、テンプレート要素HTMLインポートについて紹介します。冒頭で明らかにすると、この2つの標準フローは、Webコンポーネント開発から除外されます。したがって、両方の基準を調べるよりは、これらがWebコンポーネントとして使用されるようになった背景と限界点、そしてPolymer Summit 2017で述べられた現在の動向について紹介したいと思います。

Old School Template

JavaScriptでHTML要素を作るのは一般的です。エレメントを1つ作ってbodyに貼り付けてみよう。document.createElementappnedChildの2つの関数実行で簡単にできます。

const wrapper = document.createElement('div');
const content = document.createElement('div');
...
anotherElement.innerText = someText;
...
document.body.appendChild(wrapper);
wrapper.appendChild(content);
...

しかし、複雑なDOMを構成するときは、この方法を適用するとコードが長くなってしまい、DOM構造を把握しにくいですね。メンテナンスも困難になるので、できればこのように書きたくはありません。複雑なテンプレートを作成するときは、innerHTMLを使った方が便利です。以下のコードはより直感的で保守可能な形態になっています。

document.querySelect('#target').innerHTML = [
    '<div class="wrapper">',
    '<div class="content">',
    ...
    '<div>' + someText + '</div>',
    ...
    '</div>',
    '</div>'
].join('');

しかし、この方法もリストのように反復的な要素の生成や、条件に応じてテンプレートを変更する場合など、さまざまな要求事項に対応するには不十分です。また内部要素にアクセスするのにquerySelectorを使う点も不便です。

Template Engines

文字列の簡単な組み合わせでは解決できないLoopIfなどの要件を解決するために、テンプレートが使用され始めました。Mustache、Handlebarsなどのテンプレートは、似たような目的で使用されながら、それぞれ個性を持っています。有名なVue.js、Angular、Polymer(過去)もこのカテゴリーに属します。このテンプレートエンジン方式は、依然として多くの状況で有効活用されています。

<!-- angular: loop -->
<header ng-repeat-start="item in items">
  Header {{ item }}
</header>

<!-- handlebars: conditional -->
<div class="entry">
  {{#if author}}
    <h1>{{firstName}} {{lastName}}</h1>
  {{/if}}
</div>

JSX + Virtual DOM

フロントエンドの開発がコンポーネント化され、コンポーネントが描く複数のテンプレートを効率的に処理できるReact(JSX + Virtual DOM)が人気を集めるようになりました。JSXはESCAScriptの拡張スクリプトの内部に直接テンプレートを作成できるようにして、Virtual DOMは特定の状態に応じてDOMの一部のみを更新する役割をします。テンプレート観点では、Reactは状態に応じてテンプレートをコンポーネント単位で更新する作業を行います。JSX+Virtual DOMは、現在最も人気のあるテンプレートの処理方法と言えます。

// Virtual DOM example
var tree = render(count);               // We need an initial tree
var rootNode = createElement(tree);     // Create an initial root DOM node ...
document.body.appendChild(rootNode);    // ... and it should be in the document

setInterval(function () {
      count++;

      var newTree = render(count);
      var patches = diff(tree, newTree);
      rootNode = patch(rootNode, patches);
      tree = newTree;
}, 1000);
// jsx example
const element = (
  <div>
    <h1>Hello!</h1>
    <h2>Good to see you here.</h2>
  </div>
);
render(element);

Element

テンプレートエンジンは便利ですが、文字列として処理されるという問題点も内包します。XSS攻撃にさらされる危険性があり、innerHTMLの使用が強制されます。DOM APIはテンプレートに使用できません。これらの問題を解決するために、テンプレートの文字列処理を止揚し、エレメントで処理する方法としてテンプレート要素が作成されました。

<template id="productrow">
  <tr>
    <td class="record"></td>
    <td></td>
  </tr>
</template>
const template = document.querySelector('#productrow');
const clone = document.importNode(template.content, true);
clone.querySelector('.record').innerText = '####';
tableElement.appendChild(clone);

テンプレート要素は、JavaScriptコードで大量のコードを、条件に応じてDOMの変更も可能です。これらの変更は、DOM APIをそのまま使用できるため便利です。テンプレート要素はDOMに一度定義されると、必要に応じてコピー&ペーストもでき、パフォーマンスも素晴らしいです。

Element + Web Components

このような利点からテンプレート要素は、Webコンポーネントを構成する標準的なものになりました。テンプレート要素は、スクリプトとスタイルも含むことができます。スクリプトとスタイルは、テンプレートにあるときは適用されませんが、コピーしてドキュメントに付随すると適用されます。Shadow DOMと相乗効果を生み、Webコンポーネントのテンプレート機能を実行するのに十分なメリットがあります。

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<style>
    .outer {
      border: 2px solid brown;
      background: red;
    }
    .name {
      color: black;
    }
</style>
<div class="outer">
  <div class="name">
    Bob
  </div>
</div>
</template>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

しかしテンプレート要素は、テンプレートをHTMLで作成する必要があり、これが欠点にもなっています。コンポーネントのコントローラに対応するJavaScriptと、テンプレートビューに対応するHTMLを分離する必要があるという点です。コンポーネントとモジュールが正確に同義語とは言えませんが、モジュールとして分離され、再利用されてこそ意味があります。ところがテンプレート要素とカスタム要素をコンポーネントとして構成するには、HTML、JSの2つのファイルが必要です。この方式は、Webコンポーネントをモジュール化するとき大きな障害になります。

HTMLインポート

HTMLインポートは、スクリプトベースのCommonJS、RequireJSとは異なり、HTMLベースの依存性ソルバーとしての役割をします。HTMLからHTMLを読み込んで貼り付けるとき、以下のように整然と処理できます。HTMLインポートの構文はスタイルの連結と似ていますが、スクリプト、スタイル、HTML要素の依存性を一度に解決する強力な方法です。

<!-- main page -->
<head>
    <link rel="import" href="bootstrap.html">
</head>
<!-- bootstrap.html -->
<link rel="stylesheet" href="bootstrap.css">
<link rel="stylesheet" href="fonts.css">
<script src="jquery.js"></script>
<script src="bootstrap.js"></script>
<script src="bootstrap-tooltip.js"></script>
<script src="bootstrap-dropdown.js"></script>

HTMLインポートはHTMLとJavaScriptの2つの依存性を解決するWebコンポーネントのソルバーです。これを適用して、Webコンポーネントを構成すると以下のようになります。Webコンポーネント規格(カスタム要素、Shadow DOM、テンプレート要素、HTMLインポート)をすべて組み合わせてみよう。下記のように、HTMLでコンポーネントを構成するとき、形状と処理方法が異なりますが、PolymerやVue.jsとも類似しています。

<!-- add dependencies -->
<link rel="import" href="./dependent-element.html">

<!-- web component template -->
<template id="nameTagTemplate">
<style>
    .outer {
      border: 2px solid brown;
      background: red;
    }
    .name {
      color: black;
    }
</style>
<div class="outer">
  <div class="name">
    Bob
  </div>
  <dependent-element></dependent-element>
</div>
</template>

<!-- web component class -->
<script>
class NameTag extends HTMLElement {
...
    const shadowRoot = this.attachShadow({mode: 'open'});
    const template = document.querySelector('#nameTagTemplate');
    const clone = document.importNode(template.content, true);
    shadowRoot.appendChild(clone);
...
}
customElements.define('name-tag', NameTag);
</script>

FirefoxはHTMLインポートが嫌い

Googleを筆頭にWebコンポーネントグループは、カスタム要素、Shadow DOM、テンプレート要素、HTMLインポートの4つの標準を、Webコンポーネント標準と定めました。その後、テンプレート要素は最初に安定化され、IEを除くすべてのブラウザでネイティブ対応しています。カスタム要素とShadow DOMの実装は、いくつかの意見を取り入れながらv1スペックで定義され、IE以外のブラウザでは対応済、または開発中となっています。
ところがHTMLインポートで問題が生じました。FirefoxがHTMLインポートに対応しないと宣言したのを皮切りに、他のブラウザでも実装に乗り出していません。

Mozilla will not ship an implementation of HTML Imports. We expect that once JavaScript modules — a feature derived from JavaScript libraries written by the developer community — is shipped, the way we look at this problem will have changed. We have also learned from Gaia and others, that lack of HTML Imports is not a problem as the functionality can easily be provided for with a polyfill if desired.
Mozilla and Web Components

Webコンポーネント支持者らは、引き続きHTMLインポートの実装を要求しましたが、実現は厳しそうです。HTMLインポートにさまざまな問題が提起されたが、そのうちのいくつかを下記に紹介します。ブラウザ制作会社はこれらの理由が妥当であると判断したようです。- The Problem With Using HTML Imports For Dependency Management

  • すでにES Module標準が作られている。(現在すべてのブラウザで対応済、または開発中である)
  • ブラウザだけのために標準化作業をしたくない。
  • 標準でなくてもPolyfillで簡単に実装できる。(現在、Polyfillも1000ラインを超えており、機能実装だけを考慮すると500ライン程度で十分である)
  • HTTP/2を使うと複数ファイルを早く受信できるが、依然としてバンドルされた単一ファイルを受け取る方が早い。(正確には標準に関する内容ではないが、構造上のように言及される場合が多い)
  • De-Dupingできない。(CDNパスの使用が予想されるが、同じファイルを把握して処理するのが難しい。ex: code.jquery.com/jquery-2.1.1.min.js vs maxcdn.bootstrapcdn.com/bootstrap/2.3.0/js/bootstrap.min.js)

上記の他にも、実際のプロジェクトを構成するには、大きな障壁があります。それはWebpackが使用できないということです。すでに標準レベルにあるWebpackをロールアップのように使用できないのは致命的です。現在、polymer-webpack-loaderがリリースされて、わずか2ヶ月ですが使用してみた結果、基本的な問題の解決が必要に思われます。結局、Webコンポーネントを作成するには、Polymerなどのサポートフレームワークと独立したツールを使用する必要があるという結論になります。

HTML Imports NO!Template Literals YES?

HTMLインポートはブラウザが対応しないこと、Webpackの生態系とは別に浮遊している点などを認識して、結果として方向が変わっています。個人的な意見だがPolymerがWebpackローダーさえ高速対応していたら、より良い結果になったと思います。先週開催されたPolymer Summit 2017では、Polymer 3.0 previewを発表し、新しい方向性を示した。まさにHTMLインポートを諦めるという内容です。HTMLインポートはES Modulesに置き換えされ、HTML TemplateはES Template literalsに変わった。これでWebpack生態系の”市民”となりました。
依存性問題は解決しましたが、テンプレートはどうなるのでしょうか。最終的に文字列ベースのテンプレートに戻ると、テンプレート要素の利点を失ってしまうことになるのではないでしょうか。幸いなことに、ES6 Template literalsは単純な文字列ではなく、テンプレートの役割を実行する素晴らしい可能性を持っています。

el.innerHTML = `
    <div>
        <h1>${title}</h1>
        <body>${content}</body>
    </div>
`

しかしこれでは不十分です。幸運なことに最近、hyper(HTML)lit-htmlhyperxなどのTemplate Literalsを使ったプロジェクトが人気を博しています。lit-htmlの説明によると、これはTemplate Literalsを受けてテンプレート要素を生成します。もし同じテンプレートにエレメントを再生成すると、既に作成したテンプレート要素を使って効率的に作られる。文字列を使用しますが、テンプレート要素の利点を吸収しようとするものです。サイズもminified基準で2KBに満たず、Vanilla JavaScriptを志向する場合にも、テンプレートではTemplate Literalsを利用したこれらのプロジェクトを使用する方がよさそうです。

総合すると、カスタム要素とShadow DOMが維持され、HTMLインポートは対象外となり、テンプレート要素まで使わなくなりました。そして、HTMLインポートの空席を標準的なES ModulesとTemplate Literalsが占めるものとみられます。またTemplate Literalsを活用したプロジェクトを注視する必要があるでしょう。Polymer Summitが示した方向性が、Webコンポーネントの方向を決定するとまではいきませんが、多くの開発者が悩んでいた問題が解決される模様です。何よりWebpackも利用できるようになり、今後のWebコンポーネントの歩みに弾みがつくものと期待します。

おわりに

すでに力を失ったこの2つの技術、テンプレート要素とHTMLインポートをどのように記事にするか悩む所が多く、糸口をつかむのに時間がかかりました。両方の技術を詳細に説明しても意味がないと考え、フロントエンドでのテンプレートの流れ、依存性との関連性、最近の流れについて、1つの話にまとめました。最近の流れがウェブコンポーネントに興味がある者の一人として、Webコンポーネントが良い方向に進んでくれれば嬉しいです。

また、この記事に興味を持たれた方は、Polymer Summit 2017のlit-htmlセッション映像も推薦します。この記事も、セッションの流れに沿って作成しました。Template Literalsに対する理解とReactに関する話、これらを利用してlit-htmlがどのようにスマートに問題を解決するかが盛り込まれています。Template LiteralsライブラリがWebコンポーネントをReactよりも素敵な姿にしてくれるという期待もあります。

References

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop