LoginSignup
16
15

More than 5 years have passed since last update.

React/FluxフレームワークAltのサンプルアプリを読んでみた所感

Last updated at Posted at 2015-09-03

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

スクリーンショット 2015-08-26 19.38.51.png

検索するとアニメーションつきで表示されます。

スクリーンショット 2015-08-26 19.39.01.png

処理を順番に見ていく

ということで、早速コードを見ていきます。

まずはmain.js

app/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が怪しい。

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について

rackt/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を行うかの定義をしているくらいだということがわかりました。

app/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);
  })
})

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の構成を見ます。

app.jsx
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が描画されるはずなのでそちらを。

javascript|repos.jsx

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()はなにをするかというと

repos.jsx
〜〜〜〜〜
  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すると呼ばれています。

repos.jsx
<form onSubmit={this.search}>

でイベントをキャッチし、

repos.jsx
  search(evt) {
    evt.preventDefault()
    var searchText = this.refs.searchText.getDOMNode().value
    ReposActions.search(searchText)
  }

で、ReposActions.searchを読んでみます。

ReposActions.js
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")

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に登録していることがわかります。

src/alt/index.js
/*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が二回叩かれていましたが、どうなるかというと

repos-store.js
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の考えのもとで実装を進める利点ですね。

repos.jsx
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バージョンなどを試してみたいと思います。

16
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
15