45
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ReactのTutorialをArdaで写経してみる

Last updated at Posted at 2015-03-05

※ 3/20 arda 0.13対応

Ardaって

Arda

Concept
Today's Flux is weak at scene transitions. Arda make it simple by router and context (chunk of flux).

Context has Flux features and its stack is very simple.
•Dispatcher is just EventEmitter
•View is just React.Component (with mixin)
•Store should be covered by typesafe steps with promise

特徴が挙げられていますが、ざっくり言うとFacebookの提唱するFluxアーキテクチャーのフレームワークの1つ。Reactはこう使うのが良くない?という意図が込められていて、Reactでそのまま組むよりも楽になる。コードが短くなるとかじゃないけど、やりたいことが素直な形でできるすっきり感がある、と思う。

その前にFluxがよく分からないのだが

flux-diagram
よく見かける図ですが、ぱっと見せるにはごちゃごちゃして正直イメージが伝わりにくい。

React Tips and Best Practices
個人的には、こちらの記事中にあった図の方がどんなのもか理解しやすいと思う。
data_flow
Central Storeで状態を一元管理し、イベントの通知があったら状態を変更して描画に必要なデータを全コンポーネントにぶん投げる。実にすっきりしたアーキテクチャーだ。

もちろん、開発は楽になるけど、すぐにDOMの描画がヤバイんじゃね?とか疑問が湧くと思う。それを仮想DOMがあるから大丈夫。ビバ仮想DOM!と、力でねじ伏せてるっっぽい?

まぁ、仮想DOMでパフォーマンスを維持できなくなり、すっきりした設計を崩したチューニングして段々怪しくなるってのは、確実に来る未来だけど、どんな技術もそんなもの。

ただ、仮想DOMなんて高速化のための技術っぽいものを、設計をスッキリさせるための道具として使うってのは、個人的には好ましい。Appleが高解像度を情報量の多さではなく、画質を高める方向に使ったRetinaとかと似たようなものを感じたりも。

で、話を戻して、このアーキテクチャーで開発するのにArdaを使うと気持ちよくコードが書ける(少なくともReactそのままよりは)と期待して勉強してみた、という流れ。

読書百遍意い自ら通ず

習うより慣れろ。手を動かせ。ということで、Reactのチュートリアルをやってみよう。自分でサンプルを考えるのも面倒だし

環境構築

チュートリアルを進めてみると後半、HTTP GET/POSTでJSONデータをやり取りする部分がサラッと出てくる。サンプルコードをチマチマ打ち込んで必死に理解しようとしている最中に環境構築なんて別なことさせられるとイライラしません? なので、先に用意しておく。

HTTP-Serverは、JSON使うんだからJSON Serverかなっと。

> npm install -g json-server

引数にJSONファイルを指定して起動すると、JSONファイルを簡易DB的に扱ってGETやPOSTができるという優れもの。

comments.json

{
    "comments":[
        {"author": "Pete Hunt", "text": "This is one comment"},
        {"author": "Jordan Walke", "text": "This is *another* comment"}
    ]
}

こんなファイルを用意して、

> start json-server -p8080 comments.json

とするとHTTPサーバーが立ち上がるので、ブラウザから

を開くとデータが取得されるはず。

もちろん、JSONを返すだけでなく、普通にHTTP-Serverとして使えるので、publicフォルダを作ってindex.htmlを置けば、

で、ブラウズできる。

これで、HTTPサーバーの準備は完了。

完成形

ゴールが見えていたほうがやり易いと思うので。

  • [app]
  • [src]
    • App.coffee
    • CommentBox.coffee
    • CommentForm.coffee
    • CommentList.coffee
    • Comment.coffee
  • [public]
    • index.html
    • bundle.js
  • comments.json
  • package.json
  • gulpfile.js

srcにあるファイルが.coffeeになっている…ね。ArdaのサンプルがCoffeeScriptだったので頑張って勉強したけどまだ怪しい。ってかJavaScriptもまだまだなんだけどね。~~JavaScriptのコードも記事に入れようと思ったんだけど、分かりにくくなったのでやめ。~~JavaScriptのコードを確認したい場合は、ES6版(Babel)のコードを参照ください。

チュートリアル開始

完成形にあるファイルを一つずつ作っていこう。

まず、package.json

> npm init
(続く質問にはエンターキー連打)

comments.jsonは、JSON-Serverを用意したときに作ったファイルを持ってくる。

