WebComponentsでFlux

(この記事は実験的な記事です。フレームワークなしでSPAを作るために、WebComponentsを用いてFluxを実装するを参照してください。)

今回は、Reactや他のフレームワークを使わず、WebComponentsのCustomElementsだけでFluxのコンポーネントを実装する方法を書いていく。

(初めに、今回のサンプルで示すコードは全てGitHubで見れる。サンプルはサンプルページで見れる(polyfillを使ってないのでChromeが必要)。そして、今回のFlux実装は10分で実装するFluxを参考に書いているので、そちらも参照してもらいたい。)

さて、Fluxのコンポーネントの役割には、状態(state)が変更されたらViewを更新するというのがある。これをどのようにCustomElementsで行えるか。

CustomElementsにはattributeChangedCallbackというメソッドがある。これは、attributeが変更された時、それがobservedAttributesで監視しているattributeであった場合に呼ばれるメソッドだ。これを利用する方法もあるかもしれないが、attributeには文字列しか指定できないため、文字列以外の状態(state)の変更を受け取るには難しい。

ただ、この一連の流れは応用できそうだ。attributeが変更された時に、attributeChangedCallbackが呼ばれるなら、状態(state)が変更された時に、stateChangedCallbackというメソッドを呼ぶようにしたらどうだろうか。setStateでstateを変更して、observedStateで監視対象のstateを指定して、監視対象のstateが変更されたらstateChangedCallbackを呼ぶという流れだ。

import dispatcher from './dispatcher';
import action from './action';
import store from './store';

const template = document.createElement('template');
template.innerHTML = `
<span></span>
<button type="button">+</button>
`;

export default class Component extends HTMLElement {
	constructor() {
		super();

		const content = template.content.cloneNode(true);

		this.span = content.querySelector('span');
		const incrementButton = content.querySelectorAll('button')[0];

		incrementButton.addEventListener('click', (e) => {
			action.countUp();
		});

		this.appendChild(content);

		store.on('CHANGE', () => {
			this.setState({ count: store.count });
			//this.setState(store)と書いても問題ない;
		});
		
		//初期処理;
		this.setState({ count: store.count });
	}

	setState(state) {
		if(typeof state !== 'object' || state === null) return;
		
		if(typeof this.state !== 'object' || this.state === null) this.state = {};

		const oldState = this.state;
		const newState = this.state = Object.assign({}, this.state, state);

		if(Array.isArray(this.constructor.observedState) === false) return;
		if(typeof this.stateChangedCallback !== 'function') return;

		this.constructor.observedState.forEach((name) => {
			if(oldState[name] === newState[name]) return;
			this.stateChangedCallback(name, oldState[name], newState[name]);
		});
	}

	stateChangedCallback(name, oldValue, newValue) {
		switch(name) {
			case 'count':
				this.span.textContent = newValue;
				break;
		}
	}

	static get observedState() {
		return ['count'];
	}
}

customElements.define('x-component', Component);

このコードはstoreに変更があると、storeから必要なstateを取得して、setStateを実行している。setStateが実行されると監視対象のstate(この場合’count’)に変更がないか調べる。もし変更があれば、stateChangedCallbackを呼ぶようになっている。stateChangedCallbackで、変更のあったstateに応じてViewを更新する。

このように、コンポーネントに新しいライフサイクルを追加することによって、Fluxのコンポーネントの役割である「状態(state)が変更されたらViewを更新する」を実現できるようになる。

さて、このsetStateメソッドをコンポーネント毎に一々実装していられないので、モジュール化してみる。これをコンポーネントが必要に応じて呼び出すのがいいだろう。npmにもusestateという名で上げてみた。

const map = new WeakMap();
const privates = (object) => {
	if(! map.has(object)) {
		map.set(object, {});
	}
	return map.get(object);
};

function getState() { return privates(this).state; }

function setState(state) {
	if(typeof state !== 'object' || state === null) return;
	
	const that = privates(this);
	if(typeof that.state !== 'object' || that.state === null) that.state = {};
	const oldState = that.state;
	const newState = that.state = Object.assign({}, that.state, state);

	if(Array.isArray(this.constructor.observedState) === false) return;
	if(typeof this.stateChangedCallback !== 'function') return;

	this.constructor.observedState.forEach((name) => {
		if(oldState[name] === newState[name]) return;
		this.stateChangedCallback(name, oldState[name], newState[name]);
	});
}

