SPAを作るために必要となる設計思想Flux。今回はWebComponentsを用いてVanillaJSでFluxを実装してみたいと思います。
なお今回のコードを書くにあたって10分で実装するFluxを参考にさせていただいています。
まず、Fluxのデータの流れをおさらいしましょう。かなり簡単に書くと以下のような感じだと思います。
View -> Action -> Dispatcher -> Store -> View -> ...
まずユーザーがWebサイトにアクセスすると何らかのViewが表示されます。そしてユーザーが何らかの行動を起こしActionが発火します。Actionはアクション名と必要なデータをDispatcherを通してStoreに伝えます。StoreはActionに応じて状態(state)を更新します。そして、更新したことをViewに伝え、Viewは新しい状態に基づいて再描画します。
ActionはDispatcherを使ってStoreへ、StoreはDispatcherを使って任意のActionを監視します。そのため、DispatcherはEventEmitterで実装できそうです。なお、ブラウザにEventEmitterは標準で実装されていないので、今回は代わりにEventTargetを使います。
Storeも状態を更新したことをViewに伝え、Viewは任意のStoreを監視します。つまり、StoreもEventTargetを使って実装します。
Viewは今回は、WebComponentsのCustomElementsを用いて実装してみたいと思います。
なお今回実装するのは、ボタンを押したらカウントが増えるアプリです。
まず、Dispatcherを見てましょう。DispatcherはEventTargetそのものです。
export default new EventTarget();
では次にActionを見てましょう。
import dispatcher from 'dispatcher.mjs';
export default {
countUp() {
dispatcher.dispatchEvent(new CustomEvent('countUp'));
}
}
Dispatcherを利用してActionをStoreに伝えるにはこのようにnew CustomEvent()
を使います(new Event()
でもよい)。CustomEventを使うとデータも渡せます。データは
eventTarget.dispatchEvent(new CustomEvent('eventName', { detail: data }));
と書くことによって、受け取り側は、
eventTarget.addEventListener((e) => { const data = e.detail });
と書くことによってデータを受け取れます。
では次にStoreを見てみましょう。
import dispatcher from 'dispatcher.mjs';
const initialState = {
count: 0
};
class Store extends EventTarget {
constructor() {
super();
Object.assign(this, initialState);
dispatcher.addEventListener('countUp', () => {
this.count++;
this.dispatchEvent(new Event('CHANGE'));
//stateを変更したことをcomponentに伝える
});
}
}
export default new Store();
'countUp'
アクションをリッスンして、内部のthis.count
というstateを更新していますね。そして、'CHANGE'
イベントを発火させています。こうすることによって、Viewにstateが更新したことを伝えることができます。
では次に、View(Component)を見てみましょう。
import actions from 'actions.mjs';
import store from 'store.mjs';
const html = `
<p>Count: <span>${store.count}</span></p>
<button type="button">Count Up</button>
`;
export default class Component extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = html;
this.span = shadowRoot.querySelector('span');
shadowRoot.querySelector('button').onclick = () => {
actions.countUp();
}
this.handleStoreChange = this.handleStoreChange.bind(this);
}
connectedCallback() {
//storeのstateの変更を監視する;
store.addEventListener('CHANGE', this.handleStoreChange);
//最初の描画;
this.handleStoreChange();
}
disconnectedCallback() {
store.removeEventListener('CHANGE', this.handleStoreChange);
}
handleStoreChange() {
//storeのstateが変更されたらcomponentのstateへ渡す;
this.count = store.count;
}
set count(value) {
//componentのstateが新しい値に置き換わった時のみ描画する
if(value === this._count) return;
this._count = value;
this.span.innerHTML = value;
}
}
少し長いですね、順を追って見ていきましょう。
まず、CustomElementsを作るときはHTMLElementを継承したクラスを作ります。そして、constructor内でshadowRootをCustomElementsに追加し、そのshadowRootのinnerHTMLにベースとなるhtmlを代入します。これはお決まりのパターンでshadowRootを追加しないでconstructor内でhtmlを直接CustomElementsに代入すると、document.createElement()
時にエラーになるので気を付けましょう。
また、constructor内でbutton要素のonclickを登録しています。ボタンがクリックされたらActionが呼ばれるようにしています。
constructorの下にconnectedCallbackとdisconnectedCallbackというメソッドがあります。これはCustomElementsのライフサイクルの一つで、このCustomElementsがdocumentに追加された時connectedCallback()
が、切断された時はdisconnectedCallback()
が呼ばれます。
ここでは、connectedCallbackでStoreの監視を始めています。監視しているStore内で'CHANGE'イベントが発火されると、handleStoreChangeメソッドが呼ばれます。なお、Viewがdocumentに追加されたタイミングで一回handleStoreChangeメソッドを呼び、Storeからstateを取得しています。
そして、disconnectedCallbackではStoreの監視を止めています。メモリリーク防止のためです。
では、handleStoreChangeメソッドでは何を行っているのでしょう。Componentのsetterであるcountにstore.countを代入しています。実はComponentは内部にstateを持つようにしています。stateを持つことによって、stateが変更された部分のみが差分で描画されるようになっています。
set count()
では、Storeから渡されたstateが新しい値に置き換わっているか調べていて、新しい値が渡された時のみ、DOMを更新します。
このように、ViewにCustomElementsを使った場合、setter内部で描画を更新する処理を書きます。
以上Viewを見てきました。では、このComponentをdocumentに追加しましょう。以下のように書きます。
import Component from 'Component.mjs';
customElements.define('x-component', Component);
document.body.innerHTML = '<x-component></x-component>';
まず作ったCustomElementsを定義します。customElements.define(要素名, Component)
で定義できます。要素名には必ず一つの-(ハイフン)が必要です。
これを実行した結果が以下です。以下のコードをブラウザのコンソールに直接入力してもカウントアップアプリができます。(SafariはEventTargetのpolyfillが必要です。)
See the Pen
CustomElements-Flux-sample by shigure (@webkatu)
on CodePen.
以上、FluxをVanillaJSで実装してみました。以下のブログ記事にこのFlux実装を基にSPAを作る方法を簡単なサンプルを交えながら説明しています。
フレームワークなしで作るSPA