gulpfile.jsがあるということは、タスクランナーにgulpを使うということなので、まずはgulpをインストールしよう。(gulpって何?って人もいると思うけど、makeみたいなのと思ってくれればOK)

> npm install -g gulp
> gulp -v
(バージョンが表示されればOK)

んで、gulpfile.js

var gulp = require('gulp');
var browserify = require('browserify');
var source = require('vinyl-source-stream');

gulp.task('build', function() { 
    return browserify({ 
        entries:['./src/App.coffee'],
        extensions:['.coffee']
    })
    .transform('coffeeify')
    .bundle() 
    .pipe(source('bundle.js')) 
    .pipe(gulp.dest('./public'));
});

gulp.task('default', ['build']);

簡単な説明。gulp.taskが二か所あり、'build'と'default'となってるけど、これがタスク名。

gulp build

とか、引数にタスク名を指定するとそのタスクが実行される。'default'は特別なタスクで

gulp

とすると、'default'タスクが実行される。使用頻度が一番高いものを指定しておこう。ここでは、'default'タスクの中身が['build']なので、gulpをたたけばビルドされる。

gulp自体の説明はこのくらいにして、'build'タスクで行っている内容について。

いきなり、browserifyとか馴染みのないものが出てきますが、詳しくはこちら
大雑把にいうとnpmでインストールしたモジュールをブラウザでも使えるようにするもの。ソースのトップレベル(エントリポイントとかがあるソース)を指定すると、そこから参照されているソースを手繰ってrequireされているライブラリをかき集めて一つにまとめてくれる。

        entries:['./src/App.coffee'],
        extensions:['.coffee']

entriesに指定しているApp.coffeeがトップレベルのソースになる予定。デフォルトでは、処理対象は'.js'ファイルだけど、ここではCoffeeScriptを使うのでextensionsで指定している。

次に、

    .transform('coffeeify')

ここで、coffeeifyというプラグインを使ってCoffeeScriptからJavaScriptへ変換してる。

    .bundle() 
    .pipe(source('bundle.js')) 
    .pipe(gulp.dest('./public'));

最後に関連するすべてのソースを'bundle.js'にまとめてpublicフォルダにコピー。最終的にはこの'bundle.js'をブラウザから読んであげれば良い。

さて、gulpfile.jsでは、まだインストールしていないライブラリを使っていたので、まとめてインストールしておこう。

npm install browserify vinyl-source-stream coffeeify --save-dev

--save-devを付けるとpackage.jsonの"devDependencies"に追加され、開発時の依存関係が保持される。

ついでにCoffeeScriptも入れてしまおう。

> npm install -g coffee-script
> coffee -v
(バージョンが表示されればOk)

これでビルド環境が構築できたので、やっとArdaを使ったコーディングに入るぞっと。

まずは、src/App.coffeeですが、最初なのでHello, World的なので。

# App.coffee
window.React = require 'react'
window.Promise = require 'bluebird'
Arda = require 'arda'

App = React.createClass
    mixins: [Arda.mixin]
    render: -> return React.createElement 'div', {}, 'Hello, world! I am an App.'

class AppContext extends Arda.Context
    component: App

window.addEventListener 'DOMContentLoaded', ->
    router = new Arda.Router Arda.DefaultLayout, document.body
    router.pushContext AppContext, {}

Arda.mixinをMixinしているのがコンポーネントで、Arda.Contextから継承しているのがStore的な何かです。Fluxアーキテクチャーでは状態は中央管理なのでContextはAppContextのみ作成し、残りのCommentBox/CommentForm/CommentList/Commentには、Contextを用意しません。

上記コードでrequireしている、'react', 'bluebird', 'arda'が入っていないので、インストール。

> npm install react bluebird arda --save

Ardaはreact-0.13.0.beta以降が必要なので、バージョンを指定することに注意。 react-0.13.0がリリースされた。

そろそろ動かしてみたいので、public/index.htmlを用意。

<doctype html>
<html>
    <head>
        <title>Hello React</title>
        <script src="bundle.js"></script>
    </head>
    <body>
    </body>
</html>

bundle.jsがbrowserifyで生成されるファイルです。

じゃ、さっそくgulpコマンドをたたいてビルドしてみよう。

> gulp

publicフォルダにbundle.jsができていれば成功。

json-serverを起動して、ブラウザでアクセスすると'Hello, World! I am an App.'と表示されるはず。

