アプリケーションエンジニアのyutakaです。
先日ランサーズのメッセージ機能をリニューアルしました。
ランサーズのメッセージ機能はリアルタイムのチャット形式で他のユーザーさんとコミュニケーションがとれる機能となっています。
リニューアルに伴って、フロントエンドの技術も大幅な変更を行い、ES6 + React.js + Reduxを導入しています。
今回はランサーズで新しく導入した技術と背景についてお話させて頂きます。
背景
以前からチャット機能のクライアントサイドプログラムには以下の問題がありました。
- 細かくモジュール化されていない
- どこでどんなイベント(処理)が発火しているのか分からないので処理が追いづらい
- 一つのファイルに3つの機能が存在しており、密結合な作りになっている
- HTMLの僅かな変更でもロジックに影響が出てしまう
リニューアルの内容を考えると既存コードの改修は難しく、新たに一から書き直す必要がありました。
また、上記問題はランサーズのフロントエンド共通の問題でもあったので、これを期に問題を解消する仕組みを作り
先行して、チャット機能に導入することとなりました。
問題を解決するためには
- module化
- 処理フローの明確化
- 疎結合な作り
- DOMとロジックの分離
上記のような世界を実現する必要があり、それを実現するための手段として
ES6 + React.js + Reduxを導入するに至りました。
今回取り入れた技術
ES6
ES6はECMAScriptの次世代仕様です。
class,Map,Symbol,export/import等新たな構文やデータ型をサポートしてくれるので
より構造化されたプログラミングが可能となります。
ES6はブラウザによって対応状況が異なるので、ES6で書いたコードをそのままサービスに適用するのは難しいです。
ランサーズではBrowserify+babelifyを使用してES6のコードをES5へトランスパイルしています。
さらに、Gulpと組み合わせることでファイルの変更を検知し自動でトランスパイルされるようにしています。
オンライン上で試せるツールもあるので、カジュアルに動作検証が出来る環境も整っています。
Redux(Flux)
ReduxはFlux実装を取り入れたフレームワークです。
ここで少しだけFluxの説明をすると、FluxはFacebookが提唱したフロントエンドのアーキテクチャーです。
FluxはObserverPatternの亜種のようなアーキテクチャーで
- データフローが単一方向
- 各要素がイベントの発火と監視を行っているので非常に疎結合な作りにしやすい
- テスタブルかつメンテナブルである
といった特徴が決め手となり採用しました。
Fluxの詳しい説明はこちらをご参照ください。
ここからようやくReduxの説明です。
Reduxはアプリケーションの状態管理に特化したフレームワークで3つの原則に則って状態の流れを制限することで状態を管理しています。
Three Principles
- Single source of truth(ソースは1つだけ)
- State is read-only(状態は読み取り専用)
- Mutations are written as pure functions(変更はすべてpureな関数で書かれる
Reduxは
- Actions
- Reducers
- Store
の3つの要素で構成されています。
各要素の役割について簡単に説明していきます。
Actions
import ActionTypes from '/path/to/ActionTypes'; { type: ActionTypes.SAMPLE, foo: 'bar' }
アプリケーションのからの情報をstoreへ送るためのオブジェクトです。
識別子として必ずtypeプロパティを持ちます。それ以外のプロパティに関しては自由です。
ActionCreator
import ActionTypes from '/path/to/ActionTypes'; export function sampleActionCreator() { return { type: ActionTypes.SAMPLE, foo: 'bar' } };
その名の通りActionを生成する役割を持っています。
本家Fluxとは違いDispatchは行いません。
Reducers
import ActionTypes from '/path/to/ActionTypes'; import handleActions from 'redux-actions'; import assign from 'lodash/object/assign'; var initialState = { foo: '' }; const sampleState = handleActions({ [ActionTypes.SAMPLE]: (state, action) => { return assign({}, state, { foo: action.foo }); } }, initialState); export default sampleState;
現在の状態とactionを受け取って新たな状態を返す関数です。
Reducerは副作用のないピュアな関数であることが求められますので、直接stateを変更するのはタブーです。
新しいオブジェクトを生成して返すことになります。
ランサーズではstate更新時にlodashを利用しています。
Store
Storeはstateの保持・アクセス・stateの更新・リスナーの更新を行うオブジェクトです。
ReduxではStoreはアプリケーションに1つだけです。
データ処理をロジックを分割したい場合はreducer compositionを使ってReducerを分割します。
LancersでもReducerを分割して使用しています。
React.js
皆さんご存知のFaceBookが開発したUIライブラリです。
チャット機能のview部分を担当しています。
react-reduxを使用することで、reduxのstateをcomponentのpropsとして受け取れるようになります。
import React, { Component } from 'react'; import { Provider, connect } from 'react-redux'; import configureStore from 'path/to/configure_store'; import { bindActionCreators } from 'redux'; import * as SampleActions from 'path/to/actions'; const store = configureStore(); class SampleComponent extends Component { render() { const { foo } = this.props; return ( <div> <Provider store={store}> { foo } </Provider> </div> ) } } var mapDispatchToProps = (dispatch) => { return bindActionCreators(SampleActions, dispatch); } var mapStateToProps = (state) => { return { foo: state.sampleState.foo }; }; export default connect(mapStateToProps, mapDispatchToProps)(SampleComponent);
Reactを採用したことにより、コンポーネント指向によるアプリケーションの設計を行えるようになったは大きな収穫でした。
画面のパーツ毎にHTMLとjavascriptで構成されたコンポーネントを実装し、それを組み合わせて一つの画面を構築するようになりました。
コンポーネントは再利用されることを前提で設計していたので、同じコンポーネントを複数画面で使いまわせるようになりました。
ユニットテスト
ランサーズではjavascriptのユニットテストが存在していなかったので、導入しました。
テスト周りの構成は
- karma(テストランナー)
- mocha(テストフレームワーク)
- power-assert(アサーションライブラリ)
となっています。
power-assertはアサーションに失敗した際に出力される情報が、分かりやすいので非常に気に入っています。
今後の展望
今回導入した技術はまだ一部のメンバーしか触れていません。
ES6はimport/export機能が実装されたことにより、細かくモジュール化することが可能になったのが大きいので
ES6でフロントエンドのプログラミングを行えるよう、これから開発メンバーへの布教活動をしていきます。
- ユニットテストを自動化
- cssとの連携
- 開発メンバーへの布教活動