使用 ReactJS 作为 Backbone 的 view 实现

jopen 8年前

使用 ReactJS 作为 Backbone 的 view 实现

在 Venmo(电子商务公司)公司,我们开始将我们的前端重新设计并重写成一个清晰的,纯 Backbone  模式的代码结构。Backbone  是一个编码模式的灵活的框架(也就是 MVC 框架)。但是它的视图层最少被设计,只为视图层提供了少量的生命周期钩子函数。和 Ember.js 的组件和 AngularJS 的指令相比,它缺少很多将数据和视图层相关联的钩子函数,也没有起到层与层之前的分离的作用。

为此我们很吃惊,于是我们不仅使用 Backone 视图,也在开始探索编写我们 UI 的更高级的选择,我的目标是找到一个即能和 Backones 视图层交互的,又能在一个大型框架里将视图写数据绑定和限定作用域的框架。幸运的是,我的朋友 Scott 介绍了 React 给我,经过几个小时的折腾之后,React 给了我一个很深刻的印象。

概述

React 是 非死book 用来创建隔离化组件的一个很新颖的 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 重新开发了“使用 非死book 登录”按钮。实际的 非死book API 调用发生在组件内部,但是组件所在的 view 针对不同的事件绑定了不同的处理函数。这些事件(比如“非死book 认证通过”或者“ 非死book 已登出”)本质上是组件的“公共API”。React 也可以在事件中传递参数,以便在用户连接到 非死book 的时候,Backbone view 可以获取到用户的 非死book 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 是一个神奇的组合。有其他几个博客有这样的讨论帖子:

React 也有一个 Backbone+React 的例子 TodoMVC app 值得试试。

当 React 还不成熟的时候,它是一个非常令人兴奋的库,看起来好像是可供生产使用的一个库了。它很容易分享和重用组件,我最感兴趣的是它与 Backbone 集成地如此之好,弥补了默认的视图逻辑。