ここまで来れば、Ardaで開発する枠組みはできたので、あとはひたすらコンポーネントを作るだけ。Comment -> CommentList -> CommentForm -> CommentBoxの順に作る。

Commentコンポーネントの作成

# Comment.coffee
Arda = require 'arda'
md2react = require 'md2react'

Comment = React.createClass
    mixins: [Arda.mixin]
    render:->
        React.createElement 'div', className:'comment', [
            React.createElement 'h2', className:'commentAuthor', @props.author
            md2react @props.children.toString()
        ]

module.exports = Comment

前述の通りFluxでの状態は中央管理とするので、表示に必要なデータはprops経由で渡される。逆にいうとCommentは、props.authorおよび、props.childrenを要求するコンポーネントと言える。ちなみにprops.childrenは'children'という名前のプロパティではなく、子要素を意味している。

ここで、md2reactを使っているけど、これはMardown表記をReact要素へ変換するライブラリ。チュートリアルではShowdownを使ってたけど、私の環境だとfs.readDirSyncが無いというエラーになるので、こっちを使っている。

なので、これもインストールする。

> npm install md2react --save

CommentListコンポーネントの作成

# CommentList.coffee
Arda = require 'arda'
Comment = require './Comment'

CommentList = React.createClass
    mixins: [Arda.mixin]
    render: ->
        commentNodes = @props.data.map (comment)->
            React.createElement Comment, author:comment.author, comment.text

        React.createElement 'div', className: 'commentList', commentNodes

module.exports = CommentList

CommentListは先ほど作ったCommentをリスト表示するコンポーネントなので、Commentコンポーネントの要求するデータを渡してあげる必要がある。

        commentNodes = @props.data.map (comment)->
            React.createElement Comment, author:comment.author, comment.text

この部分でデータを渡しており、自分自身はcommentの配列を要求するコンポーネントであり、各commentをCommentコンポーネントに渡している。渡し先プロパティはauthorとchildrenとなっている。

CommentFormの作成

# CommentForm.coffee
Arda = require 'arda'

CommentForm = React.createClass
    mixins: [Arda.mixin]
    render: ->
        React.createElement 'form', className: 'commentForm', onSubmit:@handleSubmit.bind(@), [
            React.createElement 'input', type:'text', placeholder:'Your name', ref:'author'
            React.createElement 'input', type:'text', placeholder:'Say someting...', ref:'text'
            React.createElement 'input', type:'submit', value:'Post'
        ]

    handleSubmit: (e)->
        e.preventDefault()
        author = React.findDOMNode(@refs.author).value.trim()
        text = React.findDOMNode(@refs.text).value.trim()
        if (!text || !author)
            return

        @dispatch 'commentSubmit', {author:author, text:text}

        React.findDOMNode(@refs.author).value = ''
        React.findDOMNode(@refs.text).value = ''

module.exports = CommentForm

CommentFormコンポーネントは先ほどの2つとは異なり、データを受け取らず、ユーザーからの入力を受けて、その内容を'commentSubmit'というキーで、ディスパッチャーに投げ、誰が受け取るかは気にしません。

ここで注意したいのは、@refs。Reactのコンポーネントにref='hoge'と名前を付けることで、@refs.hogeでアクセスできるようになる。

また、

        React.findDOMNode(@refs.author).value = ''

のように、React.findDOMNodeを使うことでDOM要素にアクセスできる。ちなみにチュートリアルでは、@refs.author.getDOMNode().valueとしていたけど、この書き方は廃止されるらしい。

CommentBoxコンポーネントの作成

# CommentBox.coffee
Arda = require 'arda'
CommentList = require './CommentList'
CommentForm = require './CommentForm'

CommentBox = React.createClass
    mixins: [Arda.mixin]
    render:->
        React.createElement 'div', className:'commentBox', [
            React.createElement 'h1', {}, 'Comments'
            React.createElement CommentList, data:@props.data
            React.createElement CommentForm, {}
        ]

module.exports = CommentBox

CommentBoxはCommentListとCommentFormのコンテナで、コメント配列を受け取って、それをCommentListに渡すだけ。

Appコンテキストの作成

# App.coffee
window.React = require 'react'
window.Promise = require 'bluebird'
request = require 'superagent'
Arda = require 'arda'
CommentBox = require './CommentBox'

App = React.createClass
    mixins: [Arda.mixin]
    render: ->
        React.createElement 'div', {}, [
            React.createElement CommentBox, data:@props.data
        ]

