你為什么應該試一試Reflux?

jopen 9年前發布 | 43K 次閱讀 Reflux

 

一點背景

React在設計之初就只關注在View本身上,其余部分如 數據的獲取事件處理 等,全然不在考慮之內。不過構建大型的Web前端應用,這些點又實在不可避免。所以非死book的工程師提出了前端的 Flux 架構,這個架構的最大特點是 單向數據流 (后面詳述)。但是 Flux 本身的實現有很多不合理的地方,比如單例的Dispatcher會在系統中有多種事件時導致臃腫的 switch-cases 等。

這里是非死book官方提供的提供 Flux的結構圖

你為什么應該試一試Reflux?

其實整個Flux背后的思想也 不是什么新東西 。在很久之前,Win32的消息機制(以及很多的GUI系統)就在使用這個模型,而且這也是一種被證實可以用來構建大型軟件的模型。

鑒于Flux本身只是一個架構,而且非死book提供的參考實現又有一些問題,所以社區有了很多版本的Flux實現。比如我們這里會用到的 Reflux

Reflux簡介

簡而言之, Reflux 里有兩個組件:Store和Action。Store負責和數據相關的內容:從服務器上獲取數據,并更新與其綁定的React組件(view controller);Action是一個事件的集合。Action和Store通過convention來連接起來。

