用React编写组件

去年年底非常热门的前端议题就是facebook的开源库react.js,还有尚未开源的react native也带来了一丝神秘感,下面就简单说下用react写组件的过程和心得。
如果你不想听我啰嗦,直接看示例看源码就点击下面

demo。文章大体是根据facebook的文档翻译过来的,英文水平有限,有点捉急,另外写了一个tab组件

开始

用你最喜欢的编辑器创建一个新的html文件,代码如下:

<html>
  <head>
    <title>苟富贵</title>
    <script src="https://fb.me/react-0.13.0.js"></script>
    <script src="https://fb.me/JSXTransformer-0.13.0.js"></script>
    <script src="https://code.jquery.com/jquery-1.10.0.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/jsx">
      // 在这里写你的代码
    </script>
  </body>
</html>

组件的结构

组件的结构如下:CommentBox是整体组件,分为回复列表(CommentList)和表单(CommentForm),回复列表(CommentList)包含回复项(Comment)。

- CommentBox
  - CommentList
    - Comment
  - CommentForm

先来编写第一个组件:

var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        Hello, world! I am a CommentBox.
      </div>
    );
  }
});
React.render(
  <CommentBox />,
  document.getElementById('content')
);

需要注意的是上面这段代码写在<script type="text/jsx">标签里面,JSXTransformer会将里面的代码转换为正常的javascript代码。可以在线转换,也可以离线做好转换。html和javascript代码混合着写UI层,乍看觉得变扭,实则带来了便利,在编码中能够体会得到。

在创建新组件时,调用的是React.createClass()方法,传入一个javascript对象,最重要的一个是render方法,返回的React组件最终渲染为HTML。render方法里面的<div>标签不是真正的DOM节点,而是React的div组件的实例。由于React的渲染过程不是HTML字符串的拼接,默认就避免了XSS漏洞,因此React是安全的。

因为class在javascript中保留的关键字,所有样式的名称必须赋值给className

Return返回的内容也不一定得是最基本的HTML标签,还可以是现成的React组件,组件之间可以随意组合。组件可组合是前端的一条重要原则。

React.render()实例化根组件,框架的入口函数。

组合组件

接下来新建两个组件CommentListCommentForm,从最简单的div开始

var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        Hello, world! I am a CommentList.
      </div>
    );
  }
});

var CommentForm = React.createClass({
  render: function() {
    return (
      <div className="commentForm">
        Hello, world! I am a CommentForm.
      </div>
    );
  }
});

然后框架更新CommentBox组件

var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList />
        <CommentForm />
      </div>
    );
  }
});

注意,现在HTML标签和React组件混合在一起构建。HTML组件和正规的React组件没什么区别,在JSX编译的时候,会调用React.createElement(tagName)写HTML,表达式等其他的东西会单独处理,也规避了全局变量的污染。

使用props

然后我们要创建Comment组件,组件的数据依赖父组件。通过父组件的property变量传递数据,这里有点类似DOM节点的属性。组件的值通过this.props取得父组件的值。

var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {this.props.children}
      </div>
    );
  }
});

author 是Comment组件定义的属性,这个值通常是外部传递进来的。{}花括号中可以包含javascript表达式,Comment定义的任何属性都会挂靠在this.props上。React还提供了this.props.children来访问子组件。来看下使用的实例:

var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        <Comment author="Pete Hunt">This is one comment</Comment>
        <Comment author="Jordan Walke">This is *another* comment</Comment>
      </div>
    );
  }
});

数据模型

下面的例子中,数据是直接写死的,通常我们的业务中数据是来自server端,也就是我们通过CGI请求去获取得到。假设返回的数据结构是一个json:

var data = [
  {author: "Pete Hunt", text: "This is one comment"},
  {author: "Jordan Walke", text: "This is *another* comment"}
];

现在我们要把组件和数据联系起来,CommentList是负责展示回复数据的组件,可以利用props来获取到data。改造下代码:

// tutorial10.js
var CommentList = React.createClass({
  render: function() {
    var commentNodes = this.props.data.map(function (comment) {
      return (
        <Comment author={comment.author}>
          {comment.text}
        </Comment>
      );
    });
    return (
      <div className="commentList">
        {commentNodes}
      </div>
    );
  }
});

通过this.props.data获取到父组件传进来的数据,所以data 是 CommentList组件的一个属性:

