React.jsに触れてみる4 (+Sinatra)
ふとした思いつきで、今年のはじめに作成しかけたまま放置しているブログで、Reactを使ってみることにしました。
元のプロジェクトはsntVaguely - GitHubです。
やったこと
- Ajaxを使って、ActiveRecordで取得したデータをJSON形式で取得する。
- 取得したJSONデータをReactで表示する。
構成
プロジェクトのルートディレクトリ Lapp.rb Lconfig.ru LGemfile LRakefile Ldb Lmigrate L20141231050512_create_contents.rb Lblogposts.db Ldatabase.yml Lshema.rb Lmodels LdbAccesser.rb Lvendor Lbundle L... Lviews Lblog.slim Lerror.slim Lcss Lstylesheet.sass Lpublic Ljs Ljquery-2.1.3.min.js LJSXTransformer.js Lreact.min.js Ljavascript.js Lsrc Ljavascript.js
ソースコード
blog.slim
DOCTYPE html head meta charset="utf-8" title Vaguely link href="/css/stylesheet.css" rel="stylesheet" link rel="alternate" type="application/rss+xml" title="Vaguely" href="/feed" body header a.header_title href="/" Vaguely main#main script src='js/react.min.js' script src='js/JSXTransformer.js' script src='js/jquery-2.1.3.min.js' script src='js/javascript.js'
- htmlからslimに置き換わった以外特に変化はないです(強いて言えばブログで書いている今、スペースを入れなくてもタグとして認識されないので便利ですw)。
error.slim
DOCTYPE html head meta charset="utf-8" title Vaguely link href="/css/stylesheet.css" rel="stylesheet" body main#error_main ページがみつかりません。
- 404 Errorが発生した場合に表示するページです。
app.rb
require 'json' require 'rss/maker' require 'sass' require 'sinatra' require 'sinatra/base' require 'sinatra/reloader' require 'slim' require 'will_paginate' require 'will_paginate/active_record' require 'will_paginate/view_helpers/sinatra' require './models/dbAccessers' # TODO:テスト用に5件1ページで表示中。分量的にはこのままでもいいかも? POST_LIMIT_COUNT = 5 POST_URL_DIR = '/article/' TAG_URL_DIR = '/tag/' 〜省略〜 class MainApp < Sinatra::Base # viewでwill_pagenateを使用するのに必要. helpers WillPaginate::Sinatra::Helpers configure :development do register Sinatra::Reloader end get '/' do slim :blog end get '/list' do # get blog posts and return json data. intStartNum = (params[:page] =~ /\d+/ && params[:page].to_i > 0)? params[:page].to_i: 1 aryBlogList = Post.order(post_id: 'desc').paginate(:page => intStartNum, :per_page => POST_LIMIT_COUNT) aryJsonList = [] aryCategoryTest = [{:title =>'category1', :url => '/tag/1'}, {:title => 'カテゴリ2', :url => '/tag/2'}] aryBlogList.each do |post| aryJsonList << { postUrl: POST_URL_DIR + post.post_id.to_s, postTitle: post.post_title, post: post.post, category: aryCategoryTest, updateDate: post.updated_at.strftime('%Y-%m-%d %H:%M:%S') } end aryJsonList.to_json end not_found do slim :error end 〜省略〜
- 「bundle exec rackup」で起動し、localhost:9292でページを表示します(コントローラ側は表示するだけです)。
「localhost:9292/list?page=1」のようにアクセスされた場合に、ActiveRecordで取得した以下のデータをJSON形式で返します。
- post_id: 記事のID。アクセス用のURLに整形して使用。
- post_title: 記事タイトル。
- post: 記事本文。
- update_at: 記事の更新時間。「2015-04-09 00:43」のように整形して使用。
加えて「aryCategoryTest」として記事につけたタグ名、URLをテスト用にハッシュとしてセットしています。こちらはDBから取得した値に置き換える予定です。
ページが存在しないURLにアクセスしたら「not_found do」で、エラーページを表示します。
javascript.js
(function () { var PostLists = React.createClass({ getInitialState: function() { return{ pageNum: 1, postList: [] }; }, getPostItems: function() { $.ajax({ type: 'GET', url: '/list?page=' + this.state.pageNum, dataType: 'json', success: function(postData) { this.setState({postList: postData}); }.bind(this), error:function() { alert('Error'); }.bind(this) }); }, componentDidMount: function() { // Componentのマウント時にAjaxを自動で実行. this.getPostItems(); }, render: function() { return ( < Post postList={this.state.postList} / > ); } }); var Post = React.createClass({ render: function() { var posts = this.props.postList.map(function(postItem) { return ( < section > < a class='post_title' href={postItem.postUrl} >{postItem.postTitle}< /a > < article class='post' >{postItem.post}< /article > < Category categoryList={postItem.category} / > < footer class='updateddate' >{postItem.updateDate}< /footer > < /section > ); }); return ( < div >{posts}< /div > ); } }); var Category = React.createClass({ render: function(){ var categorys = this.props.categoryList.map(function(categoryItem) { return ( < a href={categoryItem.url} >{categoryItem.title}< /a > ); }); return ( < nav class='category' >{categorys}< /nav > ); } }); React.render(< PostLists/ >, document.getElementById('main')); }).call(this);
- javascriptのコードについては下記に。
ページのロード時に記事を自動で取得する
このコードが読み込まれると、PostListsがComponentとして読み込まれ、ページに表示されます。
しかし、そのためにはDBからデータを取得する必要があります。
「getPostItems」がその関数ですが、React.createClassの中に書いただけでは実行されません。
かといってrenderの中に書いてしまうと、Ajaxで読み込む <--> ページが更新されたので再ロード が無限ループされてしまいます。
そのため、ページがロードされた時(正確にはComponentがマウントされた時)に実行される「componentDidMount」を使用します。
取得したJSONデータをそれぞれの項目に分けて表示する
「getPostItems」によって取得したJSONデータは、以下のような内容になっています。
[{"postUrl":"/article/8","postTitle":"記事タイトル","post":"記事本文","category":[{"title":"category1","url":"/tag/1"},{"title":"カテゴリ2","url":"/tag/2"}],"updateDate":"2015-01-08 00:00:00"}, 〜省略〜
これをUrl、タイトルなどに分けて使用するには、前回使用したmapを利用します。
var Post = React.createClass({ render: function() { var posts = this.props.postList.map(function(postItem) { return ( < section > < a class='post_title' href={postItem.postUrl} >{postItem.postTitle}< /a > < article class='post' >{postItem.post}< /article > < Category categoryList={postItem.category} / > < footer class='updateddate' >{postItem.updateDate}< /footer > < /section > ); }); return ( < div >{posts}< /div > ); } });
また、タグ(カテゴリ)情報として連想配列が渡されているので、これを更に子のComponentに渡してマッピングします。
var Category = React.createClass({ render: function(){ var categorys = this.props.categoryList.map(function(categoryItem) { return ( < a href={categoryItem.url} >{categoryItem.title}< /a > ); }); return ( < nav class='category' >{categorys}< /nav > ); } });
以上で、とりあえずブログのリスト表示(1ページ分)ができるようになりました。
タグ(カテゴリ)情報のDBからの取得、CSSの設定、ページャやサイドカラムについてはまた次回。
参考
React.js
- Tutorial - React
- javascript - How to render asynchronously initialized components on the server - Stack Overflow