Nic Lin's Blog

喜歡在地上滾的工程師

在Rails 4 上 實作 React.js 入門教學

React正夯

目前網路上的React.js實作在Rails大概都是在本篇文章產生前的一兩年,雖說時間並不久遠,但是以現今React火熱的程度,這兩年足以產生巨大的差異性更新,筆者搜尋到的範例所使用react-rails Gem版本大約都是(1.0.0.pre),目前卻已經到(1.7.2),舊的範例在執行上或許問題不大,但如果直接寫在新的專案可能就會爆炸,對於學習入門React在Rails上面就會一直不斷撞牆,可能連在Rails上實作React.js的HelloWorld都有問題,所以在無限撞牆之餘,就來寫個簡單的入門教學吧!

關於React.js

React.js是一個”JavaScript library”,相對於Ruby on Rails這種實現了MVC框架來說,我們說React.js更專注於實現V(view),React主要透過兩個原則來建造:Components和States。

Components 可以用其他更小的零件來封裝成組件,然後像插入普通HTML tag一樣,在網頁插入組件。

State驅動了one-way data flow (或稱 one-way binding)單向數據流保持所有資料一致,同時也方便模組化。這樣代表我們的UI將會對每一次的改變做出反應

而State是React放資料Model的其中一種,另一種則是props,這其中的差別請閱讀官方文章

React.js on Rails 實作教學

Source code位置在我的Github,以下的code是修改自官方Tutorial,讓其程式碼能夠順利於Rails運行,其中遇到的問題解決不是很好的部分,在歡迎issue給我。

目前遇到的問題 1. Juery_ujs拔掉之後,出現csrf無法驗證的情況,目前將create的csrf驗證暫時關閉。

創建你的Rails App

這邊筆者使用的版本分別是 Ruby-2.2.3 Rails-4.2.6

1.新建專案 rails new react-tutorial-on-rails 2.切換至專案目錄 cd react-tutorial-on-rails 3.Run bundler bundle install 4.加入Git追蹤 git init . 5.將剛剛生成的專案第一次commit git add . && git commit -m "rails new react-tutorial-on-rails"

利用Rails的鷹架(Scaffold)建立基礎的留言板模型(Comment model)

1.為了直接對應官方React.js的入門教學,這邊就用Comment做為開始 rails generate scaffold Comment author:string text:text 2.建立資料庫 rake db:migrate 3.Commit存檔 git add . && git commit -m "Generate scaffold Comment and rake db:migrate"

建立一個page頁面作為React渲染的地方

1.建立controller rails generate controller pages index 2.更改首頁指向位置,將程式生出來的get 'pages/index'替換成resources :pages

  resources :pages
  resources :comments
  
  # The priority is based upon order of creation: first created -> highest priority.
  # See how all your routes lay out with "rake routes".
  .......

測試目前的Rails App 運作情形

1.將你的Rails App跑起來 rails server 2.打開你的瀏覽器,並輸入 localhost:3000/pages,檢查是否導入首頁page/index

螢幕快照_2016-06-22_下午3_09_48.png

3.在瀏覽器輸入localhost:3000/comments,測試一下CRUD是否都可以使用了

點擊New comment,任意輸入一些等等會用到的資料。

螢幕快照 2016-06-22 下午1.35.15.png

資料建立完成 螢幕快照 2016-06-22 下午1.36.39.png

4.測試我們使用scaffold所建立的comment在吐json資料時是否正常,可以另開一個Terminal,輸入curl localhost:3000/comments.json

螢幕快照 2016-06-22 下午1.40.55.png

5.到這裡為止都沒有出錯的話,就Commit吧! git add . && git commit -m "Added Pages controller"

安裝React.js所需的Gem

這裡我們使用的Gem是react-rails,詳細請看Github 這個Gem已經幫我們把React.js打包好,並且有一些helper可以做為使用,這邊使用的Gem版本為1.7.2。

1.打開Gemfile 並且加入react-rails gem

    gem 'react-rails'

2.bundle bundle install 3.照的官方文件進行安裝 rails g react:install,會生成的檔案如下

螢幕快照 2016-06-22 下午1.51.19.png

4.移除 jquery_ujs,避免與react_ujs衝突

//= require jquery
-  //= require jquery_ujs <--刪除這行
//= require turbolinks
//= require react
//= require react_ujs
//= require components
//= require_tree .

5.記得重新開啟 rails server

使用React官方的Tutorial,寫一些React code

這邊我們建議將React官方的Tutorial看過一遍,並且可以參考實作的React-Tutorial Github)

1.依照官方範例創建以下四隻檔案