var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.props.data} />
        <CommentForm />
      </div>
    );
  }
});

React.render(
  <CommentBox data={data} />,
  document.getElementById('content')
);

这里可以看到data数据是CommentBox组件的属性,用this.props.data传递给CommentList组件。data指向前面定义好的json数据,渲染数据这块就基本做好了。

从服务端获取数据

现在换一种方式获取数据:

React.render(
  <CommentBox url="comments.php" />,
  document.getElementById('content')
);

定义一个url属性,通过该url获取回复的数据。设置一个定时查询,组件就可以自渲染,当有新的回复时,就能渲染新的回复,而且是增量更新,这个得益于React的diff算法,还没有仔细研究源码,有机会可以分析下。

组件状态

前面写的组件能够通过定义的props属性渲染一次,可props是不变的,他们是通过父组件传递过来的,数据是属于父组件。为了实现互动,React引入了一个可变的state状态。this.state对组件是私有的,通过this.setState()改变它的值。当state更新了,组件会自渲染。这样输入可以直接体现在UI的更新上。
下面改写下代码,给CommentBox组件加上状态:

var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: []};
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

getInitialState()在组件的生命周期内只会执行一次,就是初始化组件的状态值,这里设置了一个data空数组,然后可以调用this.state.data来获得当前的状态。

更新state

数据通过ajax异步从server获得,获取数据后通过this.setState()改变组件状态,下面继续改代码:

var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

componentDidMount是React内部定义的一个方法,在组件渲染的时候会自动调用一次。加一个定时更新的功能:

// tutorial14.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

React.render(
  <CommentBox url="comments.json" pollInterval={2000} />,
  document.getElementById('content')
);

回复组件

用户的输入,并且保存。让组件变得可交互,用户提交表单后,清空表单,提交到server,刷新回复列表;

var CommentForm = React.createClass({
  handleSubmit: function(e) {
    e.preventDefault();
    var author = this.refs.author.getDOMNode().value.trim();
    var text = this.refs.text.getDOMNode().value.trim();
    if (!text || !author) {
      return;
    }
    // TODO: send request to the server
    this.refs.author.getDOMNode().value = '';
    this.refs.text.getDOMNode().value = '';
    return;
  },
  render: function() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit}>
        <input type="text" placeholder="Your name" ref="author" />
        <input type="text" placeholder="Say something..." ref="text" />
        <input type="submit" value="Post" />
      </form>
    );
  }
});

通过onSubmit 属性绑定事件。值得注意的是Refs这个属性,做为子组件的一个name属性标记,通过this.refs.text.getDOMNode()获得浏览器原生的DOM节点。
还遗留了一点,提交请求到server,为了组件的通用性,最好的办法当然不要将数据提交放在这里。不过可以执行一个回调函数,关键是回调函数该怎么传递给组件呢?
答案是props属性。

// tutorial17.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  handleCommentSubmit: function(comment) {
    // TODO: submit to the server and refresh the list
  },
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </div>
    );
  }
});

CommentForm组件中调用

this.props.onCommentSubmit({author: author, text: text});

handleCommentSubmit 就是提交请求了

 $.ajax({
      url: this.props.url,
      dataType: 'json',
      type: 'POST',
      data: comment,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });

如果提交成功会执行this.setState({data: data});更新组件的状态,然后绘制UI。为了让用户感知的更快一点,不等到请求完成后更新,也可以直接更新UI,同时提交到server。

handleCommentSubmit: function(comment) {
    var comments = this.state.data;
    var newComments = comments.concat([comment]);
    this.setState({data: newComments});
    $.ajax({
     // .....
    })
}

恭喜,看到这里组件编写的过程基本完成。

总结

以下几点在学习的时候值得注意:

1、React 的API比较少也比较简单,但入门也并不轻松,主要是打破了一些固有思维。
2、主要理解state,props作为React的状态和属性等机制。
3、完全用不上事件代理,还是挺颠覆传统前端开发思维。
4、组件间的通信,前面看到了,可以通过callback,但如果再隔一级,或者平级的话,就不灵通了。复杂的情况甚至有多个组件。
5、不要把React当jQuery用,比如:$(‘xxx’).click(doSomething),这是典型的库思维。
6、不要把React当模板引擎用。

评论

  1. yyj 的头像
    yyj

    翻译的不错

回复 yyj 取消回复

您的邮箱地址不会被公开。 必填项已用 * 标注

更多文章