export default function useState(target) {
	if(typeof target !== 'function') throw new TypeError();

	target.prototype.getState = getState;
	target.prototype.setState = setState;
	return target;
}

これを以下のように呼び出すと、コンポーネントはsetStateメソッドを使えるようになる。

import useState from './useState';
class Component extends HTMLElement { /* ... * /}

export default useState(Component)

また、現在ES Proposal stage2のDecoratorsを使って書くこともできる。(要babel)

import useState from './useState';
@useState
export default class Component extends HTMLElement { /* ... */}

さて、先程例示した、Component.jsは最終的に以下のようなコードになった。実際に先程のコードのように書いてしまうとメモリリークを起こしてしまう可能性があるためその点も含め若干変わっている

import dispatcher from './dispatcher';
import action from './action';
import store from './store';
import useState from './useState';

const template = document.createElement('template');
template.innerHTML = `
<span></span>
<button type="button">+</button>
`;

class Component extends HTMLElement {
	constructor() {
		super();

		this.handleStoreChange = this.handleStoreChange.bind(this);

		const content = template.content.cloneNode(true);
		this.span = content.querySelector('span');
		const incrementButton = content.querySelectorAll('button')[0];

		incrementButton.addEventListener('click', (e) => {
			action.countUp();
		});

		this.appendChild(content);
	}

	handleStoreChange() {
		this.setState({ count: store.count });
	}

	connectedCallback() {
		store.on('CHANGE', this.handleStoreChange);
		//初期処理
		this.handleStoreChange();
	}

	disconnectedCallback() {
		store.removeListener('CHANGE', this.handleStoreChange);
	}

	stateChangedCallback(name, oldValue, newValue) {
		switch(name) {
			case 'count':
				this.span.textContent = newValue;
				break;
		}
	}

	static get observedState() {
		return ['count'];
	}
}

customElements.define('x-component', Component);

export default useState(Component)

dispatcherやstoreやactionの実装も載せておく。 (eventsモジュールはNode.jsの標準モジュール。中身はEventEmitter)

import Emitter from 'events';

export default new Emitter();
import dispatcher from './dispatcher';

export default {
	countUp() {
		dispatcher.emit('countUp');
	},
}
import Emitter from 'events';
import dispatcher from './dispatcher';

class Store extends Emitter {
	constructor(dispatcher) {
		super();

		this.count = 0;
		dispatcher.on('countUp', {});
	}

	onCountUp() {
		this.count++;
		this.emit('CHANGE');
	}
}

export default new Store(dispatcher);
このコンポーネントを例えば以下のように描画してやれば、サンプルページ(要chrome)のようになる
import Component from './Component';

const component = new Component();
document.body.appendChild(component);

これで。Fluxの一連の流れをフレームワークを使わなくても実現できそうだ。これは、データーバインディングの方法を例示したに過ぎないからCustomElementsに限らず使える。ただ、今回の肝は、監視対象のattributeが変更されたらattributeChangedCallbackが呼ばれるというCustomElementsのライフサイクルに習って、監視対象のstateが変更されたらstateChangedCallbackを呼ぶというライフサイクルを追加したことにある。だから、CustomElementsとは親和性が高く、理解しやすいだろう。

しかし、上の方法はある種オレオレフレームワークだと思う方がいるかもしれない。もし、この方法が気に入らないのであれば、個別にsetterを用意して状態をまとめてではなく個別に監視することも出来るだろう。Componentが必要とするstateが多くなれば少し冗長な書き方になってしまうが、全く標準な方法で実装ができる。useStateを使ったComponentとの違いを見比べてほしい。

import dispatcher from './dispatcher';
import action from './action';
import store from './store';

const template = document.createElement('template');
template.innerHTML = `
<span></span>
<button type="button">+</button>
`;

class Component extends HTMLElement {
	constructor() {
		super();

		this.handleStoreChange = this.handleStoreChange.bind(this);

		const content = template.content.cloneNode(true);
		this.span = content.querySelector('span');
		const incrementButton = content.querySelectorAll('button')[0];

		incrementButton.addEventListener('click', (e) => {
			action.countUp();
		});

		this.appendChild(content);
	}

