使用ReactJS作為Backbone的view實現

行者武松發表於2017-06-06

在 Venmo(電子商務公司)公司,我們開始將我們的前端重新設計並重寫成一個清晰的,純 Backbone 模式的程式碼結構。Backbone 是一個編碼模式的靈活的框架(也就是 MVC 框架)。但是它的檢視層最少被設計,只為檢視層提供了少量的生命週期鉤子函式。和 Ember.js 的元件和 AngularJS 的指令相比,它缺少很多將資料和檢視層相關聯的鉤子函式,也沒有起到層與層之前的分離的作用。

為此我們很吃驚,於是我們不僅使用 Backone 檢視,也在開始探索編寫我們 UI 的更高階的選擇,我的目標是找到一個即能和 Backones 檢視層互動的,又能在一個大型框架裡將檢視寫資料繫結和限定作用域的框架。幸運的是,我的朋友 Scott 介紹了 React 給我,經過幾個小時的折騰之後,React 給了我一個很深刻的印象。

概述

React 是 Facebook 用來建立隔離化元件的一個很新穎的 JavaSript 庫。在很多方面,它和 Angular directives 或者 Polymer web 元件很相似。React 元件本質是是一個帶有作用域的自定義 DOM 元素。不管是在 JavaScript 中還是在 DOM 中,它都不能和你的應用程式狀態的其它部分進行直接互動。

React 最獨特也是最有爭議的部分是使用 JSX,JSX 把內嵌 JavaScript 程式碼中的 HTML 轉換成可以解析的 JavaScript 程式碼。這是一個 React 元件,使用 JSX 來渲染 a 標籤 :

/** @jsx React.DOM */
var component = React.createClass({
  render: function() {
    return <a href="http://venmo.com">Venmo</a>
  }
});

轉換後的 JavaScript 程式碼:

/** @jsx React.DOM */
var component = React.createClass({
  render: function() {
    return React.DOM.a( {href:"http://venmo.com"}, "Venmo")
  }
});

由於有大量的編譯 / 轉換工具,整合 JSX 到你的工作流中是很容易的:

require-jsx,是一個用來載入 JSX 檔案的 RequireJS 外掛

reactify 是一個 JSX 檔案的 Browserify 轉換工具

grunt-react 使用 Grunt 編譯 JSX 檔案

JSX 是可以選的 – 如果你願意,你可以使用 React.DOM DSL 來寫你的模板, 儘管我僅在模板很簡單時才推薦這麼幹。

在這篇文章中我不會教 React 的基礎知識, 你可以參考 excellent React tutorial 來學習. 那篇文章有點長,不過在繼續往下看之前還是有必要瀏覽一下的!

從 Backbone 檢視中渲染元件

讓我們建立一個非常基礎的元件:一個連線,當點選的時候能觸發點選事件處理函式。我們想要把這個元件作為 Backbone 檢視的一部分進行渲染,而不是單獨使用它。這個元件很容易建立:

var MyWidget = React.createClass({
  handleClick: function() {
    alert(`Hello!`);
  },
  render: function() {
    return (
      <a href="#" onClick={this.handleClick}>Do something!</a>
    );
  }
});

這段程式碼和上個例子的程式碼幾乎一樣,除了增加了 handleClick 函式來響應點選事件。現在我們唯一需要做的就是在 Backbone 檢視中渲染它:

var MyView = Backbone.View.extend({
  el: `body`,
  template: `<div class="widget-container"></div>`,
  render: function() {
    this.$el.html(this.template);
    React.renderComponent(new MyWidget(), this.$(`.widget-container`).get(0));
    return this;
  }
});
new MyView().render();

這就是我們完整的新元件:

/** @jsx React.DOM */
/* global React, Backbone, $ */
var MyWidget = React.createClass({
  handleClick: function() {
    alert(`Hello!`);
  },
  render: function() {
    return (
      <a href="#" onClick={this.handleClick}>Do something!</a>
    );
  }
});

元件 -> Backbone 通訊

當然,要想真正利用 React 元件的優勢,我們需要將 React 元件的變化通知到 Backbone。隨便舉個例子,假設當元件裡的連結被點選的時候顯示一些文字。不止如此,我們還想讓文字位於元件的 DOM元素之外。

關於不同的元件之間如何通訊,React 文件已經解釋得很好了,但是沒有很明顯地說明子元件如何跟Backbone 父檢視互動。不過,有個簡單容易的方式通訊:我們可以通過元件的屬性繫結一個事件處理器。

這裡有個例子程式,JSX 程式碼:

function anEventHandler() { ... }
React.RenderComponent(<MyComponent customHandler={anEventHandler} />,
                      this.$(`body`).get(0));

