Phoenix Framework (Elixir)でReact.jsのTutorialを写経してみる

主夫在宅パートのeitoballです。暖かくなってきて、洗濯物の乾きが早くなってうれしいこの頃です。

はじめに

前回、Phonenix Framework (以下、Phoenix)上で、React.jsが動作する環境を構築しました。今回は、React.jsのTutorialを写経してみます。

Tutorialを写経する

前提条件

前回からの続きとなります。ソースは、前回と同じく GitHub (https://github.com/eitoball/react_phoenix_demo/tree/tutorial_commentbox) にあります。

新しいバージョン(0.13.1)のPhoenix Frameworkがリリースされたので、更新したいと思います。プロジェクトが依存するライブラリは、mix.exs内のdepsに定義されています。更新するには、mix deps.update <name>を使います。今回は、mix deps.update --allを実行して、依存するライブラリ全てを更新します。

$ mix deps.update --all                                                  
A new Hex version is available (v0.8.0), please update with `mix local.hex`
Running dependency resolution
Dependency resolution completed successfully
  ranch: v1.0.0
  postgrex: v0.8.1
  cowlib: v1.0.1
  cowboy: v1.0.0
  decimal: v1.1.0
  phoenix_ecto: v0.4.0
  phoenix_live_reload: v0.4.0
  phoenix: v0.13.1
  phoenix_html: v1.1.0
  poolboy: v1.5.1
  ecto: v0.11.3
  fs: v0.9.2
  poison: v1.4.0
  plug: v0.12.2
* Updating phoenix_html (Hex package)
Checking package (https://s3.amazonaws.com/s3.hex.pm/tarballs/phoenix_html-1.1.0.tar)
Fetched package
Unpacked package tarball (/Users/eito/.hex/packages/phoenix_html-1.1.0.tar)
* Updating ecto (Hex package)
Checking package (https://s3.amazonaws.com/s3.hex.pm/tarballs/ecto-0.11.3.tar)
Fetched package
Unpacked package tarball (/Users/eito/.hex/packages/ecto-0.11.3.tar)

A new Hex version is available (v0.8.0), please update with `mix local.hex`なので、mix local.hexを実行して更新します。

$ mix local.hex
Found existing archive(s): hex.ez.
Are you sure you want to replace them? [Yn] Y
* creating /Users/eito/.mix/archives/hex.ez

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/aeaa55617d622cf9e0b19c87aa950d9f307a41bf となります。

Running Server

Tutorialでは、RubyPythonで動作する簡単なサーバーのコードを提供していますが、ここでは、もちろん、iex -S mix phoenix.serverでサーバーを実行します。

Getting Started

TutorialのようなHTMLを提供するためのコントローラなどを作成します。以下のように4ファイルを追加・編集します。

web/controllers/comment_controller.ex

defmodule ReactPhoenix.CommentController do
  use ReactPhoenix.Web, :controller

  plug :action

  def index(conn, _params) do
    render conn, "index.html"
  end
end

web/views/comment_view.ex

defmodule ReactPhoenix.CommentView do
  use ReactPhoenix.Web, :view
end

web/views/comment/index.html.eex

<div id="content"></div>

web/router.ex (一部のみ)

  scope "/", ReactPhoenix do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    get "/hello_world", HelloWorldController, :index
    get "/comment", CommentController, :index
  end

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/b49f8699bc2c878b122809cfeb2f4d00bc1369c1 となります。

Your first component

最初にコメント一覧や投稿フォームを入れるためのCommentBoxコンポーネントを作成します。

TutorialのJavaScriptのコードは、web/static/js/comment.react.jsというファイルに書いていきます。ここでは、tutorial1.jsと同じ内容を書きます。

そして、web/static/js/app.jsrequire('./comment.react');という行を追加します。

http://localhost:4000/js/app.js の最後辺りを見てみるとcomment.react.jsが、TutorialのJSX Syntaxに記載されているように変換されていることがわかります。

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/93eefb146f76cf91e5a76263b83279ff50b8ef3a になります。

Composing components

次にコメント一覧のCommentListと投稿フォームのCommentFormコンポーネントを作成していきます。

tutorial2.jstutorial3.jsの内容をweb/static/js/comment.react.jsに反映します。

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/3053e2fe65720ffb6efecc136e29bfcb9ed18c6d となります。

Using props

コメントを格納するCommentコンポーネントを作成します。コメントのデータは、親コンポーネントCommentList)よりプロパティ(this.props)として提供されます。

tutorial4.jsの内容をweb/static/js/comment.react.jsに反映します。

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/d0ca88f3ed7b18e86c1885817463322d3d96eefb となります。

Component Properties

仮のCommentコンポーネントCommentList内に配置します。コメントのデータは、this.props.authorthis.props.childrenを通して、提供されます。

tutorial5.jsの内容をweb/static/js/comment.react.jsに反映します。

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/7ff5d152354f1cd9ddb49dae5810cf12a2d68289 となります。

Adding Markdown

コメントの内容をmarkdown形式で記述できるようにします。はじめにmarkedライブラリをインストールするためにbower install marked --saveを実行します。

$ bower install marked --save
bower cached        git://github.com/chjj/marked.git#0.3.3
bower validate      0.3.3 against git://github.com/chjj/marked.git#*
bower install       marked#0.3.3

marked#0.3.3 bower_components/marked

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/05a19973a28845351670ebd2960c73e5503184ae となります。

インストールしたら、サーバーを再起動します。Ctrl-Cを押すと以下のように表示されるので、

BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution

aを押して、サーバーを停止してから、iex -S mix phoenix.serverでサーバーを再実行します。

markedメソッドでmarkdown形式のコメントをHTMLに変換するようにします。tutorial6.jsの内容をweb/static/js/comment.react.jsに反映します。

コメントが、<p>This is <em>another</em> comment</p>と表示されているのは、XSSクロスサイトスクリプティング)攻撃されないように変換されたHTMLをReact.jsがエスケープさしているためです。

変換されたHTMLをそのまま表示するためにtutorial7.jsの内容をweb/static/js/comment.react.jsに反映します。

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/25fe37859fc4a42840bb25b35f8e7d540e2857b4 となります。

Hook up the data model

直接、記述していたコメントのデータをJSON形式のデータから作成するように変更します。

tutorial8.jstutorial9.js、そして、tutorial10.jsの内容をweb/static/js/comment.react.jsに反映します。

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/6efecdcd09d8072c24060b82e63094a0e09f1bf9 となります。

Fetching from server

次にJSON形式のデータをサーバーから取得するように変更します。

CommentControllercommentsメソッドを作成します。web/controllers/comment_controller.excommentsメソッドを追加します。

web/controllers/comment_controller.ex

def comments(conn, _params) do
  json conn, [
    %{author: "Pete Hunt", text: "This is one comment"},
    %{author: "Jordan Walke", text: "This is *another* comment"}
  ]
end

web/router.exに新しいスコープを作成します。

scope "/comment", ReactPhoenix do
  pipe_through :api

  get "/comments", CommentController, :comments
end

tutorial11.jsと異なり、web/static/js/comment.js内のCommentBoxurl属性の値は/comment/commentsとします。

web/static/js/comment.js

React.render(
  <CommentBox url="/comment/comments">,
  document.getElementById('content')
);

Reactive state

コンポーネントから、this.props経由で、データを受け取っていますが、このデータはイミュータブル(immutable)なため、動的に更新することができません。ミュータブル(mutable)なステート(state)を使って、動的に更新できるように変更します。

tutorial12.jsの内容をweb/static/js/comment.react.jsに反映します。

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/6df5eb2d2f54da4a510ebc51e9723422d4cf9ebc となります。

Updating state

componentDidMountというメソッドは、コンポーネントが描画(render)された時、React.jsによって自動的に呼び出されるので、ここにjQueryを使って、非同期にコメントのデータをサーバーから取得するようにします。

tutorial13.jsの内容をweb/static/js/comment.react.jsに反映します。

それから、コメント一覧を自動的に更新するようにtutorial14.jsのようにweb/static/js/comment.react.jsを変更します。

Adding new comments

コメントを投稿するためのフォームを作成します。

tutorial15.jstutorial16.jsの内容をweb/static/js/comment.react.jsに反映します。

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/b8bda88a412f23e5a678e1f983d0ae9807958297 となります。

そして、コメントフォームが送信される時のonSumitイベントのコールバックをthis.props経由で指定できるように変更します。

tutorial17.jstutorila18.js、そして、tutorial19.js の内容をweb/static/js/comment.react.jsに反映します。

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/460571ec8f7b6b037366a658e541c992e870802f となります。

送信されたコメントをデータベースに保存できるように`Commentモデルを作成します。mix`コマンドを使用して作成します。

$ mix phoenix.gen.model Comment comments author:string text:text
Generated react_phoenix app
* creating priv/repo/migrations/20150524045623_create_comment.exs
* creating web/models/comment.ex
* creating test/models/comment_test.exs

そして、データベースにcommentsテーブルを作成します。

$ mix do ecto.create, ecto.migrate
The database for ReactPhoenix.Repo has been created.
[info] == Running ReactPhoenix.Repo.Migrations.CreateComment.change/0 forward
[info] create table comments
[info] == Migrated in 0.0s

CommentControllercreate_commentメソッドを作成します。

web/controllers/comment_controller.ex

alias ReactPhoenix.Comment

def create_comment(conn, params) do
  changeset = Comment.changeset(%Comment{}, params)
  if changeset.valid? do
    Repo.insert(changeset)
    send_resp(conn, 201, "Created")
  else
    send_resp(conn, 422, "Unprocessable Entity")
  end
end

web/router.ex

scope "/comment", ReactPhoenix do
  pipe_through :api

  get "/comments", CommentController, :comments
  post "/comments", CommentController, :create_comment
end

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/671646f89f6da1daca98b54f0d6813c4c44df58e となります。

ここで、フォームを使ってコメントを投稿して、データベースにコメントが格納されていることを確認します。

$ psql react_phoenix_dev
psql (9.4.1)
Type "help" for help.

react_phoenix_dev=# select * from comments;
 id |    author    |           text            |     inserted_at     |     updated_at
----+--------------+---------------------------+---------------------+---------------------
  5 | Pete Hunt    | This is one comment       | 2015-05-22 01:23:24 | 2015-05-22 01:23:24
  6 | Jordan Walke | This is *another* comment | 2015-05-22 01:23:43 | 2015-05-22 01:23:43
(2 rows)

Optimization: optimistic updates

コメントを投稿したら、サーバーからの応答を待たずに即時に反映するようにします。tutorial20.jsの内容をweb/static/js/comment.react.jsに反映します。

def comments(conn, _params) do
  json conn, Repo.all(Comment)
end

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/commit/5b0b8bf463f6beb5608d3b2ffb214aab6d81eaf1 となります。

コメントを投稿すると、投稿した内容がコメント一覧の最後に表示されることを確認して下さい。一覧は2秒毎にリフレッシュされるので、新しいコメントは一瞬だけしか表示されません。

そして、データベースにあるコメントを読み込んで表示するようにweb/controllers/comment_controller.excommentsメソッドを次のように変更します。

ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/commit/c71356f825d72811a9e121108464bb5a87b683d3 となります。

変更したら、データベースの内容が、コメント一覧に反映されているのを確認してみて下さい。また、コメントを投稿して、新しいコメントが一覧に反映されることも確認してみて下さい。

さいごに

Tutorialに沿って、トップダウンでReact.jsのコンポーネントを作成しながら、Phoenixで、コントローラやモデルを作成して、動的にコメント一覧が更新されるコメント投稿フォームを作成しました。

f:id:eitoball:20150528012253g:plain

今後も、Phoenix を使ったり、React.js を使ったりして、開発を楽しんでいきたいと思います。