読者です 読者をやめる 読者になる 読者になる

vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

React.jsに触れてみる4 (+Sinatra)

Javascript React.js Ruby 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
  • Railsの場合はReact用のGemがあるようなのですが、Sinatraでは見つからなかったので今までと同じくターミナルからreact-toolを使ってビルドしています。

ソースコード

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

Ruby