	set count(value) {
		if(value === this._count) return; //変更がないなら何もしない
		this._count = value;
		this.span.textContent = value;
	}

	handleStoreChange() {
		this.count = store.count;
	}

	connectedCallback() {
		store.on('CHANGE', this.handleStoreChange);
		//初期処理
		this.handleStoreChange();
	}

	disconnectedCallback() {
		store.removeListener('CHANGE', this.handleStoreChange);
	}
}

customElements.define('x-component', Component);

export default Component

これらの実装は、状態(state)を監視しているので、変更のあった部分だけDOM操作するのを助ける。そして、これは生DOMを操作して開発する時の方法である。だから、今流行りの仮想DOMと比べて面倒くささもあるだろう。例えば、状態(state)の差分を測るのは自分でやらなければならない。それにしても、フレームワークに振り回されず標準に近い方法で開発していくメリットは大きいと思う。ぜひ興味のある方はこの方法を試してほしい。

先程のusestatenpm i -S usestate でインストールできる。

fetchやxhrを使った時にIEが勝手にキャッシュする問題を解決する

fetchやxhrを使って非同期通信をしているとIEでは勝手にキャッシュからデータを取得してサーバーにリクエストしないことがある。

そういう時はIf-Modified-Sinceヘッダーを追加してリクエストするとよい。

fetch('url', {
	defaultHeaders: {
		'If-Modified-Since': 'Thu, 01 Jun 1970 00:00:00 GMT'
	}
});
xhr.setRequestHeader('If-Modified-Since', 'Thu, 01 Jun 1970 00:00:00 GMT');

If-Modified-Sinceは指定した日時より新しいファイルがサーバーに存在する場合のみ取得するという条件つきリクエストで、この例では、キャッシュを実質的に無効にしている。

以下はIf-Modified-Sinceヘッダーの書式だ


If-Modified-Since: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT

IEはよくわからないタイミングでキャッシュを利用するのでこのヘッダーは必須なように思われる。IEの寿命が尽きるまで・・・

【WebComponents】CustomElements(v1)とbabelでトランスパイルしたコードを組み合わせる

CustomElements(v1)では下のコードのようにES6Classを使ってCustomElementsを作れるようになった。

class CustomElement extends HTMLElement {
	constructor() {
		super();
	}
}

しかし、ES5ClassでCustomElements(v1)は作れない。そのため、babelでES6ClassをES5Classに変換してしまうと、エラーが発生する。これは、native-shimというpolyfillを読み込ませることによって解決される。

(逆に、CustomElements(v1)に対応していないブラウザはCustomElementsのpolyfillを読み込ませればエラーは発生しない。)

しかし、このnative-shimはES6で書かれているため、IE11などの古いブラウザでは読み込み時にSyntaxErrorが発生してしまう。なんてこったい。babelを使う時というのは、新しいESの構文を使って書いたコードをIE11などの古いブラウザにも対応させる状況だというのに・・・

ということで、このnative-shimを読み込めるブラウザのみ読み込むという方法を取る。具体的にどうやるのかというと、native-shimのコードを全て文字列にして、try~catche句の中でeval()を使って実行させる。SyntaxErrorをcatchするパターンである。

native-shimを文字列にするにはminifyにかけて1行にしてやって、クォートで囲むのがいいだろう。(テンプレートストリングを使いたくなるが、古いブラウザはテンプレートストリングには対応していない。)

try {
	eval("(()=>{'use strict';if(!window.customElements)return;const NativeHTMLElement=window.HTMLElement;const nativeDefine=window.customElements.define;const nativeGet=window.customElements.get;const tagnameByConstructor=new Map();const constructorByTagname=new Map();let browserConstruction=!1;let userConstruction=!1;window.HTMLElement=function(){if(!browserConstruction){const tagname=tagnameByConstructor.get(this.constructor);const fakeClass=nativeGet.call(window.customElements,tagname);userConstruction=!0;const instance=new(fakeClass)();return instance}browserConstruction=!1};window.HTMLElement.prototype=NativeHTMLElement.prototype;window.customElements.define=(tagname,elementClass)=>{const elementProto=elementClass.prototype;const StandInElement=class extends NativeHTMLElement{constructor(){super();Object.setPrototypeOf(this,elementProto);if(!userConstruction){browserConstruction=!0;elementClass.call(this)}userConstruction=!1}};const standInProto=StandInElement.prototype;StandInElement.observedAttributes=elementClass.observedAttributes;standInProto.connectedCallback=elementProto.connectedCallback;standInProto.disconnectedCallback=elementProto.disconnectedCallback;standInProto.attributeChangedCallback=elementProto.attributeChangedCallback;standInProto.adoptedCallback=elementProto.adoptedCallback;tagnameByConstructor.set(elementClass,tagname);constructorByTagname.set(tagname,elementClass);nativeDefine.call(window.customElements,tagname,StandInElement)};window.customElements.get=(tagname)=>constructorByTagname.get(tagname)})()");
}catch(e) {}

これで、CustomElements(v1)に対応しているブラウザはnative-shimを読み込むので、ES5Classにトランスパイルされたコードが動くようになり、かつnative-shimを読み込めないIE11のようなブラウザも通常のCustomElementsのpolyfillを読み込ませることによってCustomElementsが使えるようになる。

なぜ、CustomElements(v1)がES5Classに対応していないかというのはたぶんCustomElements(v1)を実装しているブラウザはすでにES6Classも実装しているからだろう。

補足: こういうbabelのプラグインもあるらしい。試してないので何とも言えないが。 babel-plugin-transform-custom-element-classes

【Redux】ReactのComponentとの連携について

Reduxを学び始めたばかりの時、ReduxのチュートリアルのReactのComponentと連携しているコードが何をやっているかわからなかったのだが、コードを書いているうちに少しずつわかってきたので、自分の考えの整理のためと同じところで躓いている人のために順序立てて説明していきたい。

まず、Fluxの処理の流れを再確認しておきたい。Fluxの概念図はググれば色々出てくるが、まずAction,Store,Componentで以下のように表してみる。

Action —–> Store —–> Component (—–> Action…)

ここで、Componentに目を向けてみるが、ComponentはStoreからstateを得て、必要に応じてActionを発行する。(実際にActionを発行するのはActionCreaterなのでComponentはそれを呼ぶということになる。)

さて、Actionは{type: 'addTodo'}のようなただのオブジェクトだが、この発行されたActionをどのようにStoreに伝えるのか。それはDispatcherの役割である。つまり、発行したActionをDispatcherに渡さないといけない。実装ではこれもComponentに書く。つまり、ComponentはActionを発行するだけでなく、その発行したActionをDispatcherに渡す処理を書く。

さて、ReduxではそのDispatcherはどこにあるのか。

実はReduxにはDispatcherが無い。その代わりStoreにdispatchというメソッドが用意されている。これはReduxがStoreを単一として設計しているからだろう。そのようなわけで、store.dispatch(action)というような処理をComponent内に書けば、

Component —–> Action —–> Store

と流すことができる。

では、どのようにComponentは単一storeを得るのか。それは親コンポーネント(ルートコンポーネント)からもらうしかない。

チュートリアルを見るとReduxとReactの連携にReactReduxというライブラリが使われている。このライブラリは必須ではない。しかし、このライブラリを使うことにっよて、親から子へstoreを渡すというような先に述べた処理を書かずに済む。一方、初めてReduxとReactの連携しているコードを見た時、何をやっているのか混乱しやすい。

さて、Reduxは単一storeなのでそのstoreはまずルートコンポーネントに渡され、子に渡されていく。チュートリアルのindex.js

render(
	<Provider store={store}>
		<App />
	</Provider>,
	document.getElementById('root')
)

とあるが、これはルートコンポーネントにstoreを渡している。このProviderというのはReactReduxのコンポーネントである。なぜこれで子のコンポーネントがstoreを得られるようになるか。それは、Reactが提供する機能には、propsのようにいちいち子のコンポーネントに明示的に値を渡す必要のないcontextというのがあり、ReactReduxも内部でそれを使っているからだ。そのため<App store={store} />と書く必要がないのである。この機能を知らないと子のコンポーネントがどのようにstoreを得ているのかわかりづらい。

では次に子のコンポーネントがstoreを得る方法、本題のReactとComponentの連携している部分を説明していく。

Reduxと連携するComponentはContainerComponentと呼ばれる。Reduxと連携と言っているが、Componentがstoreを得られるようになることを連携と言っているのである。ただ、ReactReduxを利用した場合、ReactReduxがstoreを得て、そこから必要な機能だけがComponentに渡される。

ReduxとComponentを連携しているコードを見ると、ReactRedux.connect()(Component)のような記述があるが、これを実行することにより、Componentのpropsにdispatchという関数が渡される。このdispatch関数にActionを渡すことによって

Component —–> Action —–> Store

と処理を流すことができるのである。以下、具体的なコードである。

import React from 'react'
import * as ReactRedux from 'react-redux'
import * as actions from '../actions/index.js'

class MyComponent extends React.Component {

	handleClick() {
		//propsにdispatch関数がある
		this.props.dispatch(actions.addTodo);
	}

	render() {
		return (
			<div>
				<button
					onClick={this.handleClick.bind(this)}
				/>
			</div>
		);
	}
}

export default ReactRedux.connect()(MyComponent);

さて、これでComponent —–> Action —–> Storeと処理を流すことができた。では、Componentはstoreからどのようにstateを得るのか。つまりStore —–> Componentについてである。

これはRedux.connectの第一引数に渡す関数で得ることができる。チュートリアルではmapStateToPropsと命名されている関数のことだ。このmapStateToPropsの第一引数にstoreのstateが渡される。mapStateToPropsの返り値はObjectで、このObjectのプロパティがそのままComponentのpropsに渡される。state全部を渡したいなら返り値にstateを指定する。

import React from 'react'
import * as ReactRedux from 'react-redux'
import * as actions from '../actions/index.js'

class MyComponent extends React.Component {
	render() {
		//stateが全て入っている
		console.log(this.props);
		
		return (<div />);
	}
}

const mapStateToProps = (state) => {
	return state;
}

export default ReactRedux.connect(mapStateToProps)(MyComponent);

stateの中の一部を取り出したいなら以下のように返り値を指定する。

const mapStateToProps = (state) => {
	return {
		partOfState: state.partOfState
	};
}

こうしてComponentはstateを得ることができ、dispatchも得ることができたので、

Action —–> Store —–> Component( —–> Action…)

の流れに忠実な実装にできるということが理解できたと思う。

ReduxとReactの連携ではReactReduxが何をやっているか概観を理解することが重要である。QiitaにReactReduxを使ったときと使わなかったときの比較が載っていたので参照してほしい。

ReactReduxの役割を整理

少し冗長な説明になってしまったが、もしわからなくても、実際にコードを書いているうちに理解できる範囲だと思われる。自身もまだ学んでいる途中なのでもし変な部分があれば指摘していただければ嬉しい。

【JavaScript】privateなプロパティやメソッドを定義する

JavaScriptでprivateなプロパティやメソッド(以下メンバ)を定義する方法を書いていく。

ES6未満では、privateなメンバを定義するために色々な工夫が重ねられていた。それでも全くprivateなプロパティを作るのは難しかった。しかし、ES6ではWeakMapというオブジェクトが使えるようになり、完全にprivateなメンバを定義できるようになった。

ここではまず、ES6未満でprivateなメンバを定義するための工夫の数々を紹介し、その後、WeakMapを使ったprivateなメンバを定義する方法を書く。そして、最後にES6でのClass定義の自分の書き方パターンを紹介したい。

ES6未満でのprivateなメンバの定義方法

まず、1つの工夫としてprivateなメンバには先頭に_(アンダーバー)をつけるという方法があるが、これは外部からアクセスできるためprivateでもなんでもない。

もう1つの方法としてメンバを全てconstructor内に記述するという方法もある。

function Foo() {
	var privateProp = 1;
	this.method = function() {
		return privateProp++;
	};
}

これは確かにprivateなメンバを作れるが、PrototypeベースというJavaScriptの利点をなくしてしまう他、インスタンス化する度にメソッドを定義するためメモリも余計に食う。

