前言

这是系列文章中的第二篇。我们正在构建单页面应用(SPA),随着我们的应用逐渐丰满,我们开始要 面对数据的处理,需要将数据处理逻辑和我们的展示逻辑分离开来,容器组件应运而生。

在第一篇文章中,我们构建了路由和视图。本文我们将探索容器组件的使用,它自己不创建视图, 而是为别的组件创建视图助力。相信你在开始之前就希望我先"放码过来",我肯定非常乐意: Github

我们将会在应用中引入数据部分,如果你熟悉任意一种组件设计或者MVC模式, 你应该会知道将视图和应用逻辑混淆在一起是不好的做法。换句话说, 当一个视图需要获取数据来渲染的时候,它最好不知道数据是从哪里来的, 它要做的仅仅是渲染和展示。

使用Ajax获取数据

先来看一种不好的做法,我们为之前的UserList组件添加获取数据的功能:

// JSX

// This is an example of tightly coupled view and data which we do not recommend

var UserList = React.createClass({
  getInitialState: function() {
    return {
      users: []
    }
  },

  componentDidMount: function() {
    var _this = this;
    $.get('/path/to/user-api').then(function(response) {
      _this.setState({users: response})
    });
  },

  render: function() {
    return (
      <ul className="user-list">
        {this.state.users.map(function(user) {
          return (
            <li key={user.id}>
              <Link to="{'/users/' + user.id}">{user.name}</Link>
            </li>
          );
        })}
      </ul>
    );
  }
});

注: 如果你需要一个对于这个组件的使用更加详细/入门的说明,请看这里

为什么说这个例子做法不理想?首先,我们违背了"逻辑行为"域"如何渲染视图"要分离的原则。

其实,使用getInititalState来初始化组件的state,以及在componentDidMount中使用 Ajax请求没有错(虽然我们应该讲实际的调用抽象出来到别的函数中).问题在于,我们将 将上面两件事情放在了同一个存储我们视图的组件中来做。这样的高耦合使我们的应用变得很不灵活。 试想,如果我们需要从别的地方获取用户列表呢?"获取用户列表"这个动作已经绑定到这个视图组件中, 使其变得很难复用。

第二个问题是,我们这里使用了jQuery的Ajax函数。我们肯定不能否认jQuery有很多不错的地方, 但大部分是用在处理DOM的渲染,而我们的React有它自己的处理方式。至于jQuery中的不是 处理DOM的部分例如Ajax,我们其实有很多更轻量的专门处理这件事情的第二选择。

其中一个选择是Axios,一个基于promise的 Ajax工具,和jQuery中的类promise的Ajax特性很相似,不信看下面例子:

// JSX

// jQuery
$.get('/path/to/user-api').then(function(response) { ... });

// Axios
axios.get('/path/to/user-api').then(function(response) { ... });

现在开始我们都将使用Axios来处理这种请求。其他类似的工具有 got, fetch, SuperAgent


Props和State

在我们开始区分Container(容器)和Presentational(展示)组件之前,我们 需要说明一下props和state.

props和state相同的地方在于它们都是React组件中的"数据".它们都可以从父组件中传进, 也可以传给子组件。 但是,父组件的props和state属性在传到子组件中之后, 都会变成子组件的props属性。

举个例子,我们从ComponentA传递一些props和state属性给ComponentB, ComponentA 的render方法可能如下:

// JSX

// ComponentA
render: function() {
  return <ComponentB foo={this.state.foo} bar={this.props.bar} />
}

即使foo是父亲中的"state"属性,它还是会成为ComponentB组件的"prop"属性。因此现在 ComponentB的props拥有foobar两个属性,下面来验证:

// JSX

// ComponentB
componentDidMount: function() {
  console.log(this.props.foo);
  console.log(this.props.bar);
}

在前面Fetching Data with Ajax的例子中,Ajax返回的数据赋值给了组件的state. 例子中的组件没有子组件,但你应该知道了,如果有的话也会成为子组件的props

如果要更好的理解state,请看React Documentation


将它们拆分开

