この記事はelectronアドベントカレンダー 2016 21日目の記事です。
遅くなってしまい申し訳ありません。。。
※アドベントカレンダーのリンクが間違っていたので修正しました・・・汗
前置き
↓去年はこんな記事を書いていました。
このCSS Grid Layout Module Level1ですが、少しずつ仕様の策定が進み、とうとう勧告候補の段階まできました。
CSS Grid Layout Module Level 1CSS Grid Layout Module Level 1 (日本語訳)
CanIUseを見ると、もうすぐFirefoxとChromeでの対応が行われるようです。
http://caniuse.com/#search=grid
https://developer.mozilla.org/ja/Firefox/Releases/52
未来は意外と早く来るもんですね。
本題
前置きが長くなりましたが、ここからが本題です。
近年のフロントエンド界隈では、ReactやらAngularやらのコンポーネント指向なライブラリ/フレームワークがもてはやされていると思います。
ですが、「ちょっとUIを部品化してみたい」という程度のケースでは、Reactなどのライブラリは少々OverKillな代物では、と感じます。
この手のライブラリを導入すると、なんだかんだで大量のツールチェインが必要になって大変ですよね。
一方で、UIをコンポーネント単位で部品化するための方法としてWebComponentsという仕様の策定が進められています。
主要ブラウザ全てで対応されるのはまだまだ先の話になりますが、このWebComponentsの機能はChrome54以降ではすでに実装されています。
Electron環境に限定すれば、わざわざReactやAngularを使わずとも、Web標準な方法でコンポーネント指向開発ができる未来が、もうすぐそこに来ています。
この記事では、そんなWebComponentsについてご紹介したいと思います。
補足
ReactにはUIのコンポーネント化だけでなく、ステートレスなコンポーネントとか一方向のデータフローなど、単純にUIを部品化する以上の目的があると思います。
なので、Web Componentsが使えるようになれば、ReactやAngularなどが不要になるということはないと思います。
開発対象の規模や用途に応じてケースバイケースで技術選定する上での、もう一つの選択肢ができるというイメージでしょうか?
ElectronのChrome54対応
現時点(2016/12/25現在)でElectronの内部で使用されているChromiumは53.0.2785.143となっています。
そのため、今のElectron最新版(v1.4系)では、WebComponentsの機能はまだ一部しか使えません。
ということで、この記事のサンプルコードは現在のElectronでは使用できません。
(Electronアドベントカレンダーの記事なのに・・・)
次のElectronメジャーバージョンアップでは、Chromium54以降のバージョンのものになると思われます。
以下のプルリクで対応が進められているようなので、動向が気になる人はこれをウォッチしているとよいかと。
Chrome 54 update by groundwater · Pull Request #7909 · electron/electron · GitHub
ElectronとChromiumの関係について
余談ですが、Electronは通常であればChromiumのバージョンアップに1~2週間遅れくらいで追従していました。
http://electron.atom.io/docs/faq/#when-will-electron-upgrade-to-latest-chrome
しかし、Chrome54から、Chromeのビルドツールがgypからgnというツールに変わりました。
これに伴い、Electron内部で使用するChromiumのバージョンアップに、いつもより多くの時間がかかっているようです。
Chrome54が9月にリリースされたので、12月にはElectronでもこの機能が使えるようになる、、、と踏んでこんな記事を用意してましたが、完全に誤算でした。。。
Web Componentsとは
WebComponents自体の詳細な解説は、すでにWeb上に優良な記事が多数あるので、ここでは詳細な説明は割愛し概要だけ取り上げることとします。
一言で説明すると↓こんな風に、独自タグを定義して別途作りこんだUI部品を組み込んでいけるような仕組みです。
<body><sample-element></sample-element></body>
使ってみる
Web Componentsを構成する要素
Web Componentsは、以下の4つの技術を組み合わせて実現されています。
- Custom Elements v1
- HTML templates
- HTML Imports
- Shadow DOM v1
これら4つの要素を順番に使い、Web Componentsの基本的な動作を見ていきます。
サンプルコード
現時点のElectronでは、まだWebComponentsの機能は未実装な部分があるので、今回のサンプルコード類は動かせません。
サンプルコードは以下の場所に上げていますが、これは普通に静的なWebサーバーを立ち上げて、ブラウザで動作確認をするものです。
Chrome54以降のブラウザであれば、このサンプルコードの動作確認ができるかと思います。
一応、Electronの次バージョンがリリースされたら、Electron用のサンプルコードを作って追記しようと思います。
Custom Elements v1
まずは、Custom Elementsから。
Custom Elementsでは、独自タグを定義してhtmlやJavaScriptから利用できるようにすることができます。
Web Componentsの肝になる部分です。
HTMLElementなどのベースとなる型を継承して、独自タグ用のクラスを作ります。
そして、customElements.define()
という関数に、タグ名と先ほど作成したクラスを指定して実行します。
index.js
class SampleElement extends HTMLElement { constructor() {super(); } connectedCallback() {this.innerHTML = ` <div> <h1>Sample Component</h1> <p> CustomElements v1のサンプル </p> <button>sample</button> </div>`; }} customElements.define('sample-element', SampleElement);
これでsample-element
というタグが定義されて、html上から使用できるようになります。
以下のように書いてページを表示してみると、先ほどinnerHTMLに文字列で渡したDOM要素が描画されることが確認できます。
index.html
<!DOCTYPE html><html><head><metacharset="UTF-8"><title>Document</title><scriptsrc="index.js"></script></head><body><sample-element></sample-element></body></html>
HTML templatesと組み合わせる
先ほどの例では、コンポーネントの中身となるDOM要素を、JavaScript中に文字列として定義していました。
文字列として書いてしまっているので、エディタでのシンタックスハイライトも効かないですし、コード短縮化など、htmlに関わる各種ツール類との連携もできません。
これはイケてませんね。
ということで、templateタグを使い、DOM要素の定義をhtmlファイル側に移動します。
index.js
class SampleElement extends HTMLElement { constructor() {super(); } connectedCallback() {const template = document.querySelector('#sample-element-template'); const instance = template.content.cloneNode(true); this.appendChild(instance); }} customElements.define('sample-element', SampleElement);
index.html
<!DOCTYPE html><html><head><metacharset="UTF-8"><title>Document</title><scriptsrc="index.js"></script><templateid="sample-element-template"><div><h1>Sample Component</h1><p> CustomElements v1のサンプル </p><button>sample</button></div></template></head><body><sample-element></sample-element></body></html>
実行結果は特に変わりません。
HTML Imports
html側にコンポーネントのDOM要素の定義を持ってくることができました。 しかし、このままではコンポーネント利用者側のhtmlに、templateタグによるコンポーネント定義が残ってしまいます。
これらを、HTML Importsの機能を使って外部にもっていきましょう。
ついでにフォルダ構成なども変更し、以下のような構成にしてみます。
sample-elementというフォルダ内に、コンポーネント定義をすべて閉じ込めることができました。
sample-element/sample-element.js
class SampleElement extends HTMLElement { constructor() {super(); } connectedCallback() {const ownerDocument = document.currentScript.ownerDocument; const template = ownerDocument.querySelector('#sample-element-template'); const instance = template.content.cloneNode(true); this.appendChild(instance); }} customElements.define('sample-element', SampleElement);
sample-element/sample-element.html
<templateid="sample-element-template"><div><h1>Sample Component</h1><p> CustomElements v1のサンプル </p><button>sample</button></div></template><scriptsrc="sample-element.js"></script>
index.html
コンポーネント利用者側は、HTML Importsを使って以下のようにコンポーネント定義ファイルを読み込むだけで使えるようになります。
<linkrel="import"href="sample-element/sample-element.html">
<!DOCTYPE html><html><head><metacharset="UTF-8"><title>Document</title><linkrel="import"href="sample-element/sample-element.html"></head><body><sample-element></sample-element></body></html>
実行結果は変わらないので省略します。
Shadow DOM
CSSには名前空間のようなスコープはありません。
意図しない場所への影響を与えないように、クラス名の命名規則で頑張ったり、セレクタの書き方を工夫して衝突回避していました。
ですが、Shadow DOMを使えば、Shadow Rootという他のDOM要素からの影響を受けない領域を作ることができます。
Shadow Rootの内部で書いたスタイルなどは、コンポーネントの外部には影響を与えないので、クラスの命名規則でムリヤリ頑張ったりしなくても、スタイルの衝突を防ぐことができます。
sample-element/sample-element.js
class SampleElement extends HTMLElement { constructor() {super(); } connectedCallback() {const ownerDocument = document.currentScript.ownerDocument; const template = ownerDocument.querySelector('#sample-element-template'); const instance = template.content.cloneNode(true); // ShadowDOMの構築let shadowRoot = this.attachShadow({mode: 'open'}); shadowRoot.appendChild(instance); }} customElements.define('sample-element', SampleElement);
sample-element/sample-element.html
<templateid="sample-element-template"><style>.title{color: red; }button{background: lightblue; }</style><div><h1class="title">Sample Component</h1><p> CustomElements v1のサンプル </p><button>sample</button></div></template><scriptsrc="sample-element.js"></script>
index.html
<!DOCTYPE html><html><head><metacharset="UTF-8"><title>Document</title><style>button{font-size: 20px; }</style><linkrel="import"href="sample-element/sample-element.html"></head><body><sample-element></sample-element><hr /><h2class="title">Web Componentsの呼び出し元</h2><button>ほげほげ</button></body></html>
コンポーネントの内部/外部で、スタイルが互いに干渉していないことが確認できます。
具体的なサンプル
以上で、WebComponentsを構成する一通りの要素の使い方を見てきました。
具体的なコンポーネントの例として、シンプルなストップウォッチのコンポーネントを作ってみました。
stopwatch-element.html
<templateid="stopwatch-element-template"><div><spanid="content"></span><buttonid="btnStartStop">start</button><buttonid="btnReset">reset</button></div></template><scriptsrc="stopwatch-element.js"></script>
stopwatch-element.js
class StopwatchElement extends HTMLElement { constructor() {super(); this.baseTime = null; this.offset = null; this.timerId = null; }/** DOMに要素が追加された際に発生するイベント */ connectedCallback() {const ownerDocument = document.currentScript.ownerDocument; const template = ownerDocument.querySelector('#stopwatch-element-template'); const instance = template.content.cloneNode(true); let shadowRoot = this.attachShadow({mode: 'open'}); shadowRoot.appendChild(instance); this.content = shadowRoot.querySelector("#content"); this.showTime(0); // 各種イベントハンドラの設定this.btnStartStop = shadowRoot.querySelector("#btnStartStop"); this.btnStartStop.addEventListener('click', this.onStartStop.bind(this)); let btnReset = shadowRoot.querySelector("#btnReset"); btnReset.addEventListener('click', this.onReset.bind(this)); }/** start/stopボタン押下時のイベント */ onStartStop() {if (!this.timerId) {this.btnStartStop.textContent = "stop"; this.baseTime = Date.now(); this.timerId = setInterval(() => {let ellapse = this.offset + Date.now() - this.baseTime; this.showTime(ellapse); }, 10); }else{ clearInterval(this.timerId); let ellapse = Date.now() - this.baseTime; this.offset += ellapse; this.timerId = null; this.btnStartStop.textContent = "start"; }}/** resetボタン押下時のイベント */ onReset() { clearInterval(this.timerId); this.showTime(0); this.timerId = null; this.baseTime = null; this.offset = null; }/** 経過時間を表示するための関数 */ showTime(time) {let pad = (num, digit) => ('000' + Math.floor(num)).slice(-digit); let h = pad(time / (60*60*1000), 2); time = time % (60*60*1000); let m = pad(time / (60*1000), 2); time = time % (60*1000); let s = pad(time / 1000, 2); time = time % 1000 let ms = pad(time, 3); this.content.textContent = `${h}:${m}:${s}.${ms}`; }} customElements.define('stopwatch-element', StopwatchElement);
index.html
<!DOCTYPE html><html><head><metacharset="UTF-8"><title>Document</title><linkrel="import"href="stopwatch-element/stopwatch-element.html"></head><body><stopwatch-element></stopwatch-element><hr /><stopwatch-element></stopwatch-element><hr /></body></html>
ただテキストで経過時間が表示されるだけのものですが、コンポーネントに関わるUI定義やロジックを、再利用しやすいように独立して記述できるのがわかると思います。
まとめ
以上、駆け足でElectronでのWeb Componentsの使い方を見てきました。
この記事のサンプルでは、フロント側では特にライブラリを使用していません。
(ブラウザでの動作確認のために、テスト用Webサーバーとしてnode-staticを使っているだけ。)
ReactもAngularも使ってないですが、ちゃんとコンポーネント指向な開発ができる、という雰囲気はつかめていただけたのではないでしょうか?
未来感ハンパねぇですね!!
Electronの次のメジャーバージョンアップを楽しみに待ちましょう♪