HTML
JavaScript
es6
WebComponents
es2015

Web Components の最近の仕様と開発手法


はじめに

Web Components だけで開発を行う場合、どのような技術を使えばいいのかを調べてみました。結論は最後に書いています。


Web Components


Specifications

Web Components specifications: https://github.com/w3c/webcomponents


Web Components are a new browser feature that provides a standard component model for the Web, consisting of several pieces:


  • Shadow DOM

  • Custom Elements

  • HTML Imports

  • HTML Templates



Can I use Web Components?

Can I use... Support tables for HTML5, CSS3, etc: https://caniuse.com/

(Edge は Chromium ベースになることで、Shadow DOM (v1)、Custom Elements (v1) も :white_check_mark: になるかもしれません。)


開発環境

今回は macOS 上で試してみましたが、特に OS 依存な内容はないと思います。


開発に利用するツール


環境構築方法


開発用サーバインストール

$ npm install -g http-server


ディレクトリ作成

$ cd /path/to/development

$ mkdir my-app
$ cd my-app
$ touch index.html


開発用サーバ起動

$ http-server


Hello world


HTML Imports を使った実装(非推奨)

Web Components のすべての要素 (Shadow DOM, Custom Elements, HTML Imports, HTML Templates) を使った実装です。

ただし、HTML Imports は将来的に削除される可能性が高いため、この方法は推奨しません。

スクリーンショット 2018-03-02 15.01.28.png


Structure

/ (root)

+-- elements/
| +-- my-app.html
+-- index.html


index.html

<!DOCTYPE html>

<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>My App</title>
<link rel="import" id="my-app" href="/elements/my-app.html">
</head>
<body>
<p>This is not a custom element.</p>
<my-app></my-app>
</body>
</html>


my-app.html

<template>

<style>
p {
color: #f00;
}
</style>

<p>This is a custom element!</p>
</template>

<script>
class MyAppElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
const myApp = document.getElementById('my-app').import;
const templateNode = myApp.getElementsByTagName('template')[0].content.cloneNode(true);
shadowRoot.appendChild(templateNode);
}

connectedCallback() {
console.log('my-app element is connected');
}
}

customElements.define('my-app', MyAppElement);
</script>



HTML Imports から ES6 Modules へ

「ES6 の Modules で実現できるよね」ということでお蔵入りになった HTML Imports。現状で Chrome でしか実装されておらず、Mozilla が HTML Imports を実装する予定がないと表明しているので、HTML Imports は今後削除される可能性が高いです。

また、Web Components をベースにした Polymer も、バージョン 3.0 では ES Modules を使った実装になるよう開発を進めているようです。

詳細は以下の記事を参照。


ES6 Modules を使った実装

HTML Imports の代わりに ES6 の Modules を使って実装した場合は次のような感じになります。Custom Elements がひとつだけだと Modules を使う必要がないので、ひとつ増やしました。

スクリーンショット 2018-03-02 15.07.24.png


Structure

/ (root)

+-- elements/
| +-- my-app.js
| +-- my-date.js
+-- index.html


index.html

<!DOCTYPE html>

<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>My App</title>
<script type="module" src="/elements/my-app.js"></script>
</head>
<body>
<p>This is not a custom element.</p>
<my-app></my-app>
</body>
</html>


my-app.js

import '/elements/my-date.js';

class MyAppElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = this.template();
}

connectedCallback() {
console.log('my-app element is connected');
}

template() {
return `
<style>
p {
color: #f00;
}
</style>

<p>This is a custom element!</p>
<my-date></my-date>
`;
}
}

customElements.define('my-app', MyAppElement);



my-date.js

class MyDateElement extends HTMLElement {

constructor() {
super();
this.now = new Date();

const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = this.template();
}

connectedCallback() {
console.log('my-date element is connected');
}

template() {
return `
<p>現在日時は <time datetime="
${this.now.toISOString()}">${this.now.toLocaleString()}</time> です。</p>
`
;
}
}

customElements.define('my-date', MyDateElement);



ES6 Modules を使う場合の欠点

上記のように ES Modules を使えば HTML Imports を使わない実装が可能になります。ただし、以下のような欠点があると考えます。


キモい

JavaScript の中で HTML を書くのがキモい。上記の例では ES6 の Template Literals を使っているので、従来の方法に比べれば、JavaScript 内で HTML を書きやすくなっています。でもやっぱりマークアップとスクリプトはきっちり分けたいというのが個人的な感想です。

ちなみに、この場合は HTML Templates も使わないことになります。


遅延読み込みができない

ES Modules は最初のページ読み込み時に import される JS ファイルをすべて読み込むため、特定のファイルだけ別のタイミングで読み込ませるといったことができません。

コレを解決するには、以下の方法をとる必要があります。


任意のタイミングで script 要素を追加する

const scriptElement = document.createElement('script');

scriptElement.src = '/elements/my-date.js';
document.head.appendChild(scriptElement);


import() を使う

関数ライクな import() を使って動的インポートを行うということが TC39 で提案されています。返り値は Promise です。

現在では Chrome 63+ および Safari 11+ で利用可能です。


結論

ここまでの調査をまとめると、フレームワーク等を使わずに Web Components だけを使った実装をするには、次のような方法をとることが考えられます。


  • Shadow DOM (v1) + Custom Elements (v1) + ES Modules を使う。


    • HTML Imports は非推奨。



  • 部品化された要素のマークアップは ES6 の Template Literals を使う。


    • ただし、キモい。

    • マークアップとスクリプトは分けたい(せっかく HTML Templates もあるのに)。



  • 遅延読み込みさせるには、任意のタイミングで script 要素を追加する処理を書くか、import() を使う。


    • ただし、import() は時期尚早か。



本番のプロダクトでは素直に React や Vue.js などのフレームワークを使ったほうがいいかもしれないですが、ベータ版として興味本位で作ってみるのは全然アリだろうと思います。