class AppContext extends Arda.Context
    component: App

    initState: -> data :  []

    expandComponentProps: ->
        data: @state.data

    loadCommentsFromServer: =>
        request
            .get @props.url
            .set {Accept:'application/json'}
            .end (res)=> @update (s) -> data:res.body

    delegate: (subscribe) ->
        super
        subscribe 'context:started', =>
            @loadCommentsFromServer()
            setInterval @loadCommentsFromServer, @props.pollInterval

        subscribe 'commentSubmit', (comment) =>
            request
                .post @props.url
                .send comment
                .set {Accept:'application/json'}
                .end (res) =>
                    if res.ok
                        @update (s) -> data:s.data.concat([res.body])
                    else
                        console.log res.text

window.addEventListener 'DOMContentLoaded', ->
    router = new Arda.Router Arda.DefaultLayout, document.body
    router.pushContext AppContext, {url:'/comments.json', pollInterval:2000}

中央管理なのでちょっとコードは長め。先頭から少しずつ見てみよう。

window.React = require 'react'
window.Promise = require 'bluebird'

ArdaはReactとPromiseを要求するので、グローバルに配置。Promiseライブラリとしては、'bluebird'が推奨らしい。

request = require 'superagent'

ajaxのためにjQueryを使いたくなかったので、'superagent'を使ってみた。

> npm install superagent --save

インストールは忘れずに。

Appコンポーネントは単にCommentBox要素を作成し、コメント配列を渡すだけ。

で、本題のAppContext。

    initState: -> data :  []

initStateで状態の初期値を返す。管理する状態はコメントの配列なので、初期値は空配列とする。

    expandComponentProps: (props, state) ->
        data: state.data

expandComponentPropsが返すオブジェクトがコンポーネントのプロパティとなる。Contextが持っているstate, propsは全てがコンポーネントの描画のためのものとは限らない。Ardaではそれを区別していて、props, stateからコンポーネント用プロパティを作る。プロパティなので同然immutable。

    loadCommentsFromServer: =>
        request
            .get @props.url
            .set {Accept:'application/json'}
            .end (res)=> @update (s) -> data:res.body

AppContextはコメント取得用のurlを受け取る。そのurlに対しGETリクエストを発行し、コメント配列に設定する。Ardaでは、状態の変更にはupdateメソッドを使う。updateメソッドにより状態が変更される毎にコンポーネントへの通知が飛ぶ。

    delegate: (subscribe) ->

CommentFormにて、@dispatch 'commentSubmit' ... と受取先不明で投げていたけど、その受け口がここ。

        subscribe 'commentSubmit', (comment) =>
            request
                .post @props.url
                .send comment
                .set {Accept:'application/json'}
                .end (err, res) =>
                    if res.ok
                        @update (s) -> data:s.data.concat([res.body])
                    else
                        console.log res.text

subscribe 'commentSubmit'でイベントを購読しており、ユーザの入力値がcommentわたってくるので、それをurlにPOSTして登録している。ここでもupdateが呼ばれ状態が変更され、コンポーネントへの通知が飛ぶ。

イベントはユーザが発行するもの以外にも、

        subscribe 'context:started', =>
            @loadCommentsFromServer()
            setInterval @loadCommentsFromServer, @props.pollInterval

上記のようなライフサイクルイベントのようにフレームワーク側から発生するものもある。

window.addEventListener 'DOMContentLoaded', ->
    router = new Arda.Router Arda.DefaultLayout, document.body
    router.pushContext AppContext, {url:'/comments.json', pollInterval:2000}

ここが最初に呼び出される場所。Ardaでは、routerにContextをpushすると、そのContextが開始される。別のcontextがpushされると、元あったほうはpauseされ、新しいほうが開始される。この辺は公式の説明を参照のこと。

これで、すべてのコードを書き終えたので、gulpを叩く。
ブラウザでアクセスすると、著者とコメントを入力/表示する画面が表示されるはず。

最後に

自分の怪しい理解のまま書き連ねたので間違っている箇所は沢山あると思う。理解が進めばそのうち修正されるんではないかと。あと、後半、疲れてきたのでかなり流してるね…

(3/7 追記)
expandComponentPropsの説明が抜けてたのと、コードがおかしかったのを修正。
(3/20 追記)
arda 0.13にてArda.Componentが廃止されたのでコードを修正。
(4/3 追記)
superagentの変更に対応してendのコールバックを修正。

45
44
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
45
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?