具體來說,一個典型的過程是:

  1. 用戶的動作(或者定時器)在組件上觸發一個Action
  2. Reflux會調用對應的Store上的callback(自動觸發)
  3. 這個callback在執行結束之后,會顯式的觸發(trigger)一個數據
  4. 對應的組件(可能是多個)的state會被更新
  5. React組件檢測到state的變化后,會自動重繪自身
  6. </ol>

    你為什么應該試一試Reflux?

    一個例子

    我們這里將使用React/Reflux開發一個實際的例子,從最簡單的功能開始,逐步將其構建為一個較為復雜的應用。

    這個應用是一個書簽展示應用(數據來源于我的Google Bookmarks)。第一個版本的界面是這樣的:

    你為什么應該試一試Reflux?

    要構建這樣一個列表應用,我們需要這樣幾個部分:

    1. 一個用來 fetch 數據,存儲數據的store (BookmarkStore)
    2. 一個用來表達事件的 Action (BookmarkActions)
    3. 一個列表組件(BookmarkList)
    4. 一個組件條目組件(Bookmark)
    5. </ol>

      定義Actions

      var Reflux = require('reflux');
      var BookmarkActions = Reflux.createActions([
        'fetch'
      ]);

      module.exports = BookmarkActions;</pre>

      第一個版本,我們只需要定義一個 fetch 事件即可。然后在 Store 中編寫這個Action的回調:

      定義Store

      var $ = require('jquery');
      var Reflux = require('reflux');
      var BookmarkActions = require('../actions/bookmark-actions');

      var Utils = require('../utils/fetch-client');

      var BookmarkStore = Reflux.createStore({ listenables: [BookmarkActions],

      init: function() { this.onFetch(); },

      onFetch: function() { var self = this; Utils.fetch('/bookmarks').then(function(bookmarks) { self.trigger({ data: bookmarks, match: '' }); }); } });

      module.exports = BookmarkStore;</pre>

      此處,我們使用 listenables: [BookmarkActions] 來將Store和Action關聯起來,根據 conventionon +事件名稱就是回調函數的名稱。這樣當 Action 被觸發的時候, onFetch 會被調用。當獲取到數據之后,這里會顯式的 trigger 一個數據。

      List組件

      var React = require('react');
      var Reflux = require('reflux');

      var BookmarkStore = require('../stores/bookmark-store.js'); var Bookmark = require('./bookmark.js');

      var BookmarkList = React.createClass({ mixins: [Reflux.connect(BookmarkStore, 'bookmarks')],

      getInitialState: function() { return { bookmarks: {data: []} } },

      render: function() { var list = []; this.state.bookmarks.data.forEach(function(item) { list.push(<Bookmark title={item.title} created={item.created}/>) });

        return <ul>
            {list}
        </ul>
      

      } });

      module.exports = BookmarkList;</pre>

      在組件中,我們通過 mixins: [Reflux.connect(BookmarkStore, 'bookmarks')] 將Store和組件關聯起來,這樣List組件 state 上的 bookmarks 就和 BookmarkStore 連接起來了。當 state.bookmarks 變化之后, render 方法就會被自動調用。

      對于每一個書簽,就只是簡單的展示內容即可:

      Bookmark組件

      var React = require('react');
      var Reflux = require('reflux');
      var moment = require('moment');

      var Bookmark = React.createClass({

      render: function() { var created = new Date(this.props.created * 1000); var date = moment(created).format('YYYY-MM-DD');

        return <li>
            <div className='bookmark'>
                <h5 className='title'>{this.props.title}</h5>
                <span className='date'>Created @ {date}</span>
            </div>
        </li>;
      

      } });

      module.exports = Bookmark;</pre>

      這里我使用了 moment 庫將 unix timestamp 轉換為日期字符串,然后寫在頁面上。

      最后, Utils 只是一個對 jQuery 的包裝:

      var $ = require('jquery');
      var Promise = require('promise');

      module.exports = { fetch: function(url) { var promise = new Promise(function (resolve, reject) { $.get(url).done(resolve).fail(reject); });

          return promise;
      }
      

      }</pre>

      我們再來總結一下, BookmarkStore 在初始化的時候,顯式地調用了 onFetch ,這個動作會導致 BookmarkList 組件的 state 的更新,這個更新會導致 BookmarkList 的重繪, BookmarkList 會依次迭代所有的 Bookmark

      更復雜一些

      當然,Reflux的好處不僅僅是上面描述的這種單向數據流。當 StoreActions 以及具體的 組件 被解耦之后,構建大型的應用才能成為可能。我們來對上面的應用做一下擴展:我們為列表添加一個搜索功能。

      隨著用戶的鍵入,我們發送請求到服務器,將過濾后的數據渲染為新的列表。我們需要這樣幾個東西

      1. 一個SearchBox組件
      2. 一個新的 Actionsearch
      3. BookmarkStore 上的一個新方法 onSearch
      4. 組件 SearchBox 需要和 BookmarkActions 關聯起來
      5. </ol>

        為了讓用戶看到匹配的效果,我們需要將匹配到的關鍵字高亮起來,這樣我們需要在 Bookmark 組件中監聽 BookmarkStore ,當 BookmarkStore 發生變化之后,我們就可以即時修改書簽的 title 了。

        搜索框組件

        var React = require('react');
        var BookmarkActions = require('../actions/bookmark-actions');

        var SearchBox = React.createClass({ performSearch: function() { var keyword = this.refs.keyword.value; BookmarkActions.search(keyword); },

        render: function() { return <div className="search"> <input type='text' placeholder='type to search...' ref="keyword" onChange={this.performSearch} />
        </div> } });

        module.exports = SearchBox;</pre>

        BookmarkStore

        var $ = require('jquery');
        var Reflux = require('reflux');
        var BookmarkActions = require('../actions/bookmark-actions');

        var Utils = require('../utils/fetch-client');

        var BookmarkStore = Reflux.createStore({ listenables: [BookmarkActions],

        init: function() { this.onFetch(); },

        onFetch: function() { var self = this; Utils.fetch('/bookmarks').then(function(bookmarks) { self.trigger({ data: bookmarks, match: '' }); }); },

        onSearch: function(keyword) { var self = this;

          Utils.fetch('/bookmarks?keyword='+keyword).then(function(bookmarks) {
              self.trigger({
                  data: bookmarks,
                  match: keyword
              });
          });
        

        } });

        module.exports = BookmarkStore;</pre>

        我們在 BookmarkStore 中添加了 onSearch 方法,它會根據關鍵字來調用后臺API進行搜索,并將結果 trigger 出去。由于數據本身的結構并沒有變化(只是數量會由于過濾而變少),因此 BookmarkList 是無需修改的。

        書簽高亮

        當搜索匹配之后,我們可以將對應的關鍵字高亮起來,這時候我們需要修改 Bookmark 組件:

        var React = require('react');
        var Reflux = require('reflux');
        var moment = require('moment');

        var BookmarkStore = require('../stores/bookmark-store.js');

        var Bookmark = React.createClass({ mixins: [Reflux.listenTo(BookmarkStore, 'onMatch')],

        onMatch: function(data) {
            this.setState({
                match: data.match
            });
        },
        
        

        getInitialState: function() { return { match: '' } },

        render: function() { var created = new Date(this.props.created * 1000); var date = moment(created).format('YYYY-MM-DD');

          var title = this.props.title;
          if(this.state.match.length > 0) {
              title = <span
                  dangerouslySetInnerHTML={{
                    __html : this.props.title.replace(new RegExp('('+this.state.match+')', "gi"), '<span class="highlight">$1</span>')
                  }} />
          }
        
          return <li>
              <div className='bookmark'>
                  <h5 className='title'>{title}</h5>
                  <span className='date'>Created @ {date}</span>
              </div>
          </li>;
        

        } });

        module.exports = Bookmark;</pre>

        mixins: [Reflux.listenTo(BookmarkStore, 'onMatch')] 表示,我們需要監聽 BookmarkStore 的變化,當變化發生時,調用 OnMatch 方法。 OnMatch 會修改組件的 match 屬性,從而觸發 render

        render 中,我們替換關鍵字為 <span class="highlight">$keyword</span> ,這樣就可以達到預期的效果了:

        你為什么應該試一試Reflux?

        結論

        從上面的例子可以看到,我們從一開始就引入了Reflux。雖然第一個版本和React原生的寫法差異并不是很大,但是當加入 SearchBox 功能之后,需要修改的地方非常清晰:添加 Actions ,在對應的 Store 中添加callback,然后在組件中使用。這種方法不僅可以最大程度的使用 React 的長處(diff render),而且使得代碼邏輯變得較為清晰。

        隨著業務代碼的不斷增加,Reflux提供的方式確實可以在一定程度上控制代碼的復雜性和可讀性。

        完整的 代碼地址在這里

        其他參考


         本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
         轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
         本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!