ライブラリを使わずここまでできる!Web Componentsで近未来のフロントエンド開発

Cacooチームエンジニアの川端です。普段はCacooのエディター(編集画面)のフロントエンドの開発をしております。最近パパになったので娘の写真を親に共有するアプリを作ってみました。その際に、Web Componentsなる機能を使ってみました。JSフレームワーク群雄割拠の昨今、ライブラリを使わずWeb Componentsだけでどこまでできるのかご紹介したいと思います。

Web Componentsとは?

Web Componentsとは、HTMLの要素をカプセル化して再利用可能なパーツにするためのブラウザのAPI群です。ReactやVueやRiotでいうところのコンポーネントをライブラリを使うことなく素のJSだけで作ることができる技術になります。

Web Components | MDNによると次のように記されています。

Web Components は、オープンなウェブテクノロジーを使用して作成された再利用可能なユーザーインターフェイスウィジェットと考えることができます。これらはブラウザーの一部であるため、jQuery や Dojo などの外部ライブラリーは必要ありません。

まだ未サポートなブラウザもある

2018年3月の時点でまだ未サポートなブラウザもあるので本番投入するにはちょっと早い技術にはなりますが、近未来のフロントエンドの現場で使われるであろう概念なので知っておいて損はないと思います。公式サイト(webcomponents.org)上で各APIのブラウザごとのサポート状況を確認できます。サポートの差異を吸収するpolyfillもあります。

※この記事のサンプルはWeb ComponentsのAPIをサポートするブラウザでのみ動作します。(Chrome推奨)

Web Componentsを構成するAPI群

Web Componentsを構成するAPI群は以下の4つになります。本記事ではこれらのAPIの特徴と使い方についてご紹介します。

Custom Elements, Shadow DOM, ES Modules, HTML TemplatesWeb Componentsを構成するAPI群

 

1. Custom Elements – 好きなタグ名の要素を作成する機能

HTML要素のタグ名を自分で定義できる機能です。

Custom Elementsのサポート状況

今までは同じデザインの要素を使いまわすためにdiv要素などにクラス名をつけることで再利用を実現していましたが、独自のタグ名だけで使いまわせるパーツを作ることができます。

例えば、1つのボタンのデザインを色違いで使いまわしたい場合こんな風にそれぞれの色のクラスを定義するかと思います。(命名が雑ですが説明のためなのでご容赦ください)

色違いのボタン色違いのボタンデザイン

 

<button class="red">my button</button>
<button class="yellow">my button</button>
<button class="green">my button</button>
button {
  border: none;
  border-radius: 3px;
  padding: 10px 20px;
}

button.red {
  background-color: red;
}

button.yellow {
  background-color: yellow;
}

button.green {
  background-color: green;
}

Custom Elementsを使えばベースとなるボタンを<my-button>要素にラップできます。このサンプルでは<button>要素しかラップしていませんが、実際にはもっと大きな要素単位にラップするのでその場合にコードの見通しもスッキリします。

<my-button class="red">my button</my-button>
<my-button class="yellow">my button</my-button>
<my-button class="green">my button</my-button>
.red>button {
  background-color: red;
}

.yellow>button {
  background-color: yellow;
}

.green>button {
  background-color: green;
}

↓実際に動作するサンプル

See the Pen aqPEBa by kwst (@kwst) on CodePen.

使い方はHTMLElementを継承したクラスを作って、customElements.defineというAPIを用いて好きなタグ名を登録します。

class MyButton extends HTMLElement {}
customElements.define("my-button", MyButton);

CustomElementsのライフサイクルイベント

HTMLElementには各種ライフサイクルイベントハンドラが用意されていて、以下のタイミングで処理を実行することができます。ReactのcomponentDidMount()componentWillUnmount()のように使えるためコンポーネントが作りやすくなっています。

constructor()

要素が生成された時に呼ばれます。

connectedCallback()

要素がdocumentに追加された時に呼ばれます。
Reactで言うところのcomponentDidMount()に当たります。

disconnectedCallback()

要素がdocumentから削除された時に呼ばれます。
Reactで言うところのcomponentWillUnmount()に当たります。

attributeChangedCallback(attributeName, oldValue, newValue, namespace)

属性に変更が加えられた時に呼ばれます。observedAttributesで指定された属性のみが監視対象になります。親から何かしら属性が変更された時のキックに使えます。

adoptedCallback(oldDocument, newDocument)

ownerDocumentが変更された時に呼ばれます。

↓ライフサイクルイベントを使って入力されたテキストと同期させるサンプル

See the Pen ddwmMJ by kwst (@kwst) on CodePen.

2. Shadow DOM – 要素を影に隠しこむ機能

名前の通り、外界へ影響を与えないように要素を隠しこんでしまう機能です。

Shadow DOMのサポート状況

今まではCSSで指定したセレクタはdocument全体に影響を与えていました。これは長年CSSが抱えてきたツラミの根源だと言っても過言では無いでしょう。Shadow DOMを使えば、スコーピングされた要素の中だけにセレクタの影響範囲を絞ることができます。