var Comment = React.createClass({
  rawMarkup: function() {
    var md = new Remarkable();
    var rawMarkup = md.render(this.props.children.toString());
    return { __html: rawMarkup };
  },

  render: function () {
    return (
      <div className="comment panel panel-default">
        <div className="panel-heading">
          <h3 className="panel-title">
            {this.props.author} 說了:
          </h3>
        </div>
        <div className="panel-body">
          {this.props.text}
        </div>
      </div>
    )
  }
});
var CommentBox = React.createClass({
  getInitialState: function () {
    return JSON.parse(this.props.presenter);
  },

  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: function(data) {
        this.setState({ comments: data });
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },

  handleCommentSubmit: function ( formData, action ) {
    $.ajax({
      data: { comment: formData},
      url: action,
      type: "POST",
      dataType: "json",
      success: function ( data ) {
        this.setState({ comments: data });
      }.bind(this)
    });
  },

  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, 2000);
  },

  render: function () {
    return (
      <div className="comment-box">
        <img src={ this.props.imgSrc } alt={ this.props.imgAlt } />
        <CommentList comments={ this.state.comments } />
        <hr />
        <h2>新增留言</h2>
        <CommentForm form={ this.state.form } onCommentSubmit={ this.handleCommentSubmit } />
      </div>
    );
  }
});
var CommentForm = React.createClass({
  getInitialState: function() {
    return {author: '', text: ''};
  },
  handleAuthorChange: function(e) {
    this.setState({author: e.target.value});
  },
  handleTextChange: function(e) {
    this.setState({text: e.target.value});
  },
  handleSubmit: function(e) {
    e.preventDefault();
    var author = this.state.author.trim();
    var text = this.state.text.trim();
    if (!text || !author) {
      return;
    }
    this.props.onCommentSubmit({author: author, text: text});
    this.setState({author: '', text: ''});
  },
  render: function() {
    return (
      <div className="panel panel-default">
        <div className="panel-heading">聊聊吧</div>
        <div className="panel-body">
          <form className="commentForm " onSubmit={this.handleSubmit} action={ this.props.form.action } acceptCharset="UTF-8" method="post" onSubmit={ this.handleSubmit } >
            <div className="form-group">
              <label className="control-label" forName="authorInput">暱稱</label>
              <input type="text" id="authorInput" className="form-control" placeholder="Your name" ref="author" value={this.state.author} onChange={this.handleAuthorChange}/>
            </div>
            <div className="form-group">
              <label className="control-label" forName="commentInput">留言</label>
              <textarea className="form-control" id="commentInput" rows="3" placeholder="Say something..." ref="text" value={this.state.text} onChange={this.handleTextChange}/>
            </div>
            <input type="submit" className="btn btn-primary" value="Post" />
          </form>
        </div>
      </div>
    );
  }
});

var CommentList = React.createClass({
  render: function () {
    var commentNodes = this.props.comments.map(function ( comment ) {
      return <Comment author={ comment.author } text={ comment.text } key={ comment.id } />
    });

    return (
      <div className="comment-list">
        { commentNodes }
      </div>
    )
  }
});

2.利用React gem的helper 呼叫component

  <%= react_component('CommentBox', {:presenter => @presenter.to_json}, {:prerender => true}) %>

3.處理json所需的資料

class PagesController < ApplicationController
  skip_before_filter :verify_authenticity_token, only: [:create]
  def index
    @presenter = {
      :comments => Comment.last(5),
      :form => {
        :action => comments_path,
        :csrf_param => request_forgery_protection_token,
        :csrf_token => form_authenticity_token
      }
    }
  end

  def create
    @comment = Comment.new(comment_params)
    @comment.save

    if request.xhr?
      render :json => Comment.last(5)
    else
      redirect_to comments_path
    end
  end

  private
  def comment_params
    params.require(:comment).permit(:author, :text)
  end
end

4.於瀏覽器輸入 http://localhost:3000/pages 進行測試,沒意外會看到以下畫面,可以利用chrome進行檢查,看react生出的html

螢幕快照 2016-06-22 下午2.28.05.png

5.以上步驟都沒問題請記得commitgit commit -am “React component loads”

bonus: 加入bootstrap讓畫面更好看吧

1.加入bootstrap-sass

+  gem 'bootstrap-sass'

2.安裝Gem bundle install

3.安裝bootstrap所需javascript

+ //= require bootstrap-sprockets

4.安裝bootstrap所需CSS,並將原本的application.css檔名改為application.css.scss

+ @import "bootstrap-sprockets";
+ @import "bootstrap";

5.利用bootstrap的css修正畫面

<div class="container">
<h3>React</h3>
<hr>
  <%= react_component('CommentBox', {:presenter => @presenter.to_json}, {:prerender => true}) %>
</div>

6.完成並且commit git add . & git commit -m "Added Bootstrap sass"

comments powered by Disqus