主夫在宅パートの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では、RubyやPythonで動作する簡単なサーバーのコードを提供していますが、ここでは、もちろん、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.js
にrequire('./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.js
とtutorial3.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.author
とthis.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.js
、tutorial9.js
、そして、tutorial10.js
の内容をweb/static/js/comment.react.js
に反映します。
ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/6efecdcd09d8072c24060b82e63094a0e09f1bf9 となります。
Fetching from server
次にJSON形式のデータをサーバーから取得するように変更します。
CommentController
にcomments
メソッドを作成します。web/controllers/comment_controller.ex
にcomments
メソッドを追加します。
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
内のCommentBox
のurl
属性の値は/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.js
とtutorial16.js
の内容をweb/static/js/comment.react.js
に反映します。
ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/tree/b8bda88a412f23e5a678e1f983d0ae9807958297 となります。
そして、コメントフォームが送信される時のonSumit
イベントのコールバックをthis.props
経由で指定できるように変更します。
tutorial17.js
、tutorila18.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
CommentController
にcreate_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.ex
のcomments
メソッドを次のように変更します。
ここまでの結果は、 https://github.com/eitoball/react_phoenix_demo/commit/c71356f825d72811a9e121108464bb5a87b683d3 となります。
変更したら、データベースの内容が、コメント一覧に反映されているのを確認してみて下さい。また、コメントを投稿して、新しいコメントが一覧に反映されることも確認してみて下さい。
さいごに
Tutorialに沿って、トップダウンでReact.jsのコンポーネントを作成しながら、Phoenixで、コントローラやモデルを作成して、動的にコメント一覧が更新されるコメント投稿フォームを作成しました。
今後も、Phoenix を使ったり、React.js を使ったりして、開発を楽しんでいきたいと思います。