例えば先のボタンの例で下記のようにセレクタを指定していました。これではdocument上にある全てのbuttonタグに対してこのスタイルが反映されてしまいます。

button {
  border: none;
  border-radius: 3px;
  padding: 10px 20px;
}

Shadow DOMで定義された要素はshadow-rootに要素がぶら下がった形になり、スタイルの適応範囲はshadow-root内に限定されます。

#shadow-root
  <style>
    button {
      border: none;
      border-radius: 3px;
      padding: 10px 20px;
    }
  </style>
  <button>my button</button>

これを回避する方法として、BEMのような命名規則に則ってセレクタを記述する手法が一般的にはとられています。その他にはCSS Modulesのようにセレクタの名前にハッシュ値をプリプロセスで付け加えて一意のセレクタを生成するという方法もあります。

.block__element--mode { ... }

Shadow DOMはそういったワークアラウンドを使わずしてツラミから解放される手段となる(かもしれません)。attachShadowというAPIを使うとshadowRootが使えるようになります。このshadowRootがshadow DOMそのものになります。

this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = "<button>my button</button>";

下のサンプルで示すとおり、Shadow DOMで定義されたボタン以外にはスタイルは適応されていません。

See the Pen zRyjMy by kwst (@kwst) on CodePen.

CSSカスタムプロパティで外からShadow DOMのスタイルを変更する

外界から隔離できると言っても、Custom Elementsの例で示したように色違いのボタンを作りたいなど外から何かしらスタイルに変更を加えたいことが出てくると思います。その場合はCSSカスタムプロパティが使えます。関数の引数のような感覚で値をやりとりでき、柔軟なスタイル変更が可能になります。

CSSカスタムプロパティのサポート状況

外から--colorというカスタムプロパティで色を指定します。

<style>
.pink {
  --color: pink;
}
</style>
<my-button class="pink">my button</my-button>

Shadow DOM内のスタイルで--colorを使うことができます。これで外からスタイルを当てることができます。

button {
  background-color: var(--color);
}

新しく外から変更したいスタイルが出てきた場合にコンポーネントの中身をいじらないといけないので、実際に運用する上ではちょっとネックになってくるかもしれません。Shadow DOMを使ったUIライブラリを使う場合などには気をつける必要がありそうです。

slot要素でShadow DOMに中身を入れ込む

外界から隔離されてしまうので、普通にタグで子要素を囲っても入れ子にはなりません。Shadow DOMを使った要素の場合、slot属性を使って子要素を入れ込みます。

<my-button>
  <span slot="label">my button</span>
</my-button>

Shadow DOM側では以下のようにname属性で名前を指定します。その場所にslot="label"で指定した要素が入り込みます。

<button type="button">
  <slot name="label" />
</button>

slot属性でボタンの内部に要素を挿入するサンプル

See the Pen XZOeqv by kwst (@kwst) on CodePen.

Shadow DOM側からslotで指定された要素の参照をとりたい場合、HTMLSlotElementsのassignedNodesというAPIを使ってとることができます。Shadow DOMの状態によって要素のスタイルを変更したい場合などに有用です。タブUIをslotを使って組んでみました。

See the Pen bLyomg by kwst (@kwst) on CodePen.

HTMLのコードはこれだけでタブUIを表現しています。タブのコンテンツに関する部分のみで記述することができます。

<my-tab class="tab">
  <div slot="tab">Tab1</div>
  <div slot="tab-content">This is Tab1.</div>
  <div slot="tab">Tab2</div>
  <div slot="tab-content">This is Tab2.</div>
  <div slot="tab">Tab3</div>
  <div slot="tab-content">This is Tab3.</div>
</my-tab>

3. ES Modules – 外部JSファイルを動的に読み込む機能

外部のJSでexportされたオブジェクトなどをimport文で読み込む機能です。

ES Modulesのサポート状況

今まではJSファイルはscriptタグで読み込むしかなく、JSファイル単独では管理できませんでした。それを解決する手段として、TypescriptやBabelなどのトランスパイラやwebpackやbrowserifyといったモジュールバンドラでモジュール機能を実現しています。それがデフォルトで使えるようになります。

使い方は読み込まれたいJSファイルでexport文で書き出したいオブジェクトやクラスを設定します。

export default class MyButton {}

import文で読み込みたいJSファイルを指定してやります。CDNで公開されている、ES Modulesで設計されたライブラリなども読み込むことができます。

import * as Three from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.module.js';
import MyButton from "my-button.js";
const myButton = new MyButton();

HTMLでもtype="module"で読み込むこともできます。

<script type="module" src="my-button.js"></script>

ES Modules同様、外部HTMLファイルを動的に読み込む機能としてHTML Importsという構想があるのですが、これは各ブラウザのサポートが進んでおらずFirefoxはshipしないとのことなので、お蔵入りになる可能性が高いです。

HTML Importsのサポート状況

4. HTML Templates – 要素をテンプレートとして扱う機能

HTML要素をテンプレートとしてdocument上に配置し、使いまわすことができる機能です。

HTML Templatesのサポート状況

