React.jsの基本もFluxの概念もわかったけど、Fluxはフレームワーク乱立していてわからない...といった状況なんですが
We Compared 13 Top Flux Implementations — Medium
こういった比較記事を見ると、
- helperが充実してる
- ES5/ES6対応
- React以外のライブラリと共存できる
- コミュニティーが活発
などの理由からAltをオススメしていました。確かにstar数も1500を越えていてcommitも活発なので、試してみる価値あり、ということでまずはAltやそのサンプルコードを読んで実際の使い勝手を見ておいこうと思います。
今回読むサンプルはこちら:
RookieOne/react-alt-github-example
ローカルでサンプルが動くようにする
まずはgit clone.
git clone git@github.com:RookieOne/react-alt-github-example.git
ディレクトリを移動し、packageをインストール
npm install
次にbowerでライブラリたちを入れますが、bowerが入ってなかったので、
npm install bower -g
でbowerをゲット。
bowerからassetsをinstall。
bower install
glupもなかったのでinstall
npm install -g gulp
して実行。
$ gulp
[19:36:37] Using gulpfile ~/work/react/react-alt-github-example/gulpFile.js
[19:36:37] Starting 'move-webpages'...
[19:36:37] Starting 'bower-css'...
[19:36:37] Starting 'bower-js'...
[19:36:37] Starting 'fonts'...
[19:36:37] Starting 'fonts-awesome'...
[19:36:37] Starting 'css'...
[19:36:37] Starting 'images'...
[19:36:37] Starting 'react'...
[19:36:37] Finished 'images' after 36 ms
[19:36:37] Finished 'move-webpages' after 94 ms
[19:36:37] Finished 'css' after 95 ms
[19:36:37] Finished 'bower-js' after 197 ms
[19:36:37] Finished 'bower-css' after 208 ms
[19:36:38] Finished 'fonts-awesome' after 1.11 s
[19:36:39] Finished 'fonts' after 1.55 s
[19:36:41] Finished 'react' after 3.56 s
[19:36:41] Starting 'build-app'...
[19:36:41] Finished 'build-app' after 13 μs
[19:36:41] Starting 'webserver'...
[19:36:41] Webserver started at http://localhost:8000
[19:36:41] Finished 'webserver' after 15 ms
[19:36:41] Starting 'watch'...
[19:36:41] Finished 'watch' after 11 ms
[19:36:41] Starting 'default'...
[19:36:41] Finished 'default' after 13 μs
....
(gulpってなんだよって人はこちらから 【Gulp.js入門】新鋭フロンエンド・タスクランナーツール を試してみました。 | Developers.IO )
としたらもう見れますので、http://localhost:8000
で見れますので早速アクセス。
open http://localhost:8000
検索するとアニメーションつきで表示されます。
処理を順番に見ていく
ということで、早速コードを見ていきます。
まずはmain.js
var React = require("react")
var $ = require("jquery")
var Router = require("react-router")
var routes = require("./routes.jsx")
$(function() {
Router.run(routes, function(Handler) {
React.render(<Handler />, document.body);
})
})
Handerというコンポーネント?をbodyに対してmountして描画。
いきなりHandlerなんてコンポーネントないぞ?となりますが、app/routes.jsx
が怪しい。
var React = require("react")
var Router = require("react-router")
var { Route } = Router
var App = require("./views/app.jsx")
var Repos = require("./views/repos.jsx")
var RepoDetails = require("./views/repo-details.jsx")
module.exports = (
<Route handler={App}>
<Route name="repos" handler={Repos} path="/" />
<Route name="repo-details" handler={RepoDetails} path="/repo/:owner/:name" />
</Route>
)
その前に
var Router = require("react-router")
これをしっかり理解する。
寄り道: react-routerについて
var routes = (
<Route handler={App} path="/">
<DefaultRoute handler={Home} />
<Route name="about" handler={About} />
<Route name="users" handler={Users}>
<Route name="recent-users" path="recent" handler={RecentUsers} />
<Route name="user" path="/user/:userId" handler={User} />
<NotFoundRoute handler={UserRouteNotFound}/>
</Route>
<NotFoundRoute handler={NotFound}/>
<Redirect from="company" to="about" />
</Route>
);
Router.run(routes, function (Handler) {
React.render(<Handler/>, document.body);
});
最後の産業がrouterのinitializeのようなものみたい。
react-router/overview.md at 0.13.x · rackt/react-routerをみると、
- templateを提供してくれる。header/footerなどを何度も宣言しなくてよい。
- 呼び出し側もスマートに書ける
ってメリットがある模様。たしかにroutesで設定しておけば、
<li><Link to="app">Dashboard</Link></li>
とするだけで http://host/appのようにroutingで指定したページを描画してくれる。
main.jsに戻ると、react-routerをつかって、どのtemplateファイル(railsでいうならlayoutsファイル)を元にroutingを行うかの定義をしているくらいだということがわかりました。
var React = require("react")
var $ = require("jquery")
var Router = require("react-router")
var routes = require("./routes.jsx")
$(function() {
Router.run(routes, function(Handler) {
React.render(<Handler />, document.body);
})
})
routes.jsxを見ると
var React = require("react")
var Router = require("react-router")
var { Route } = Router
var App = require("./views/app.jsx")
var Repos = require("./views/repos.jsx")
var RepoDetails = require("./views/repo-details.jsx")
module.exports = (
<Route handler={App}>
<Route name="repos" handler={Repos} path="/" />
<Route name="repo-details" handler={RepoDetails} path="/repo/:owner/:name" />
</Route>
)
とあり、appをtemplateファイルとして、reposとrepos-detailsの2つが登録されている。
ということで次はapp.jsxを見る。まずはviewの構成を見ます。
var React = require("react")
var RouteHandler = require('react-router').RouteHandler
var Header = require("./header.jsx")
module.exports = React.createClass({
render() {
return (
<div>
<Header />
<div className="container">
<RouteHandler />
</div>
</div>
)
}
})
Headerコンポーネントが共通であるだけで、あとはroutingされたviewが描画される模様。最初はpath="/"のreposが描画されるはずなのでそちらを。
var React = require("react/addons")
var superagent = require("superagent")
var RepoCard = require("./repo-card.jsx")
var ReposStore = require("../stores/repos-store.js")
var ReposActions = require("../actions/repos-actions.js")
module.exports = React.createClass({
mixins: [React.addons.LinkedStateMixin],
getInitialState() {
return ReposStore.getState()
},
〜〜〜〜〜〜
render() {
return (
<div>
<form onSubmit={this.search}>
<div className="input-field">
<label>Search GitHub Repos</label>
<input type="text" ref="searchText" />
</div>
</form>
{ this.renderLoading() }
<div className="row">
{ this.renderRepos() }
</div>
</div>
)
}
})
viewはシンプルで、検索フォームとその結果がrenderRepos()で描画されるであろうdivがあります。
this.renderRepos()はなにをするかというと
〜〜〜〜〜
renderRepos() {
return this.state.repos.map((repo) => {
return (
<RepoCard repo={repo} />
)
})
},
〜〜〜〜〜
というふうに、RepoCardコンポーネントをreposの数だけ描画しています。mapはjsxでloopさせるメソッドで、アロー関数で書かれた処理を回数分やってくれる。RepoCard内部は省略するがjsxで素直にrepositoryの名前と詳細へのリンク <Link to="repo-details ...>
が書かれています。
Action/Dispather/Stores/Componentを見ていく
AltのAction
Actionは検索ボックスを入力してsubmitすると呼ばれています。
<form onSubmit={this.search}>
でイベントをキャッチし、
search(evt) {
evt.preventDefault()
var searchText = this.refs.searchText.getDOMNode().value
ReposActions.search(searchText)
}
で、ReposActions.searchを読んでみます。
var alt = require("../alt-application.js")
var superagent = require("superagent")
class ReposActions {
search(searchText) {
superagent.get("https://api.github.com/search/repositories")
.query({ q: searchText })
.send()
.end((response) => {
this.actions.searchSuccess(response.body.items)
})
this.dispatch() #dispatchをcall
}
searchSuccess(repos) {
this.dispatch(repos) #dispatchをcall
}
fetchRepo(owner, repoName) {
superagent.get(`https://api.github.com/repos/${owner}/${repoName}`)
.send()
.end((response) => {
this.actions.repoFetched(response.body)
})
}
repoFetched(repo) {
this.dispatch(repo)
}
}
module.exports = alt.createActions(ReposActions)
いきなり1,2行目から見なれないファイルを読み込むが
var alt = require("../alt-application.js")
は
var Alt = require("alt")
var alt = new Alt()
module.exports = alt
altを初期化してるだけ。
次に
var superagent = require("superagent")
ってなんじゃとなるが(無知でごめんなさい)、
visionmedia/superagent
JavaScript - jQuery.ajaxの代わりにSuperAgentを使う - Qiita
SuperAgent is a small progressive client-side HTTP request library, and Node.js module with the same API, sporting many high-level HTTP client features. View the docs.
とあるようにhttpクライアント。制約があってjqeuryつかえない、もしくはjqueryキライでreactせっかくつかってるのにjqueryやだなー$.getかーって人はいいですね。
で、componentのsubmitイベント時によばれたsearchですがsuperagentをつかって、githubAPIをたていて、その処理が返ってきたタイミングでthis.actions.seachSuccess
に返ってきたデータを渡してコールし、メソッド内でthis.dispatch(repos)
とdispatchにデータを渡しています。さりげなくこちらのcallbackだけじゃなく、searchメソッドの最後でもthis.dispatch()
とdispatchが呼ばれます。
「あれ、alt絡んでなくない?dispatchってどこから?」と思ったが最後に alt.createActions(ReposActions)
とあり、そこでactionとしての機能が付与されてるんですね。
これでFluxでいうActionは終了。つぎにDispatch。
AltのDispatcher
Dispatcher自体を自分で書くことはありません。actionで呼ぶくらいです。
ソースを見ればわかりますが、オブションでdispatcherを自分で指定しないかぎり、facebookのシンプルなdispatcherをそのままalt.dispatcherに登録していることがわかります。
/*global window*/
import { Dispatcher } from 'flux'
import * as StateFunctions from './utils/StateFunctions'
import * as fn from '../utils/functions'
import * as store from './store'
import * as utils from './utils/AltUtils'
import makeAction from './actions'
class Alt {
constructor(config = {}) {
this.config = config
this.serialize = config.serialize || JSON.stringify
this.deserialize = config.deserialize || JSON.parse
this.dispatcher = config.dispatcher || new Dispatcher()
this.batchingFunction = config.batchingFunction || (callback => callback())
this.actions = { global: {} }
this.stores = {}
this.storeTransforms = config.storeTransforms || []
this.trapAsync = false
this._actionsRegistry = {}
this._initSnapshot = {}
this._lastSnapshot = {}
}
dispatch(action, data, details) {
this.batchingFunction(() => {
const id = Math.random().toString(18).substr(2, 16)
return this.dispatcher.dispatch({
id,
action,
data,
details
})
})
}
参照: alt/index.js goatslacker/alt
全てのeventにcallbackを付けることも可能で、そのときは
alt.dispatcher.register(console.log.bind(console))
や
alt.createStore(class MyStore {
constructor() {
this.dispatcher.register(console.log.bind(console))
}
})
とするとよいとのこと。
AltのStore
検索の際にdispatcherが二回叩かれていましたが、どうなるかというと
var alt = require("../alt-application.js")
var ReposActions = require("../actions/repos-actions.js")
class ReposStore {
constructor() {
this.repos = []
this.loading = false
this.bindAction(ReposActions.search, this.onSearch) #listen
this.bindAction(ReposActions.searchSuccess, this.onSearchSuccess) #listen
}
onSearch() {
this.loading = true
}
onSearchSuccess(repos) {
this.loading = false
this.repos = repos
}
}
module.exports = alt.createStore(ReposStore)
上記のようにそれぞれのeventに対してcallbackが宣言されています。なので、searchがはしったときはthis.onSearch
が叩かれ、this.loading = false
にまず状態が変わり、すぐにsearchSuccessのcallbackのthis.onSuccessSearch
でloadingがoffになり、APIでとってきたデータがreposに格納されます。
AltのComponent
最後に再びComponent.
もうここまでくれば簡単で、storeが更新されたわけですから、その変化を監視しているComponentがいます。今回のルートのcomponent(view controllerのようなもの)はreposですから、そこでstoreをlistenしているはずです。こう先が見通せることがFluxの考えのもとで実装を進める利点ですね。
var React = require("react/addons")
var superagent = require("superagent")
var RepoCard = require("./repo-card.jsx")
var ReposStore = require("../stores/repos-store.js")
var ReposActions = require("../actions/repos-actions.js")
module.exports = React.createClass({
mixins: [React.addons.LinkedStateMixin],
getInitialState() {
return ReposStore.getState()
},
componentWillMount() {
ReposStore.listen(this.onChange) #listen
},
componentWillUnmount() {
ReposStore.unlisten(this.onChange)
},
onChange() {
this.setState(ReposStore.getState())
},
〜〜〜〜〜〜
ReposStore.listen(this.onChange)のところで、なにか変化があればcallbackを実行するよう登録されています。
getStateでかえってくるのはstoreのプロパティですので、{loading: false, repos: []}が得られ、stateに登録されます。そしてsetStateされたのでお決まりのviewのレンダリング...と続くところでちょうとfluxのサイクルが一周します。
所感
正直まだいろいろ比較しきれてないのでなんともいえない、が結論なんですが、ちょっと見たreduxなどよりはずっと薄く、Fluxの基本概念通りといった感じで読みやすく、環境さえ整えばすぐに取り入れることは出来そうです。
Actionからdispatcher経由でstoreへ、というときにイベントのKEYを管理せずに済むのは楽.他も比較してみるといろいろわかりそうなのであとで追記します。
次は他のライブラリと比較する、railsと組み合わせてどう使うか、ES6バージョンなどを試してみたいと思います。