在使用Ajax获取数据的例子中,我们有一个遗留的问题。我们的UserList组件可以工作, 但是它需要负责太多的事情。为了使逻辑更加清晰,我们将它拆为2个组件,每个负责不同的功能。 这两个组件分别称为容器组件展示组件,更生动一点,聪明组件哑巴组件

简单来说,容器组件负责获取数据,处理state的数据。state会被传递到展示组件中, 作为展示组件的props,展示组件只负责将数据渲染到视图。

我使用"聪明组件"和"哑巴组件"的说法来自社区。如果你以前曾经遇到过类似的说法而
不懂其中的意思那么现在你就可以知道它们其实与容器组件和展示组件讲的是同一个意思

展示组件(Presentational Components)

其实在前面的的例子中你已经见过展示组件了,思考UserList组件在 能够处理自己的state之前是长什么样子的:

// JSX

var UserList = React.createClass({
  render: function() {
    return (
      <ul className="user-list">
        {this.props.users.map(function(user) {
          return (
            <li key={user.id}>
              <Link to="{'/users/' + user.id}">{user.name}</Link>
            </li>
          );
        })}
      </ul>
    );
  }
});

它与之前的写法稍有不同,但它就是一个展示组件。其实这里作为展示组件不同的地方在于, 现在的UserList组件从props获取数据,然后构建list元素。

"哑巴组件"顾名思义,就是它只负责将传递给它的props进行展示, 而它并不知道这些数据是从哪里来的,它们也不知道怎么去维护state.

展示组件绝对不能自己去修改prop里的数据。实际上,任何的组件从父组件那里获取props, 都应该认为这些数据是不可变的只由上一级管理。虽然展示组件不能修改props的值, 但是它们可以在展示的时候对数据做一些格式化,例如将linux timestamp的值展示为我们可读的格式。

在react中,事件是与视图的属性如onClick绑定的。这里你也许就会产生疑问, 既然展示组件不应该修改props,这些绑定的事件又是怎么样去工作的? 为此,我们下面将会有整一个话题讨论事件的处理。

Iterations

我们通过循环去构建DOM结点的时候,每个结点的key属性都要是唯一的(相对于兄弟结点). 在本例子中只是DOM里面的<li>元素。

如果嵌套的return看起来不爽,可以考虑换一种写法,使用一个函数来返回其中一个item.

// JSX

var UserList = React.createClass({
  render: function() {
    return (
      <ul className="user-list">
        {this.props.users.map(this.createListItem)}
      </ul>
    );
  },

  createListItem: function(user) {
    return (
      <li key={user.id}>
        <Link to="{'/users/' + user.id}">{user.name}</Link>
      </li>
    );
  }
});

容器组件

容器组件基本上都是作为展示组件父组件的形式出现。总之,它们是作为展示组件和 整个应用其余部分(逻辑和数据等)的中间人。它们也被称为"聪明组件"因为它们可以感知整个应用。

因为容器和展示组件应该有不同的名字来区分,我们将这个称为UserListContainer来避免混淆。

// JSX

var React = require('react');
var axios = require('axios');
var UserList = require('../views/list-user');

var UserListContainer = React.createClass({
  getInitialState: function() {
    return {
      users: []
    }
  },

  componentDidMount: function() {
    var _this = this;
    axios.get('/path/to/user-api').then(function(response) {
      _this.setState({users: response.data})
    });
  },

  render: function() {
    return (<UserList users={this.state.users} />);
  }
});

module.exports = UserListContainer;

为了简单起见,我们在一般例子中都省略了`require()`和`module.exports`这些声明,
但是在这个例子中我们特意说明容器组件是需要引入展示组件,因为这是非常重要的依赖。
因此为了完整性我们干脆在这个例子中写明了所有的依赖。

容器组件和其他React组件的构建方法一样。它们也有同样的render方法,只是它们不会 自己去渲染任何东西,而是将展示组件渲染好的结果返回。

关于ES6箭头函数的一点注解: 你也许已经注意到了例子中使用的`var _this = this`这个
小技巧