React.js的Rails開發者指南

React.js的Rails開發者指南

原作者:Fernando Villalobos

原文鏈接:https://www.airpair.com/reactjs/posts/reactjs-a-guide-for-rails-developers

譯者:Sail Lee

目錄

React.js簡介

React.js是一個近似“JavaScript框架”的流行類庫,因其簡潔而出眾。相對于其他完整實現了MVC結構的框架,我們說React僅實現了V(其實有些人用React來代替它們框架的V部分)。React應用程序通過兩個主要原則來構造:Components和States。Components可以用其他更小的組件來構成,內置或定制;State驅動了Facebook稱之為單向響應式數據流的東西,這意味著我們的UI將會對每次狀態的改變作出反應。

React的一個優點之一就是它無需任何額外的依賴,這讓它幾乎能和任何其他的JS庫插接到一起。利用這個特征,我們將其囊括到我們Rails的技術棧中,來構建一個前端強大的應用,也許你會說它是個Rails視圖層的興奮劑。

一個模擬的費用跟蹤應用

在本指南中,我們正要從零做起,構建一個記錄日常花費的小應用。每個記錄將包括一個日期、標題和金額。假如一個記錄的金額大于零,它將被認為是貸方(譯者注:會計術語),相反則計入借方(譯者注:會計術語)。這是項目的模型:


項目的模型
項目的模型

總結下,該應用表現如下:

  • 當用戶通過橫向的表單創建一個新記錄時,它將被添加到記錄表格中去。
  • 用戶可以對任何存在的記錄進行行內編輯。
  • 點擊任何刪除按鈕會把相關的記錄從表格中刪除。
  • 增加、編輯或移除一個存在的記錄都將更新位于頁面頂部的各項合計項。

在Rails項目中初始化React.js

首先,我們要開始一個全新的Rails項目,我們叫它Accounts

rails new accounts

我們將使用Twitter的Bootstrap做此項目的UI。安裝流程非本文討論范圍,你可以根據官方github倉庫的指引來安裝bootstrap-sass官方gem。

一旦項目初始化后,我們接下來要把React包含進來。本文中,因為我們打算利用react-rails這個官方gem里面的一些很酷的功能,所以要將其包含進項目。其實也有其他方法來完成這項任務,如使用Rails assets、甚至從官方頁面下載源碼包并把它們復制到項目的javascripts目錄。

如果你曾經開發過Rails應用,你會知道安裝一個gem有多容易:把react-rails添加到你的Gemfile文件中去。

gem 'react-rails', '~> 1.0'

然后,(友好地)讓Rails來安裝新的gem包:

bundle install

react-rails帶有一個腳本,會在我們存放React組件的app/assets/javascripts目錄下創建components.js文件和components目錄。

rails g react:install

在跑完安裝之后,如果你看看application.js文件中會發現以下三行:

//= require react
//= require react_ujs
//= require components

基本上,它包含了實際上的react庫、components組件清單文件和一種以ujs結尾的常見文件。由文件名你可以已經猜到,react-rails包含了一種幫助我們載入React組件并且同時也處理Turbolinks事件的非入侵式JavaScript驅動。

創建Resource

我們將要構建一個包含datetitleamount字段的Record資源(resource)。我們要用resource生成器(generator)來代替scaffold,這樣我們就不會用到由scaffold創建的所有文件和方法。另一個選擇是先運行scaffold生成器,接著刪除無用的文件或方法,但是這樣會另我們的項目有點亂。進入項目目錄后,運行以下命令:

rails g resource Record title date:date amount:float

運行完后,我們最后將得到一個新的Record model、controller和routes。我們現在只需要創建我們的數據庫并運行之后的數據遷移。

rake db:create db:migrate

作為附加,你可以通過rails console創建兩個記錄:

Record.create title: 'Record 1', date: Date.today, amount: 500
Record.create title: 'Record 2', date: Date.today, amount: -100

別忘了用rails s來啟動你的服務器。
好了!我們要準備寫點代碼了。

嵌套式組件:記錄列表

我們的第一個任務需要在一個表格中展示任何已有的記錄。首先,我們需要在RecordController里面創建一個index動作(action)。

  # app/controllers/records_controller.rb

  class RecordsController < ApplicationController
    def index
      @records = Record.all
    end
  end

接著,我們要在app/views/records/目錄下創建一個新文件index.html.erb,該文件在我們的Rails應用和React組件之間扮演著橋梁的作用。要完成該任務,我們將使用helper方法react_component,通過它來獲取我們要展示的React組件的名稱連同我們要傳遞給它的數據。

  <%# app/views/records/index.html.erb %>

  <%= react_component 'Records', { data: @records } %>

需要指出的是,該helper是由react-railsgem包提供的,假如你決定使用其他的集成React的方法,就不能用到這個helper。

你現在能到localhost:3000/records這個路徑看看了。顯然,因為Records這個React組件的缺失,這還未能工作。但是,如果我們看看瀏覽器中的HTML源文件,我們就能發現類似以下的代碼:

  <div data-react-class="Records" data-react-props="{...}">
  </div>

有了這個標記,react_ujs就會檢測到我們嘗試展示一個React組件并實例化它,包括我們通過react_component發送的屬性,在本案例中,就是@records的內容。

構建我們第一個React組件的時間到了,進入javascripts/components目錄,創建一個叫records.js.coffee的新文件來放置我們的Records組件。

  # app/assets/javascripts/components/records.js.coffee

  @Records = React.createClass
    render: ->
      React.DOM.div
        className: 'records'
        React.DOM.h2
          className: 'title'
          'Records'

每個組件都需要一個render方法,它將負責渲染組件本身。render方法會返回一個ReactComponent的實例,這樣,當React執行重新渲染時,它將以最優的方式進行(當React檢測新節點存在時,會在內存中構建一個虛擬的DOM)。在上面代碼中,我們創建了一個h2實例,內置于ReactComponent中。

注意:實例化ReactComponent的另一個方法是在render方法中使用JSX語法,以下代碼段與前段代碼作用相同:

  render: ->
    `<div className="records">
      <h2 className="title"> Records </h2>
    </div>`

對我個人而言,當我使用CoffeeScript時,我更喜歡使用React.DOM語法而不是JSX,因為代碼可以排列成一個層次結構,類似于HAML。但是,如果你正嘗試集成React到一個用erb文件建立的現有應用中,你可以選擇重用現有erb代碼并將其轉換成JSX。

你現在可以刷新瀏覽器了。

records_1
records_1

好極了!我們已經畫出第一個React組件了。現在,是時候顯示我們的記錄了。

除了render方法以外,React組件還依靠properties的使用來和其他組件溝通,并且用states來檢測是否需要進行重新渲染。我們需要用期望的值來初始化我們的組件狀態和屬性值:

  # app/assets/javascripts/components/records.js.coffee

  @Records = React.createClass
    getInitialState: ->
      records: @props.data
    getDefaultProps: ->
      records: []
    render: ->
      ...

getDefaultProps方法將初始化我們組件的屬性,以防在初始化時我們忘了發送任何數據。而getInitialState方法則會生成我們組件的初始狀態。現在我們還要顯示由Rails提供的記錄。

看起來我們還需要一個格式化amount字符串的helper方法,我們可以實現一個簡單的字符串格式化工具并使其能讓所有其他的coffee文件訪問。用下列內容,在javascripts/目錄下創建一個新的utils.js.coffee文件:

  # app/assets/javascripts/utils.js.coffee

  @amountFormat = (amount) ->
    '$ ' + Number(amount).toLocaleString()

我們需要創建一個新的Record組件來顯示每個單獨的記錄,在javascripts/components目錄下創建一個record.js.coffee的新文件,并插入以下內容:

  # app/assets/javascripts/components/record.js.coffee

  @Record = React.createClass
    render: ->
      React.DOM.tr null,
        React.DOM.td null, @props.record.date
        React.DOM.td null, @props.record.title
        React.DOM.td null, amountFormat(@props.record.amount)

Record組件將顯示一個包含記錄各個屬性值單元格的表格行。不用擔心那些在React.DOM.*調用中的那些null,那意味著我們不用傳送屬性值給組件。現在用以下代碼更新下Record組件中的render方法:

  # app/assets/javascripts/components/records.js.coffee

  @Records = React.createClass
    ...
    render: ->
      React.DOM.div
        className: 'records'
        React.DOM.h2
          className: 'title'
          'Records'
        React.DOM.table
          className: 'table table-bordered'
          React.DOM.thead null,
            React.DOM.tr null,
              React.DOM.th null, 'Date'
              React.DOM.th null, 'Title'
              React.DOM.th null, 'Amount'
          React.DOM.tbody null,
            for record in @state.records
              React.createElement Record, key: record.id, record: record

你是否看到剛剛發生了什么?我們創建了一個帶標題行的表格,并且在表格體內為每個已有的記錄創建了一個Record元素。換句話說,我們正嵌套了內置或定制的React組件。相當酷,是不?

當我們處理動態子組件(本案例中為記錄)時,我們需要提供一個key屬性來動態生成的元素,這樣React就不會很難刷新UI,這就是為何我們要在創建Record元素時隨同實際的記錄一起發送key: record.id。如果不是這樣做,我們將會在瀏覽器的JS控制臺收到一條警告信息(并且在不遠的將來產生一些頭痛的問題)。

records_2
records_2

你可以到這里去看看本章的結果代碼,或者僅僅到這里看看由本章引入的改變。

父子組件間通信:創建記錄

現在我們顯示了所有的已有記錄,最好能包含一個用于創建記錄的表單,讓我們增加一個新功能給我們的React/Rails應用。

首先,我們吸引加入一個create方法到Rails控制器(不要忘了使用_strongparams):

  # app/controllers/records_controller.rb

  class RecordsController < ApplicationController
    ...

    def create
      @record = Record.new(record_params)

      if @record.save
        render json: @record
      else
        render json: @record.errors, status: :unprocessable_entity
      end
    end

    private

      def record_params
        params.require(:record).permit(:title, :amount, :date)
      end
  end

接著,我們需要構建一個用于處理創建新記錄的React組件。該組件將擁有自己的state來存放datetitleamount。用下列代碼,在目錄javascripts/components下創建一個record_form.js.coffee的新文件:

  # app/assets/javascripts/components/record_form.js.coffee

  @RecordForm = React.createClass
    getInitialState: ->
      title: ''
      date: ''
      amount: ''
    render: ->
      React.DOM.form
        className: 'form-inline'
        React.DOM.div
          className: 'form-group'
          React.DOM.input
            type: 'text'
            className: 'form-control'
            placeholder: 'Date'
            name: 'date'
            value: @state.date
            onChange: @handleChange
        React.DOM.div
          className: 'form-group'
          React.DOM.input
            type: 'text'
            className: 'form-control'
            placeholder: 'Title'
            name: 'title'
            value: @state.title
            onChange: @handleChange
        React.DOM.div
          className: 'form-group'
          React.DOM.input
            type: 'number'
            className: 'form-control'
            placeholder: 'Amount'
            name: 'amount'
            value: @state.amount
            onChange: @handleChange
        React.DOM.button
          type: 'submit'
          className: 'btn btn-primary'
          disabled: !@valid()
          'Create record'

不是太花俏,僅僅是個平常的Bootstrap內嵌表單。注意,我們定義了value屬性來設置輸入的值,并且定義了onChange屬性來綁定一個處理器方法,它將會在每次按鍵時都會被調用。handleChange處理器方法將用name屬性來檢測那一次輸入觸發了事件并更新相關的state值:

  # app/assets/javascripts/components/record_form.js.coffee

  @RecordForm = React.createClass
    ...
    handleChange: (e) ->
      name = e.target.name
      @setState "#{ name }": e.target.value
    ...

我們剛用了字符串插值來動態地定義對象的鍵值,當name等于title時,與@setState title: e.target.value等值。但為何我們必須使用@setState?為什么我們不能象對待普通的JS對象一樣,僅對@state設置期望的值呢?因為@setState會產生兩個動作:

  1. 更新組件的state
  2. 基于新狀態,安排一個UI的驗證或刷新

當我們每次在我們的組件中使用state時,掌握這個知識是非常重要的。

讓我們看看submit按鈕,就在render方法最后的地方:

  # app/assets/javascripts/components/record_form.js.coffee

  @RecordForm = React.createClass
    ...
    render: ->
      ...
      React.DOM.form
        ...
        React.DOM.button
          type: 'submit'
          className: 'btn btn-primary'
          disabled: !@valid()
          'Create record'

我們用!@valid()定義了一個disabled屬性,這意味著我們將要實現一個valid方法來判斷由用戶提供的數據是否是正確的。

  # app/assets/javascripts/components/record_form.js.coffee

  @RecordForm = React.createClass
    ...
    valid: ->
      @state.title && @state.date && @state.amount
    ...

為了簡化,我們僅僅校驗@state屬性是否為空。這樣,每次狀態更新后,Create record按鈕都根據數據的有效性來決定可用或不可用。

creating_record_1
creating_record_1

creating_record_2
creating_record_2

現在控制器和表單都已就位,是時候提交新記錄給服務器了。我們需要處理表單的submit事件。要完成這項任務,我們需要給表單添加一個onSubmit屬性和一個新的handleSubmit方法(如同之前我們處理onChange事件一樣):

  # app/assets/javascripts/components/record_form.js.coffee

  @RecordForm = React.createClass
    ...
    handleSubmit: (e) ->
      e.preventDefault()
      $.post '', { record: @state }, (data) =>
        @props.handleNewRecord data
        @setState @getInitialState()
      , 'JSON'

    render: ->
      React.DOM.form
        className: 'form-inline'
        onSubmit: @handleSubmit
      ...

讓我們逐行檢閱下這個新方法:

  1. 阻止表單的HTTP提交
  2. POST新的record信息到當前URL
  3. 提交成功后執行回調函數

success回調函數是這個過程的關鍵,在成功地創建新記錄后,關于這個動作和state恢復到初始值的信息會被通報。還記得之前我曾提到的組件通過屬性(或@props)與其他組件進行溝通嗎?對,就是它。當前我們這個組件就是通過@props.handleNewRecord發回數據給父組件,來通知它存在一個新記錄。

也許你已經猜到,無論在哪里創建RecordForm元素,我們要傳遞一個handleNewRecord屬性,并用一個方法引用到它,就像React.createElement RecordForm, handleNewRecord: @addRecord。好,父組件Records就是這個“無論在哪里”,由于它擁有一個附帶了所有現存記錄的state,所有需要我們用新建記錄來更新它的state。

records.js.coffee中添加新的addRecord方法并創建這個新的RecordForm元素,就在h2標題之后(在render方法之中)。

  # app/assets/javascripts/components/records.js.coffee

  @Records = React.createClass
    ...
    addRecord: (record) ->
      records = @state.records.slice()
      records.push record
      @setState records: records
    render: ->
      React.DOM.div
        className: 'records'
        React.DOM.h2
          className: 'title'
          'Records'
        React.createElement RecordForm, handleNewRecord: @addRecord
        React.DOM.hr null
      ...

刷新瀏覽器,在表單中填入一個新記錄,點擊Create record按鈕...這次沒有懸念,記錄幾乎立即被添加,而且在提交后表單被清理了,刷新僅僅是為了確認新數據已經被存入了后端服務器。

creating_record_and_records
creating_record_and_records

如果連同Rails一起,使用其他的JS框架(例如AngularJS)來構建類似的功能,你或許會遇到問題,因為你的POST請求不包括Rails所需的CSRFtoken。那么為什么我們沒有遇到同樣的問題?很簡單,因為部門使用jQuery與后端交互,而且Rails的jquery_ujs非入侵式驅動器為我們每個AJAX請求都包含了CSRFtoken。酷!

你可以到這里去看看本章的結果代碼,或者僅僅到這里看看由本章引入的改變。

可重用組件:合計指標

一個應用程序怎能沒有一些漂亮的指標呢?讓我們拿些有用的信息在窗口頂部添加一些指標框。我們的目的是為了在本章中顯示三個值:貸方合計、借方合計和余額。這看起來像是三個組件,或僅僅是一個帶屬性組件的工作量?

我們能構建一個新的AmountBox組件,它獲取三個屬性:amounttexttype。在javascripts/components目錄下創建一個叫做amount_box.js.coffee的文件,并粘貼以下代碼:

  # app/assets/javascripts/components/amount_box.js.coffee

  @AmountBox = React.createClass
    render: ->
      React.DOM.div
        className: 'col-md-4'
        React.DOM.div
          className: "panel panel-#{ @props.type }"
          React.DOM.div
            className: 'panel-heading'
            @props.text
          React.DOM.div
            className: 'panel-body'
            amountFormat(@props.amount)

我們只用Bootstrap的panel元素以“塊狀”的方式來顯示信息,并且通過type屬性來設定顏色。我們也包含了一個叫做amountFormatter的相當簡單的合計格式化方法,它讀取amount屬性并以貨幣格式來顯示它.

為了有個完整的解決方案,我們需要在主組件中創建這個元素(三次),依賴我們要顯示的數據,傳送給所需的屬性。讓我們首先構建計算器方法,打開Records組件并添加以下代碼:

  # app/assets/javascripts/components/records.js.coffee

  @Records = React.createClass
    ...
    credits: ->
      credits = @state.records.filter (val) -> val.amount >= 0
      credits.reduce ((prev, curr) ->
        prev + parseFloat(curr.amount)
      ), 0
    debits: ->
      debits = @state.records.filter (val) -> val.amount < 0
      debits.reduce ((prev, curr) ->
        prev + parseFloat(curr.amount)
      ), 0
    balance: ->
      @debits() + @credits()
    ...

credits合計所有金額大于0的記錄,debits合計所有金額小于0的記錄,而余額就無需多解釋了。現在計算器方法已經就位了,我們僅需在render方法中創建AmountBox元素(就像上面的RecordForm組件一樣):

  # app/assets/javascripts/components/records.js.coffee

  @Records = React.createClass
    ...
    render: ->
      React.DOM.div
        className: 'records'
        React.DOM.h2
          className: 'title'
          'Records'
        React.DOM.div
          className: 'row'
          React.createElement AmountBox, type: 'success', amount: @credits(), text: 'Credit'
          React.createElement AmountBox, type: 'danger', amount: @debits(), text: 'Debit'
          React.createElement AmountBox, type: 'info', amount: @balance(), text: 'Balance'
        React.createElement RecordForm, handleNewRecord: @addRecord
    ...

我們已經完成這個功能了!刷新瀏覽器,你會看到三個框里面顯示計算好的金額。但是!這還沒完!創建個新記錄看看有什么神奇的東西...

amount_indicators
amount_indicators

你可以到這里去看看本章的結果代碼,或者僅僅到這里看看由本章引入的改變。

setState/replaceState:刪除記錄

我們清單中的下一個功能是刪除記錄,我們需要在記錄表格中增加一個新的Actions列,對于每個記錄的該列中都會有一個Delete按鈕,相當標準的UI。和之前的例子一樣,我們要在Rails控制器中創建一個destroy方法:

  # app/controllers/records_controller.rb

  class RecordsController < ApplicationController
    ...

    def destroy
      @record = Record.find(params[:id])
      @record.destroy
      head :no_content
    end

    ...
  end

那就是我們為此功能所需的全部服務器端代碼。現在,打開Records組件并在表頭最右邊的位置添加Actions列:

  # app/assets/javascripts/components/records.js.coffee

  @Records = React.createClass
    ...
    render: ->
      ...
      # almost at the bottom of the render method
      React.DOM.table
        React.DOM.thead null,
          React.DOM.tr null,
            React.DOM.th null, 'Date'
            React.DOM.th null, 'Title'
            React.DOM.th null, 'Amount'
            React.DOM.th null, 'Actions'
        React.DOM.tbody null,
          for record in @state.records
            React.createElement Record, key: record.id, record: record

最后,打開Record組件并用Delete鏈接添加一個額外的列:

  # app/assets/javascripts/components/record.js.coffee

  @Record = React.createClass
    render: ->
      React.DOM.tr null,
        React.DOM.td null, @props.record.date
        React.DOM.td null, @props.record.title
        React.DOM.td null, amountFormat(@props.record.amount)
        React.DOM.td null,
          React.DOM.a
            className: 'btn btn-danger'
            'Delete'

保存你的文件,刷新瀏覽器并...我們有的只是沒有的按鈕,還沒把事件附上!

deleting_record_1
deleting_record_1

讓我們添加一些功能給它。和我們從RecordForm組件里學到的一樣,方法如下:

  1. 檢測在子組件Record中的事件(onClick)
  2. 執行一個動作(在本案例中,發送一個DELETE請求到服務器)
  3. 針對該動作,通知父組件Records(通過props來發送或接收一個處理器方法)
  4. 更新Record組件的狀態

要實現步驟1,我們可以為onClick添加一個處理器到Record,就像我們為onSubmit添加一個處理器到RecordForm來創建新記錄一樣。幸運的是,React以標準化方式實現了大多數常見瀏覽器事件,這樣我們就無需擔心跨瀏覽器的兼容性(你可以在這里查看到完整的事件清單)。

重新打開Record組件,添加一個新的handleDelete方法和一個onClick屬性到“無用”的刪除按鈕,代碼如下:

  # app/assets/javascripts/components/record.js.coffee

  @Record = React.createClass
    handleDelete: (e) ->
      e.preventDefault()
      # yeah... jQuery doesn't have a $.delete shortcut method
      $.ajax
        method: 'DELETE'
        url: "/records/#{ @props.record.id }"
        dataType: 'JSON'
        success: () =>
          @props.handleDeleteRecord @props.record
    render: ->
      React.DOM.tr null,
        React.DOM.td null, @props.record.date
        React.DOM.td null, @props.record.title
        React.DOM.td null, amountFormat(@props.record.amount)
        React.DOM.td null,
          React.DOM.a
            className: 'btn btn-danger'
            onClick: @handleDelete
            'Delete'

當刪除按鈕被點擊時,handleDelete發送一個AJAX請求到服務器來刪除后端的記錄,之后,針對本次動作,它通過handleDeleteRecord處理器可用的props來通知父組件。這意味著我們需要在父組件中調整Record元素的創建來包含額外的屬性handleDeleteRecord,而且還要在父組件中實現實際的處理器方法:

  # app/assets/javascripts/components/records.js.coffee

  @Records = React.createClass
    ...
    deleteRecord: (record) ->
      records = @state.records.slice()
      index = records.indexOf record
      records.splice index, 1
      @replaceState records: records
    render: ->
      ...
      # almost at the bottom of the render method
      React.DOM.table
        React.DOM.thead null,
          React.DOM.tr null,
            React.DOM.th null, 'Date'
            React.DOM.th null, 'Title'
            React.DOM.th null, 'Amount'
            React.DOM.th null, 'Actions'
        React.DOM.tbody null,
          for record in @state.records
            React.createElement Record, key: record.id, record: record, handleDeleteRecord: @deleteRecord

基本上,我們的deleteRecord方法拷貝了當前組建的recordsstate,執行了一個要被刪除記錄的索引查找,從該數組中拼接好并更新組件的state,相當標準的JavaScript操作。

我們介紹一個和state交互的新辦法,replaceStatesetStatereplaceState的主要不同在于前者僅更新state對象的一個鍵值,而后者會用任何我們發送的新對象來完全覆蓋組件的當前state。

在更新完上面那點代碼后,刷新瀏覽器并嘗試刪除一個記錄,會兩個事情發生:

  1. 該記錄會從表格中消失
  2. 指標的金額會立即更新,不需要額外的代碼了
deleting_record_2
deleting_record_2

我們幾乎完成整個應用程序了,但在實現最后一個功能之前,我們能實施一個小重構,并同時介紹一個新的React功能。

你可以到這里去看看本章的結果代碼,或者僅僅到這里看看由本章引入的改變。

重構:State Helpers

現在為止,我們已經有兩種方法讓state作為我們的數據獲取更新,沒有任何困難,并不像你所說的那么“復雜”。但設想下一個帶有多層次JSON state的更復雜的應用程序,你能自己想象下執行深度復制和變換你的state數據。React包含了一些花俏的state helpers來幫助你應對這個重擔。無論你的state有多深,這些 helper 都會讓你如同使用 MongoDB 的查詢語言一樣,更自由地操縱它(至少React的文檔是這樣說的)。

在使用這些helper之前,首先我們需要配置下我們的Rails應用程序來包含它們。打開你項目的config/application.rb文件并在Application代碼塊的尾部添加一行config.react.addons = ture

  # config/application.rb

  ...
  module Accounts
    class Application < Rails::Application
      ...
      config.react.addons = true
    end
  end

為了另其生效,你要重啟Rails服務器你要重啟Rails服務器你要重啟Rails服務器,重要的事情說三遍!現在我們可以通過React.addons.update來訪問state helpers,它們會處理我們的 state 對象(或任何我們發送給它的對象),并且能使用提供的命令。我們將會使用的兩個命令是$push$splice(對這些命令,我借用官方React文檔的解釋):

  • {$push: array}array里的所有數據項push()到目標去
  • {$splice: array of arrays}對于在arrays中的每個數組項array,在目標數組中用數據項提供的參數調用splice()

我們打算用這些helper來簡化Record組件的addRecorddeleteRecord,代碼如下:

  # app/assets/javascripts/components/records.js.coffee

  @Records = React.createClass
    ...
    addRecord: (record) ->
      records = React.addons.update(@state.records, { $push: [record] })
      @setState records: records
    deleteRecord: (record) ->
      index = @state.records.indexOf record
      records = React.addons.update(@state.records, { $splice: [[index, 1]] })
      @replaceState records: records

同樣的結果,更短更優雅的代碼,現在你可以隨便重載下瀏覽器并確認有沒什么不妥。

你可以到這里去看看本章的結果代碼,或者僅僅到這里看看由本章引入的改變。

響應式數據流:編輯記錄

為了實現最后一個功能,我們現在添加一個額外的Edit按鈕,放在我們記錄表格中的每個Delete按鈕的旁邊。當這個Edit按鈕被點擊時,它將整個數據行從只讀狀態切換成可編輯狀態,展示一個行內表單以便用戶可以更新記錄的內容。在提交被更新內容或取消該操作后,該記錄行將會到它原來的只讀狀態。

正如你從上文描述中猜到的那樣,我們需要處理可變(mutable)數據來切換在Record組件中每個記錄的狀態。這是一個React調用響應式數據流(reactive data flow)的用例。讓我們添加一個edit標志和一個handleToggle方法到record.js.coffee

  # app/assets/javascripts/components/record.js.coffee

  @Record = React.createClass
    getInitialState: ->
      edit: false
    handleToggle: (e) ->
      e.preventDefault()
      @setState edit: !@state.edit
    ...

這個edit標志默認為false,而handleToggleedit由false變為true,亦可反向操作,我們僅需要從一個用戶onClick事件中觸發handleToggle

現在,我們需要處理兩個行記錄版本(只讀和表單)并且有條件地根據edit標志來顯示它們。幸運的是,只要render方法返回一個React元素,我們就可以在它里面隨意執行任何操作。我們可以定義recordRowrecordForm兩個helper方法,并在render里面,依賴于@state.edit的內容有條件地調用它們。

我們已經有了一個recordRow的初始化版本,就是我們現在的render方法。讓我們把render的內容移到新的recordRow方法里并添加一些額外的代碼給它:

  # app/assets/javascripts/components/record.js.coffee

  @Record = React.createClass
    ...
    recordRow: ->
      React.DOM.tr null,
        React.DOM.td null, @props.record.date
        React.DOM.td null, @props.record.title
        React.DOM.td null, amountFormat(@props.record.amount)
        React.DOM.td null,
          React.DOM.a
            className: 'btn btn-default'
            onClick: @handleToggle
            'Edit'
          React.DOM.a
            className: 'btn btn-danger'
            onClick: @handleDelete
            'Delete'
    ...

我們只加入了一個額外的React.DOM.a元素,用來監聽到onClick事件后調用handleToggle

接著,recordForm的實現采用類似結構,只是每個單元格用input來代替。我們打算為這些input用一個新的ref屬性來使其變得可存取。和這個組件不出來state一樣,這個新的屬性會讓我們的組件通過@refs讀出由用戶提供的數據。

  # app/assets/javascripts/components/record.js.coffee

  @Record = React.createClass
    ...
    recordForm: ->
      React.DOM.tr null,
        React.DOM.td null,
          React.DOM.input
            className: 'form-control'
            type: 'text'
            defaultValue: @props.record.date
            ref: 'date'
        React.DOM.td null,
          React.DOM.input
            className: 'form-control'
            type: 'text'
            defaultValue: @props.record.title
            ref: 'title'
        React.DOM.td null,
          React.DOM.input
            className: 'form-control'
            type: 'number'
            defaultValue: @props.record.amount
            ref: 'amount'
        React.DOM.td null,
          React.DOM.a
            className: 'btn btn-default'
            onClick: @handleEdit
            'Update'
          React.DOM.a
            className: 'btn btn-danger'
            onClick: @handleToggle
            'Cancel'
    ...

別害怕,這個方法看起來有點大,僅僅是因為我們用了類似HAML的語法。注意,當用戶點擊 Update 按鈕時我們調用@handleEdit,我們打算使用與實現刪除記錄功能類似的流程。

你有否注意到這些React.DOM.input的創建有什么不同嗎?我們使用defaultValue代替value來設置初始化 input 的值,這是因為:僅使用value而沒有onChange會終止創建只讀的 input

最后,render方法濃縮成下列代碼:

  # app/assets/javascripts/components/record.js.coffee

  @Record = React.createClass
    ...
    render: ->
      if @state.edit
        @recordForm()
      else
        @recordRow()

你可以刷新你的瀏覽器來看看新的切換效果,但不要提交任何改變,因為我們還沒實現實際的 update 功能。

edit_record_1
edit_record_1

edit_record_2
edit_record_2

要處理記錄的更新,我們需要添加update方法到我們的Rails控制器:

  # app/controllers/records_controller.rb

  class RecordsController < ApplicationController
    ...
    def update
      @record = Record.find(params[:id])
      if @record.update(record_params)
        render json: @record
      else
        render json: @record.errors, status: :unprocessable_entity
      end
    end
    ...
  end

回到我們的Record組件,我們需要實現handleEdit方法,它將會附帶要更新的record信息發送一個 AJAX 請求到服務器,然后由發送更新后版本的記錄數據通過handleEditRecord方法通知父組件,這個方法會通過@props被接收到,我們在實現刪除記錄時用過同樣的方法:

  # app/assets/javascripts/components/record.js.coffee

  @Record = React.createClass
    ...
    handleEdit: (e) ->
      e.preventDefault()
      data =
        title: this.refs.title.value
        date: this.refs.date.value
        amount: this.refs.amount.value
      # jQuery doesn't have a $.put shortcut method either
      $.ajax
        method: 'PUT'
        url: "/records/#{ @props.record.id }"
        dataType: 'JSON'
        data:
          record: data
        success: (data) =>
          @setState edit: false
          @props.handleEditRecord @props.record, data
    ...

為簡單起見,我們不校驗用戶數據,我們僅僅通過React.findDOMNode(@refs.fieldName).value讀取它,并且一字不差的把它發送給后端。在success時更新狀態來切換 edit 方式不是強制性的,但用戶會因此而明確地感謝我們。

最后但并非最不重要,我們僅需要更新Records組件上的state,用子組件的新版本記錄來覆蓋之前的舊記錄并讓React發揮它的魔力。實現的代碼如下:

  # app/assets/javascripts/components/records.js.coffee

  @Records = React.createClass
    ...
    updateRecord: (record, data) ->
      index = @state.records.indexOf record
      records = React.addons.update(@state.records, { $splice: [[index, 1, data]] })
      @replaceState records: records
    ...
    render: ->
      ...
      # almost at the bottom of the render method
      React.DOM.table
        React.DOM.thead null,
          React.DOM.tr null,
            React.DOM.th null, 'Date'
            React.DOM.th null, 'Title'
            React.DOM.th null, 'Amount'
            React.DOM.th null, 'Actions'
        React.DOM.tbody null,
          for record in @state.records
            React.createElement Record, key: record.id, record: record, handleDeleteRecord: @deleteRecord, handleEditRecord: @updateRecord

和我們從上一章學到的一樣,使用React.addons.update來改變我們的 state 會產生更穩固的方法。RecordsRecord之間最后的聯接是通過handleEditRecord屬性來發布方法@updateRecord。

最后一次刷新瀏覽器并嘗試更新一些已有的記錄,注意頁面頂端的金額框如何與你改變的每個記錄關聯。

edit_record_3
edit_record_3

搞定了!我們剛剛一步步地構建了一個小型的 Rails + React 的應用程序!

你可以到這里去看看本章的結果代碼,或者僅僅到這里看看由本章引入的改變。

結尾的思考:React.js,簡潔又靈活

我們已經驗證了一些React的功能,而且我們還學到了幾乎所有它引入的新概念。我聽到人們評論這個或那個的JavaScript框架因引入新概念而使其學習曲線變得陡峭,但React不是這樣的。它實現了例如事件處理和綁定等核心JavaScript概念,使其易于使用和學習。再次證明,其優勢之一就是簡潔。

通過實例,我們也學到了如何使其集成到Rails的assets pipeline,而且也能很好的與CoffeeScript、jQuery、Turbolinks及Rails的其余部分協同工作。但是,這并非是想要獲取結果的唯一方式。例如,你不想使用Turbolinks(因此你不需要react_ujs),你能用Rails Assets來代替react_rails這個gem,你可以使用Jbuilder來構造更復雜的JSON響應來代替提供的JSON對象,等等。你仍然會得到同樣不錯的效果。

React將明顯地提升你的前端能力,讓它成為你Rails工具箱中一個強大的庫吧!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 深入JSX date:20170412筆記原文其實JSX是React.createElement(componen...
    gaoer1938閱讀 8,106評論 2 35
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,447評論 25 708
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,973評論 19 139
  • Nothing special today. I have many things to do. I need t...
    Carolin大C閱讀 219評論 1 0
  • 參加了一場婚禮,過兩天又要參加下一場。 (1) 人是喜歡熱鬧的,無論是城里新潮的人們,還是保留傳統的農村人,婚禮理...
    王久立閱讀 436評論 1 2