概要
WebComponentsって何か聞いたことあるけど部品の再利用でしょ?UIライブラリで良くない?って人向けのざっくりまとめ。深いところはあまり書かずにざーっと流して「何となくわかった気になった」感じになってもらえたら良いなと思います。結構長くなる(と思う)ので、気楽に「にゃーん」とか言いながら流し読んでください。
まず試してみたいんだ!という方は「実際に使ってみよう」まで飛んで頂ければと思います。
今までのWebにおける部品再利用のアプローチ
WebComponentsという言葉自体位は数年前から小耳に挟むことはありましたが、ここ最近は比較的良く聞く単語になりました。それはそれとして、今までWebにおける部品の再利用はライブラリやフレームワークに依存していました。依存していると言うことは依存しているものに準じる必要があり、そこから別のライブラリやフレームワークに移行するのであれば、やはり新しい依存先に関して学習を行う必要がありました。ここで利用しているものはWeb標準技術ではなく、ライブラリやフレームワークでした。WebComponentsはライブラリやフレームワークではなく、Web標準技術でこれらの問題を解決します。
WebComponentsのメリット
- ブラウザ上で動作する
動作しないブラウザもある
モダンブラウザであれば基本的に動くと思って良いが、プロダクトへの適用はまだ早い感があるかもしれない - ツールやライブラリ、フレームワークに依存しない
Web標準技術のため、流行り廃りではない。またライブラリやフレームワーク(サードパーティ)に影響されない - 既存技術と競合しない
ReactやVueと言ったUIライブラリと共存可能
WebComponentsに関する技術
Web標準技術と先に記しましたが、A+B+C=WebComponentsという構図ではないようです。ただ、一般的には以下の技術がWebComponentsを構成するものと紹介されています。
- Custom Elements
独自のHTMLとその動作を定義するJavascript API - Shadow DOM
DOMに独立したDOMツリーを定義し、DOMにスコープを実現させることができます
Shadow DOMに関してはこちらの記事がわかりやすいかもしれません - HTML Template
サーバサイドで愛されてきたテンプレートエンジンみたいなものが使えるようになります
こちらの記事がわかりやすいかもしれません - ES Modules
各ブラウザで実装されているJavascript(ECMA Script)は他言語と同じようにexport/importが行えます
ES対応状況は各ブラウザの実装状況を確認してみてください
これらの詳細に関しては後述していきます。HTML Templateに関しては説明を割愛する予定です(先に書いたURLが非常にわかりやすいです)。
Custom Elements
HTMLにおけるElementは非常にシンプルで基礎的なものです。そのElementを組み合わせてこだわりのあるデザインを持った部品を作ろうとすると、途端に汎用性の無い複雑なものになってしまいます。その汎用性の無い複雑なものに再利用性を見出すことは非常に難しいのではないかと思います。もしの話ですが、その再利用が難しいものを新しいHTML Elementとして定義できて、使いたい時はh1やpと同じように非常にシンプルで簡単に扱えるとしたらそれは非常に素晴らしいものではないでしょうか?Custom Elementsはそれを実現します。
Custom Elementの定義
それではさっそく独自のHTML要素を定義してみましょう。新しいHTML要素を定義するにはJavascriptが必要になります。customElementsオブジェクトからHTML要素の登録・参照が必要になります。簡単な例を以下に記します。
class sampleElement extends HTMLElement {
constructor() {
super();
}
}
customElements.define('sample-elem', sampleElement);
上記の例では<sample-elem>
という要素を定義しています。ここで注意しなければならないのはカスタム要素の名前には-
を含めなければならないという点と第二引数に与えるClassはHTMLElementを継承する必要がある点の2点です。
登録していないHTML要素を使うとHTMLUnknownElementとして解釈される...はずです、ちょっと自信無いかも。
Custom Elementのライフサイクル
カスタム要素にはライフサイクルが存在します。アプリ開発経験者の方は、思い付くライフサイクルと同じイメージで大丈夫です。クラスに定義するコールバックメソッドでフックすることで振る舞いを制御することができます。UIライブラリを使ったことのある方も馴染みがあるのではないでしょうか。
class sampleElement extends HTMLElement {
// 属性監視オブザーバ
static get observedAttributes() { return ['hoge', 'piyo']; }
// 属性getter/setter
get foo() { return this.getAttribute('foo'); }
set foo(value) { this.setAttribute('foo', value); }
get piyo() { return this.getAttribute('piyo'); }
set piyo(value) { this.setAttribute('piyo', value); }
// Custom Elementが作成されたタイミング
constructor() {
super();
console.log('constructor');
}
// Custom ElementがDOMに追加されたタイミング
connectedCallback() {
console.log('connectedCallback');
}
// Custom ElementがDOMから削除されたタイミング
disconnectedCallback() {
console.log('disconnectedCallback');
}
// Custom Elementの属性が追加/削除/編集されたタイミングで呼ばれる
attributeChangedCallback(attributeName, oldValue, newValue, namespace) {
console.log('attributeChangedCallback');
if (attr === 'hoge') {
console.log(`hoge: ${newValue}`);
}
if (attr === 'piyo') {
this.updateRendering();
}
}
// Custom Elementが属するDOMが変わったタイミングで呼ばれる
adoptedCallback(oldDocument, newDocument) {
console.log('adoptedCallback');
}
updateRendering() {
console.log('_updateRendering');
}
}
customElements.define('sample-elem', sampleElement);
attributeChangedCallback
に関してはあらかじめ監視対象の属性をobservedAttributes
メソッドで定義しておく必要があります。また、Javascriptから属性を参照したり値を編集したりすることもあると思いますのでgetter/setterを用意しておくと良いかと思います。
Custom Elementクラスの参照
customElements.define
でカスタム要素を定義できるのは先述の通りですが、定義済みのカスタム要素を参照する場合はcustomElements.get
を利用します。
class sampleElement extends HTMLElement {
constructor() {
super();
}
}
customElements.define('sample-elem', sampleElement);
// sampleElement
console.log(customElements.get('sample-elem'));
// undefined
console.log(customElements.get('sample-hoge'));
未定義の名前を引数に与えるとundefinedが返って来ます。
Custom Elementが有効になるタイミングについて
カスタム要素の実装で気を付けなければいけない部分として、customElements.define
で定義されるまでカスタム要素は評価されません。<sample-elem>Hello custom element!</sample-elem>
というようなHTMLを書いた場合、スタイルが適用されない状態でHello custom element!と表示される可能性があります。カスタム要素は定義する前でも使えてしまう、ということですね。
これを回避する場合、customElements.whenDefined
を使うのが最も簡単な方法の1つでしょう。
customElements.whenDefined('sample-elem').then(() => {
console.log('sample-elem defined');
});
初期スタイルは非表示スタイルを当てておき、whenDefined
でPromiseオブジェクトが返ってきたところで表示してあげるよ良いでしょう。
Shadow DOM
DOMやCSSOMはプログラムで言うところのグローバル領域に格納されるものになります。つまりスコープという概念が無く、定義は後勝ちとなります(CSSを思い浮かべてもらうとわかりやすいと思います)。この問題は長らくWeb開発者を悩ませて来ました。
これを回避する方法としてCSSの命名規則や設計思想、ライブラリやツールといったアプローチがあります。しかしながら、これらは問題を根本的に解決する方法ではありません。それを解決するのに現れたのがShadow DOMです。
カプセル化
Shadow DOMはShadow Rootを境界にDOMに対してスコープを生成します。Shadow Rootの中の情報/評価はShadow Rootの外に影響を及ぼさず、Shadow Rootの外の情報/評価はShadow DOMの中に影響を及ぼしません。これはStyleやScriptはお互いに干渉せず、DOM毎に独立しているということを意味します。これは先述したWeb開発者を長らく悩ませていた問題を解決する1つのアプローチになるものです。
ここで少しわかりやすい例を上げてみましょう。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Shadow DOM Sample</title>
</head>
<body>
<section>
<p>Hello DOM.</p>
</section>
<section>
# Shadow Root
<style>
p { color: red; }
</style>
<p>Hello DOM.</p>
</section>
</body>
</html>
上記はあくまでも例でこのように実装できるわけではありません。
この例だとシンプルなDocument Treeが生成されます。このTreeの要素の下にDOM Tree(Shadow host)を追加し、親DOMとShadow DOMが相互に影響を及ぼし合わないTree構造を生成する技術がShadow DOMの最たる特徴の1つです。
shadow DOM の使い方 - Web Components | MDNが素晴らしくわかりやすいので、一読しておくべきでしょう。
Shadow DOM生成
理屈や概要はさておいて実際にShadow DOMを生成してみましょう。大きな流れは以下の通りです。
- Element.attachShadowでShadow DOMを生成
- 生成したShadow DOMにNode/Elementを追加
非常にシンプルで簡単ですね。コードで書くと以下のようになります。
const div = document.querySelector('div');
const root = div.attachShadow({
// mode
});
const elem = document.createElement('p');
elem.textContent = 'Shadow DOM Sample';
root.appendChild(elem);
attachShadowのmodeパラメータに関しては後述します。
attachShadowができる(Shadow hostにできる)要素は以下の通りです。
Custom Element
, body
, header
, footer
, nav
, main
, aside
, article
, section
, h1
, h2
, h3
, h4
, h5
, h6
, div
, p
, span
, blockquote
mode: open/closed
先に出てきたmodeについて簡単に説明すると「外部からShadow DOM内部にアクセスを許可するか」を決定します。基本的にはopenで実装し、closedにしなければならない(外部から操作されたくない)ものがある場合のみclosedにする形で問題ないかと思います。modeをopenにしてもDOMとしてはお互い干渉しません、あくまでもElement.shadowRootプロパティの参照について設定していると理解して頂ければ問題ありません。
Shadow DOMに対する装飾
実際のWebではブラウザの標準スタイルで表示されているサービスやページはかなり少ないと言って間違いないでしょう。この項ではShadow DOMに装飾を当てる方法を軽く説明していこうと思います。
自分の説明よりこちらの方が非常にわかりやすいと思います...そこは気にせず進めていきます。
styleやscriptもDOM間で干渉しないのでセレクタの衝突を気にする必要はありません。もうBEMやらOOCSSといった命名規則や設計でカバーする必要はありません(と言うと大袈裟ですね)。ただ、CSSでShadow DOMを装飾する時に少しだけ覚えておく必要があることがあります。
- :host
- :host(<selector>)
各CSSセレクタをコードベースで説明をしていこうと思います。HTML, Javascript, CSSは以下の通りです。この他にも:host-context
や::slot
、:shadow
等はおまけ的に後半で書くかもしれません(書かないかもしれません)。
<p>hoge</p>
<div></div>
const div = document.querySelector('div');
const root = div.attachShadow({
mode: 'open'
});
root.innerHTML = `
<style>
:host {
background-color: gray;
}
:host(:hover) {
background-color: black;
}
p {
color: red;
}
:host span {
color: white;
}
</style>
`;
const elemP = document.createElement('p');
elemP.textContent = 'Shadow DOM Sample text1';
root.appendChild(elemP);
const elemSpan = document.createElement('span');
elemSpan.textContent = 'Shadow DOM Sample text2';
root.appendChild(elemSpan);
p, span {
color: blue;
}
一応上記内容のJSFiddleを置いておきます。もし良ければ実際に軽く触ってみて下さい。
:host
はhost(shadow-root)そのものを指します。これと擬似クラスもそう問題はありませんね。おそらく疑問を抱くのはdivの装飾がグローバルなCSSが優先になるのか:host
での指定が優先になるのかという部分だと思います。これに関してはグローバルなCSSが優先になります。CSSにdiv{background-color:blue;}
と追加して動作を確認してみてください。これだけであれば然程難しいことではありませんね。Javascriptに直接styleを書かず、Shadow DOM用の.cssを作成し、Shadow DOM内で.cssを読み込む方法もあります。
ES Modules
これまでも書いてきましたが、Custom ElementやShadow DOMはJavascriptで生成できます。WebでのJavascriptのロードと言えばscriptタグでの読み込みが主流でしたが、ECMAScript2015ではモジュール構文が策定され、ブラウザにES Moduleとして実装されました。ここではES Moduleがどのようなものか軽く触れてみます。
export/importの簡単な説明
モジュール構文の基本はexport
でのエクスポートとimport
でのインポートになります。そのまま和訳しているだけですね。まずはexportを試してみましょう。
// app.js
import {
log1,
log2
} from './ex';
import {
hoge
} from './class';
log1();
log2();
const obj = new hoge();
obj.say();
// ex.js
function log1() {
console.log('log1');
}
function log2() {
console.log('log2');
}
export {
log1,
log2
};
// class.js
class hoge {
say() {
console.log('hoge->say()');
}
}
export {
hoge
};
Node.jsで確認する時は拡張子を.mjsに修正して、node --experimental-modules app.mjs
で動かして見て下さい。.mjsが何か、Node.jsとは何かの説明は割愛します。興味があれば是非調べてみてください。
ex.js, class.jsは以下のように書くこともできます。
// ex.js
export function log1() {
console.log('log1');
}
export function log2() {
console.log('log2');
}
// class.js
export class hoge {
say() {
console.log('hoge->say()');
}
}
実際に使ってみよう
Custom Element、Shadow DOM, ES2015、WebComponentsを扱う上で必要な技術を軽く触ってきました。ここからは「実際に」使っていこうと思います。
お試し用コード
まずは適当なディレクトリにindex.htmlとmain.jsを用意して以下のコードを書いてみてください。
<!-- index.html -->
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Web Components Test</title>
<style>
p {
color: blue;
}
</style>
</head>
<body>
<p>Sample text</p>
<sample-elem></sample-elem>
<script type="module" src="./main.js"></script>
</body>
</html>
// main.js
class sampleElement extends HTMLElement {
constructor() {
super();
console.log('constructor');
this.attachShadow({
mode: 'open'
});
this.shadowRoot.innerHTML = `
<style>
p {
color: red;
}
</style>
<p>Web Components Test</p>
`;
}
}
customElements.define('sample-elem', sampleElement);
コードを書いたらブラウザ上で実行してみましょう。PHPがインストール済みであればterminalからinde.htmlがあるディレクトリに移動してphp -S localhost:8080
とタイプし実行すると簡易的なWebサーバを起動できるので、この方法が良いかも知れません。
実行したら青いテキストSample text
と赤いテキストWeb Components Test
が表示されているでしょうか?表示されていれば成功です。Chrome dev toolで赤いテキストWeb Components Test
の部分を見てみて下さい。適用されているstyleに注目して下さい。
ライフサイクルを利用する
シンプルにUI部品として再利用するだけであれば上記の通りで問題は無いのですが、動的な処理をさせる場合は以下のようにライフサイクルを利用すると良いでしょう。main.jsを以下のように書き換えてブラウザで実行してみて下さい。
// main.js
class sampleElement extends HTMLElement {
constructor() {
super();
console.log('constructor');
this.attachShadow({
mode: 'open'
});
this.shadowRoot.innerHTML = `
<style>
p {
color: red;
}
</style>
<p id='ev1'>Web Components Test</p>
<p>hoge</p>
`;
this.ev1 = this.shadowRoot.getElementById('ev1');
}
connectedCallback() {
console.log('connectedCallback');
this.addEventListener('click', this.onClickAll);
this.ev1.addEventListener('click', this.onClickP);
}
disconnectedCallback() {
console.log('disconnectedCallback');
this.removeEventListener('click', this.onClickAll)
this.ev1.removeEventListener('click', this.onClickP);
}
onClickAll(e) {
console.log('onClickAll !');
}
onClickP(e) {
console.log('onClickP !');
}
}
customElements.define('sample-elem', sampleElement);
実行したら表示されているWeb Components Test
とhoge
をクリックしてみて下さい。コードを見ればわかりますが、Web Components Test
をタップした時には紐付けられているイベントが2つ実行されて、hogeをクリックした時には1つのイベントが実行されます。このようにライフサイクルを利用することでデザインそのものを動的に変更したり、振る舞いを定義することができます。
最後に
lit-htmlやslot要素、等々については後日まとめようと思います。