我們可以在 Backbone view 裡做同樣的事,但我們直接用 class,不使用 JSX:

var MyView = Backbone.View.extend({
  el: `body`,
  template: `<div class="widget-container"></div>` +
            `<div class="outside-container"></div>`,
  render: function() {
    this.$el.html(this.template);
    React.renderComponent(new MyWidget({
      handleClick: this.clickHandler.bind(this)
    }), this.$(`.widget-container`).get(0));
    return this;
  },
  clickHandler: function() {
    this.$(".outside-container").html("The link was clicked!");
  }
});

另外,在元件內部繫結 onClick 到事件處理器:

var MyWidget = React.createClass({
  render: function() {
    return (
      <a href="#" onClick={this.props.handleClick}>Do something!</a>
    );
  }
});

這裡是更新過的完整的例子:

/** @jsx React.DOM */
/* global React, Backbone, $ */
var MyWidget = React.createClass({
  render: function() {
    return (
      <a href="#" onClick={this.props.handleClick}>Do something!</a>
    );
  }
});
var MyView = Backbone.View.extend({
  el: `body`,
  template: `<div class="widget-container"></div>` +
            `<div class="outside-container"></div>`,
  render: function() {
    this.$el.html(this.template);
    React.renderComponent(new MyWidget({
      handleClick: this.clickHandler.bind(this)
    }), this.$(`.widget-container`).get(0));
    return this;
  },
  clickHandler: function() {
    this.$(".outside-container").html("The link was clicked!");
  }
});
new MyView().render();

再次說明,這是一個不太自然的例子,但是想象出一個更加實用的用例應該不難。

比如,在 Venmo 我們用 React 重新開發了“使用 Facebook 登入”按鈕。實際的 Facebook API 呼叫發生在元件內部,但是元件所在的 view 針對不同的事件繫結了不同的處理函式。這些事件(比如“Facebook 認證通過”或者“ Facebook 已登出”)本質上是元件的“公共API”。React 也可以在事件中傳遞引數,以便在使用者連線到 Facebook 的時候,Backbone view 可以獲取到使用者的 Facebook ID,同時把它附加到 user model 上。

Backbone與reactjs 元件通訊

現在,我們知道了元件通訊了,下一步,我們用 Backbone 模型來更新 reactjs 元件的狀態,來作為一個例子,我們會讓改變的模型欄位反應到檢視上面.這需要定義一些模板(雖然這比 Backbone 手動繫結檢視強不了多少),但是在實踐當中還是相當容易的。

首先: 我們建立一個小模型類:

var ExampleModel = Backbone.Model.extend({
  defaults: {
    name: `Backbone.View`
  }
});

然後用一個簡單的 React 元件顯示 name 欄位:

var DisplayView = React.createClass({
  render: function() {
    return (
      <p>
        {this.props.model.get(`name`)}
      </p>
    );
  }
});

儘管這個元件不能自已反應模型欄位的更改,但是我們可以為模型加一個 change 監聽事件,來告訴元件去重新渲染:

var DisplayView = React.createClass({
  componentDidMount: function() {
    this.props.model.on(`change`, function() {
      this.forceUpdate();
    }.bind(this));
  },
  render: function() {
    // ...
  }
});

然後我們增加另一個元件改變 name 欄位:

var ToggleView = React.createClass({
  handleClick: function() {
    this.props.model.set(`name`, `React`);
  },
  render: function() {
    return (
      <button onClick={this.handleClick}>
        model.set(`name`, `React`);
      </button>
    );
  }
});

最後,我們建立一個模型,然後用 JSX 來渲染兩個元件:

var model = new ExampleModel();
React.renderComponent((
  <div>
    <DisplayView model={model} />
    <ToggleView model={model} />
  </div>
), document.body);

例子到此結束, 完整的例子可以到這裡觀看http://runjs.cn/detail/eqituk3o

結論

Backbone + React 是一個神奇的組合。有其他幾個部落格有這樣的討論帖子:

Joel Burget 討論 Khan Academy(可汗學院)對 React 的移植

Clay Allsopp 寫的 Propeller 移植到 React 的帖子,包括一個非常酷的 React Mixin 被繫結到Backbone Model/Collection changes ,類似我們上面做的最後一個例子。

Paul Seiffert (require-jsx的作者)有另外一篇文章是關於用 React 取代 Backbone 的檢視

React 也有一個 Backbone+React 的例子 TodoMVC app 值得試試。

當 React 還不成熟的時候,它是一個非常令人興奮的庫,看起來好像是可供生產使用的一個庫了。它很容易分享和重用元件,我最感興趣的是它與 Backbone 整合地如此之好,彌補了預設的檢視邏輯。

文章轉載自 開源中國社群[https://www.oschina.net]


相關文章