現実的な方法としては、以下のような書き方がある。

var Foo = (function() {
	var privates = {};
	var privateId = 0;

	function Foo() {
		Object.defineProperty(this, '_id', { value: privateId++ });
		privates[this._id] = {};

		privates[this._id].prop = 1;
	}

	Foo.prototype.method = function() { return privates[this._id].prop };

	return Foo;
})();

この方法はthis._idというプロパティを作ってしまうことさえ許容できれば、かなりgoodな方法である。しかし、WeakMapを使えばこのようなプロパティを作る必要もなくなる。

ES6のWeakMapを使ってprivateなメンバを定義する

まず、WeakMapを知らない方へWeakMapとは何なのか簡単に説明するが、普通のObjectはkeyに文字列を使うのに対して、WeakMapはkeyにオブジェクトを使う。つまり、プリミティブ型でなければObjectでもArrayでもFunctionでも何でもkeyにできる

var a = {};
var b = [];
var c = () => {};

var weakmap = new WeakMap();
weakmap.set(a, 1);
weakmap.set(b, 2);
weakmap.set(c, 3);

console.log(weakmap.get(b)); // -> 2;

このようにオブジェクトをkeyとして使えるため、Classをインスタンス化する際、そのインスタンス(this)をWeakMapにsetすればWeakMap.get(this)でメンバにアクセスできるのだ。百聞は一見にしかずなので例示する。

var Foo = (function() {
	var privates = new WeakMap();

	function Foo() {
		privates.set(this, {});

		privates.get(this).prop = 1;
	}

	Foo.prototype.method = function() { return privates.get(this).prop };

	return Foo;
})();

ES6のClass定義パターン

上のように、WeakMapの中のプロパティにアクセスするためにはgetやsetというメソッドを使う必要があり、それは面倒なので以下のようなnamespaceという関数を作る。

'use strict';
export default function namespace() {
	const map = new WeakMap();

	return function(object) {
		if(! map.has(object)) {
			map.set(object, {});
		}
		return map.get(object);
	};
};

これをClassファイルにimportしてprivateなメンバを定義する方法を以下に例示する。

'use strict';
import ns from './namespace';

const privates = ns();

class Foo {
	constructor() {
		const self = privates(this);
		self.privateProp = 1;
		this.prop = 2;

		self.privateMethod1 = privateMethod1.bind(this);
		self.privateMethod2 = privateMethod2.bind(this);
	}

	method() {
		const self = privates(this);
		return self.privateProp + self.privateMethod1();
	}
}

function privateMethod1() {
	const self = privates(this);
	return self.privateProp + this.prop;
}
function privateMethod2() {
	return this.method();
}

ここでは、privates(this)で返ってくるオブジェクトにprivateなメンバをセットする。取得するときもprivates(this)から取得できる。privates(this)をいちいち呼び出すより、selfなどの変数に入れておくほうがよい。

また、privateなメソッドはclassの外でfunction文で宣言して(ホイスティング)、constructor内でprivates(this)にthisをbindして代入したほうがネストも浅くて済む。

privateなメンバのアクセスをどこまで許すかは即時関数(ブロック)で囲うか囲わないかによる。すぐ上の例のようにファイル全体まで許すこともできる。

また、browserifyなどを使って複数のファイルを1つのファイルにあらかじめコンパイルしているのであれば、それらのファイル内のみメンバのアクセスを許可するといったような名前空間を作ることも可能である。

import ns from ./namespace;
export default ns();
import internal from './internal'
export default class Foo {
	conctructor() {
		internal(this).prop = 1;
	}
}
import internal from './internal'
import Foo from ./Foo
const foo = new Foo();
console.log(internal(foo).prop); // -> 1;

1つの方法として覚えておくと使える時がくるかもしれない。

終わりに

今回は、JavaScriptでprivateなメンバを定義する方法を書いてきた。特に、WeakMapを使った方法はかなり有用性があり、途中で示したnamespace.jsと合わせて使うとJavaScriptでもprivateなメンバを簡単に書けるようになる。今後、デフォでJavaScriptでもprivateなメンバを定義できるようになるかもしれないが、それまでは、このWeakMapを使った方法で定義することを推奨したい。