Vue.jsにreduxを載せた話
この記事はVue.js Advent Calendar 2015の第三日目の記事です。
概要
先日とある案件でVue.js
を用いたアプリケーションを開発することとなりました。
一般に同種のフレームワークと比較してVue.js
は、学習コストが低く気軽にはじめやすいということがメリットとして語られています。
ただ、今回の案件のようにMVC
系フレームワークを用いて開発する際にネックとなっていたのが、コンポーネント間のデータ共有についての最適解が見つけられていないと感じていたことでした。
もちろん、適切にモデルを設計すればコンポーネント間のやり取りに責任を持つ、親コンポーネントとでもいうべき層を実装することでこの問題は解決できると思います。
しかし実案件では往々にして親コンポーネント層の実装が個々人の裁量に陥りやすく、結果的にコンポーネント間データ共有の方式が統一出来ていないケースも出てきてしまいました。
コンポーネント間で共有したいデータ=アプリケーションの本質となるデータは、もっとはっきりとコンポーネントと区別して設計した方が、結果的にはシンプルな構造を保ち続け易いのではないか?と考えたのです。
React.js
では、Flux
を踏襲した設計にすることでモデルは必然的にコンポーネントから分離され、別のレイヤーとして実装されます。
同じことをVue.js
にも適用できれば、モデルから分離されたコンポーネントによる、シンプルな構造を保ち続けやすいアプリケーションに出来そうです。
Vue.js
の公式ドキュメントでも大規模アプリケーションを構築する際のプラクティスとして、Flux
による設計に行き着き得ることが書かれています。
If we enforce a convention where components are never allowed to directly mutate state that belongs to a store, but should instead dispatch events that notify the store to perform actions, we’ve essentially arrived at the Flux architecture.
コンポーネントが(コンポーネント間で共有したいような)store
に属する状態
を直接変更せず、その代わりstore
へ発生したaction
を通知するだけに留めようとするなら、Flux
アーキテクチャの概念に辿り着いたと言えます、みたいに解釈しました。
The Flux architecture is commonly used in React applications. Turns out the core idea behind Flux can be quite simply achieved in Vue.js, thanks to the unobtrusive reactivity system.
一般にFlux
アーキテクチャはReact
によるアプリケーションで使用されています(が、)控えめなリアクティビティ機構のおかげで、とても簡単にFlux
の中心的なアイディアをVue.js
に適用できることがわかります、みたいに解釈しました。
Building Large-Scale Apps/State Management
邦訳版
公式ドキュメントにあるように、Vue.js
とFlux
系ライブラリは、互いに対して疎結合な設計を保っているので、非常に簡単に適用することが出来ました。
本稿ではFlux
のより洗練された形であるRedux
ライブラリを採用し、Vue.js
コンポーネントとの連携について書きたいと思います。
実装
およそ以下のような手順で実装しました。
React.js
のアプリケーションと同様にconstant
,actionCreator
,Reducer
を作成します- エントリーポイントとなる
Root VueModel
のdata
にstore
を作成します 1
の子コンポーネントにprops
としてstore
を渡します- 子コンポーネントは必要に応じて、自身のライフサイクルの適したタイミングで
store
を購読します - 子コンポーネントは(主にユーザーの)入力に応じて、適宜
action
を発行します
以下に簡単なコード例を載せつつ実装手順を説明します。
1.React.js
のアプリケーションと同様にconstant
,actionCreator
,Reducer
を作成します
// constant.js
const CHANGE_STATE = 'CHANGE_STATE';
// actionCreator.js
export function changeMyState(nextState) {
return {
type: CHANGE_STATE,
nextState: nextState,
};
}
// reducer.js
const initialState = {
currentState: '最初の状態',
};
function myState(state = initialState, action) {
switch (action.type) {
case CHANGE_STATE:
return {
currentState: state.nextState,
};
default:
return state;
}
}
const reducers = combineReducers({
myState: myState,
});
export default reducers;
2.エントリーポイントとなるRoot VueModel
のdata
にstore
を作成します
import { createStore } from 'redux';
import reducers from './reducers';
new Vue({
el: '#root',
data: {
stores: createStore(reducers),
},
components: {
'my-component': myComponent,
},
});
3.1
の子コンポーネントにprops
としてstore
を渡します
<!--index.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
<div id="root">
<my-component :store="store" ></my-component>
</div>
<script src="./bundle.js"></script>
</body>
</html>
React.js
のように、細かくコンポーネントを作りこんでいくような設計だと、子々孫々までstore
を適用するのが非常に面倒になるのでreact-redux
の提供するconnect
のような仕組みが必要になってきますが、Vue.js
では機能ごとくらいの粒度でコンポーネントを作成するのでそのような問題が発生しませんでした。
そのため親から子へ直接store
を手渡ししていますが、もっとコンポーネントの階層が深くなるようであれば、react-redux/connect
に類する機構が必要になってくるかも知れません。
4.子コンポーネントは、必要に応じて自身のライフサイクルの適したタイミングでstore
を購読します
// my-component.js
import Vue from 'vue';
import template from './template';
export default Vue.extend({
data() {
return {
currentState: this.store.getState().myState.currentState,
};
},
props: ['store'],
created() {
this.store.subscribe(()=> {
this.currentState = this.store.getState().myState.currentState;
});
},
template: `
<div>
{{currentState}}
<button @click="onClickAndDispatch">状態を更新する</button>
</div>
`,
});
この例ではcreated
のタイミングでstore
の購読を開始しています。
サーバーサイドでレンダリングしたHTMLにVue.js
アプリケーションを適用している場合に、jQuery
なんかのDOM
を直接触るような必要が
あれば、DOM
生成後となるready
のタイミングに購読を開始してもいいかも知れません。
5.子コンポーネントは(主にユーザーの)入力に応じて、適宜action
を発行します。
export default Vue.extend({
... // my-component.jsの続き
methods: {
onClickAndDispatch() {
const nextState = '新しい状態';
this.store.dispatch(changeMyState(nextState));
},
}
});
上述の通り、store
の伝播方法と購読のタイミング以外はほとんどReact.js
の時と同じように使う事ができます。
Redux
のミドルウェアも特別に工夫することなく使えますので、必要に応じて拡張できて便利です。
使ってみての雑感
良い点
モデルが半強制的にコンポーネントから分離される
概要に書いたようにモデルが半強制的に
コンポーネントから追い出されるため、設計方針を共有しやすくなりました。
どちらかと言うと設計からズレた実装を作る方が面倒なので、設計方針を担保し易いところが良いと思います。
また、コンポーネントの責任を描画内容に集中させやすくなりました。
テストしやすい
actionCreators
レイヤーが非同期処理を含めた最新の状態の取得
にまつわる処理を単なる関数として実装しているおかげで、とてもテストがし易い設計になりました。
一部のコードはテストファーストで書くことが出来たので、もうライオンも怖くありません。
Reduxの良さをそのまま活かせる
ミドルウェアの仕組みがそのまま使えるので、開発効率の向上用ツールをReact.js
のプロジェクトから流用できました。
悪い点
Redux(Flux)自体の学習コスト
もともとVue.js
を採用した背景に、JavaScript
専業のエンジニアとの協業のための学習コストの低さ
がありましたが、Flux
を導入したことで確実に学習コストが底上げされてしまいました。
Flux
を構成しているデザイン自体は特段珍しいものではないと思いますが、クライアントサイドアプリケーションがFlux
にたどり着いた経緯や、メリットを感じるための背景を事前に簡単に説明しておいた方がスムーズに行くかも知れません。
積極的にVue.jsを使うメリットは
これReact
+Redux
じゃダメなのか…? みたいな心の声が常に聞こえました。
ただし、Vue.js
は他のフレームワークとの差別ポイントに協業のし易さ
を挙げているので、JavaScript
専業のエンジニアが何名も確保できるのであったり、デザイナーがJSX
構文を問題なく使いこなすような環境でなければ、Vue.js
を採用するメリットはあると思います。
コード量が増える
アプリケーションの状態があるべき姿を1つずつ宣言していく手前、モデルをコンポーネントに記述する場合に比べてコード量は確実に増えます。
共通して使う宣言はクラスにまとめるなど、なるべく単純なタイプ数が増えすぎないように気を使う必要があるかも知れません。
まとめ
思いの外すんなり導入できて驚きました。
Redux
による見通しの良い設計は気に入っているので、機会があれば他のフレームワークにも組み込んでみたいと思っています(Angular
とか)
2015.12.5更新
このポストで触れたような実装を、ライブラリとして切り出しているレポジトリを見つけました。
(このポストと前後する時期に開発が始まったようです)
やっていることはほとんど同じですが、こちらのライブラリを使ったほうが楽そうです。
Vue.jsやReact、Fluxを採用したモダンなフロントエンドアプリケーション開発をご検討の企業様は、是非MMMにご相談下さいませ!