使いまわしたい要素をHTML上でtemplateタグで囲っておきます。囲われた要素はDocumentFragmentとしてふるまうので表示されません。仮にtemplate内にimgタグなどがあったとしてもリクエストは走りません。

<template id="my-button">
  <img src="">
  <button type="button"></button>
</template>

使うときは、importNodeを使って要素のクローンを生成します。cloneNodeを使うこともできます。

const template = document.querySelector("#my-button");
const clone = document.importNode(template.content, true);

clone.querySelector("button").innerText = "my button";
document.body.appendChild(clone);

↓サンプル

See the Pen qxgmgN by kwst (@kwst) on CodePen.

Custom Elementsを定義する時はどうしてもJSの中でHTMLの定義を書く必要があるのですが、この機能を使えばHTML側に書くこともできそうです。ただ、JSXなどを使ってJS側にタグを記述することに慣れている人にとってはそこまで使い勝手の良い機能ではないかもしれません。Template Instantiationというproposalが出されていて議論中ですが、これはHTML Templatesで定義されたテンプレートに挿入したいデータだけをJSで挿入できるという機能のようなので、これが実装されればテンプレートをJS側で扱いやすくなるかもしれません。これはまだproposal段階なので現時点ではどうなるのかわかりません。

組み合わせてサムネイルコンポーネントを作る – Custom Elements + Shadow DOM + ES Modules

Custom Elements、Shadow DOM、ES Modulesでカプセル化された再利用可能なコンポーネントモジュールを作ってみたいと思います。

pixabay.comという画像検索サイトのサムネイルのコンポーネントを模倣したものを作ってみました。渡されたデータを元に画像を表示して、マウスオーバーするとユーザ名・like数・fav数・コメント数などが表示されるよく見かけるようなサムネイルコンポーネントです。

外部から情報は属性で<custom-thumbnail>に渡しています。CustomThumbnailクラスではその属性を監視して要素を構築しています。

↓サンプル

See the Pen ddaZGR by kwst (@kwst) on CodePen.

上のコンポーネントを使用してpixabayの画像検索APIを使って画像を検索するデモを作りました。
Web Components Example

 

ソースコードはGitHub – SatoshiKawabata/WebComponentsExampleに置いてあります。ライブラリは一切使っておらず全てデフォルトの機能だけで実装してあります。

実際に使ってみてDOM要素の取り回しに課題あり

外部からイベントを設定しづらい

何と言ってもイベントの取り回しが最高に面倒でした。innerHTMLで文字列としてタグを記述している以上、Reactなどのように関数を直接タグ内に書くことができません。

↓ReactならJSXでこう書けます。

<button onClick={() => {}}>my button</button>

関数を渡せないので、DOM要素をquerySelectorなどで取ってきてaddEventListenerするなりonclickに直接関数を入れるなりしないといけません。『JSフレームワークの末端がWebComponentsになるのか、なれるのか、検証してみた』という検証記事でも言及されている通り、関数を直接渡せないのでJSXのような記法を使う仮想DOM系のライブラリとの相性はあまり良いとは言えません。

内部のDOM要素の取り回しが煩雑

内部ではDOM要素の更新をinnerHTMLでごっそり変更しているので、要素の繰り返しや可読性といった観点から仮想DOMを使いたいと思いました。『Web Components – React』という記事で紹介されているように、innerHTMLではなく内部的に仮想DOMにHTMLのレンダリングを任せることで取り回しの煩雑さを解消するのは有りだと思います。そもそも仮想DOMの趣旨はそういった煩雑さの解消なので当然の流れかもしれません。

まとめ – 今後の動向に期待

ここまでのことを何もライブラリを使わず実現できることにテンションが上がります。上記のように課題はありますが素直に感動しました。

各ブラウザのサポートが進んでいけば「Web Componentsで作られたUIライブラリから好きなコンポーネントを使うだけでアプリケーションが構築できる」とまではいかないにしても「デザインにまつわる実装(CSSなど)を全く書くことなくUIを構築する」くらいは可能になってくると思います。

まだまだサポート途上の技術なので実際のサービスで運用するにはちょっと早いかもしれませんが、『“Web Componentsだけ” で新サービスを実装して見えたこと』のように実践導入する事例や『Web Componentsで社内UIライブラリを作っている話』のように共通UIライブラリを作る事例も出てきました。webcomponents.orgでサポート状況やWeb Componentsで作られたコンポーネントなどの最新情報を注視しつつ、ReactやVue、RiotといったメジャーなライブラリとWeb Componentsとが相乗りした際のベストプラクティスがまだ無いのでそこにも目を向けていく必要がありそうです。Custom Elements EverywhereというサイトでCustom Elementsと各JSフレームワークの相性を見ることができます。また、GitHub – w3c/webcomponents: Web Components specificationsというレポジトリで仕様に関する議論がなされていますので、のぞいてみるのもいいかもしれません。(私はそこまで追いきれているわけではありませんが…)

JSライブラリ戦国時代の今、Web Componentsが次代の旗手となりうるのか、今後も動向を見守っていきたいです。


ヌーラボではそんな近未来の技術に興味のあるエンジニアを募集しております

開発メンバー募集中

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる