1. 1. 想要跳过这些直接看源码?
  2. 2. 启动服务器
  3. 3. 开始学习
  4. 4. 你的第一个组件
  5. 5. JSX语法
    1. 5.1. 发生了什么
  6. 6. 组装组件
  7. 7. 使用props
  8. 8. 组件属性
  9. 9. 添加Markdown
  10. 10. 与数据模型挂钩
  11. 11. 从服务器获取数据
  12. 12. 响应状态
  13. 13. 更新状态
  14. 14. 添加新的评论
    1. 14.1. 控制组件
    2. 14.2. 事件
      1. 14.2.1. 提交表单
    3. 14.3. 作为属性的回调函数
  15. 15. 优化:优化更新
  16. 16. 恭喜!

本文完全由博主自己翻译,几乎没有借助其他任何文章,仅仅作为自己入门React的切入点和学习的手段,有些内容的翻译可能会比较别扭,敬请原谅。后续还会继续调整部分翻译内容,以使得本文更加通顺

我们将构建一个由Disqus、LiveFyre或者Facebook Comment提供的,可以将之放入一个博客之中的,简单而真实的评论框。

我们将提供以下功能:

  • 所有评论的展示
  • 可以提交评论的表格
  • 为你提供自定义后端的钩子(Hooks)

同时还具有一些其他的优良特性:

  • 及时评论:在评论被保存到服务器之前就会展现在评论列表中,这样看起来更加快捷
  • 随时更新:其他用户的评论会及时进入评论
  • Markdown格式化:用户可以使用Markdown来格式化他们的文字

想要跳过这些直接看源码?

查看Github上所有源码。

启动服务器

为了能够开始学习此教程,我们需要启动一个服务器。启动服务器单纯地当做一个API的终端,我们将会使用这个终端来获取或者保存数据。为了让这个过程变得尽量简单,我们已经用多种语言建好了一个简单的服务器,做了我们需要他们所做的事情。你可以查看源码或者下载包含启动学习所需一切的压缩文件。

简单起见,服务器将会使用JSON文件作为数据库。在实际产品中不应当使用JSON文件,但是这可以轻松地模拟当你对接某个API时你所做的事情。启动服务器之后,就会支持我们的API终端,并且可以服务我们所需的静态文件。

开始学习

在这篇教程中,我们会尽可能的使其简化。我们上面讨论的服务包中有一个HTML文件,我们将会在这个文件中工作。用你喜欢的编辑器打开public/index.html文件,内容如下:

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>React Tutorial</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.2/react.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.2/react-dom.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/babel" src="scripts/example.js"></script>
    <script type="text/babel">
        // To get started with this tutorial running your own code, simply remove
        // the script tag loading scripts/example.js and start writing code here.
    </script>
  </body>
</html>

此教程接下来的部分中,我们将会在此script标签中写我们的JavaScript代码。我们没有什么高级的实时重载工具,所以保存文件之后,你需要刷新浏览器来查看更新内容。启动服务器后,在浏览器中打开http://localhost:3000以开始你的进程。在没有任何更改的情况下,首次打开时,你将会看到我们即将制作的完成后产品的。当你已经准备好开始工作时,就删掉前一个script标签然后再开始。

注意:
这里,我们引入了JQuery是因为我们为了简化我们的ajax调用代码,但是对于React的工作而言,这并非必须的。

你的第一个组件

React其实就是完全关于组件的,可组装的组件。例如,我们的评论框,将会拥有如下的组件结构:

- CommentBox
  - CommentList
  - Comment
- CommentForm

我们来创建CommentBox组件,仅仅是个简单的<div>

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

注意,原生的HTML元素名称都是以小写字母开头,而自定义的React class名字都是以大写字母开头。

JSX语法

你注意到的第一件事情就是在JavaScript中的XML风格的语法。我们有一个简单的预编译器,它会将语法糖翻译成原始的JavaScript代码。

// tutorial1-raw.js
var CommentBox = React.createClass({displayName: 'CommentBox',
   render: function() {
  return (
      React.createElement('div', {className: "commentBox"},
        "Hello, world! I am a CommentBox."
      )
  );
   }
});
ReactDOM.render(
    React.createElement(CommentBox, null),
    document.getElementById('content')
);

JSX的使用是可选项,但是我们认为JSX语法较原生JavaScrip语法而言,使用起来更加容易。阅读更多关于JSX语法的文章。

发生了什么

我们将一个JavaScript对象中的一些方法传入到了React.createClass()中,生成了一个新的React组件。这些方法中最重要的就是render方法,render方法返回了一个组件树,这个组件树最终将被渲染成为HTML。

上文中的<div>标签并非真实的DOM节点;他们只是React的<div>组件的实例。你可以认为他们是标记物或者,认为是React知道如何处理的数据碎片。React是安全的。我们并非生成HTML字符串所以XSS保护是默认的。

你无须返回基本的HTML。你可以返回你(或者别人)创建的组件树。这是使得React可组合的根本:可维护性前端的关键信条。

ReactDOM.render()将根组件实例化,启动框架,在原生DOM元素中植入标记物,这个原生DOM元素就是第二个参数。

ReactDOM模块暴露了DOM相关的方法,而React模块拥有着React在不同平台所共享的核心工具(例如,React Native)。

在这篇教程中,ReactDOM.render需要保留在script标签的底部,这一点非常重要.ReactDOM.render只能在可分解组件被定义之后调用。

组装组件

我们来创建CommentList和CommentForm的构架,他们也是简单的<div>组件。将这两个组件添加到你的文件当中,保留已经声明好的CommentBox组件和ReactDOM.render函数的调用:

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

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

接着,使用这两个新的组件更新CommentBox组件:

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

重点看一下我们是如何将HTML标签与我们创建的组件混合在一起的。HTML组件,就像你所定义的那些一样,是正常的React组件,但只有一点不同。JSX编译器将会自动重写HTML标签到React.createElement(tagName)表达式,其他的都是一样的。这样做的目的是防止全局命名空间被污染。

使用props

我们来建立Comment组件,这个组件依赖于传入其父元素的数据。从父级组件传入的数据,通过子组件的属性可以获得。这些属性可以通过this.props来获得。使用props,我们将获取从CommentList传入到Comment中的数据,并且渲染一些标记:

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

在JSX中,通过将JavaScript表达式包裹在大括号中的方法,你可以将text或者React组件拖入树中。XXXXX

组件属性

现在,我们已经定义好了Comment组件,我们希望给其传入作者名字和评论语言。这使得我们为每一个独立的comment复用同样的代码。现在我们在CommentList中添加一些评论:

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

注意,我们在父级组件CommentList中向子组件Comment传入了一些数据。例如,我们给第一个Comment组件传入了Pete Hunt(通过一个属性的方式)和This is one comment(通过一个像XML一样的子节点)。正如前面强调的,Comment组件将会通过this.props.author和this.props.children来获取这些属性。

添加Markdown

Markdown是一种格式化文字的简单的方法。例如,用星号来包裹文字会强调文字。

本篇教程中,我们将使用一个第三方的库marked,接受Markdown文字并将其转换为原生的HTML。我们已经将引入到了原始的标记中,便于我们直接使用。让我们将评论文字转化为Markdown并输出:

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

我们在这一步所做的仅仅是调用marked库。我们需要将this.props.children从React包裹的文字转化为原始的marked所理解的字符串,所以我们特意调用了toString()方法。

但是这里有一个问题!我们在浏览器中渲染出来的评论是这个样子的:“

This is another comment

”。我们希望这些标签真实地渲染成HTML。

这是因为React为了避免你受到XSS攻击。有一种方式可以迂回的解决它,但是框架警告你不要使用它:

//tutorial7.js
var Comment = React.createClass({
  rawMarkUp: function() {
      var rawMarkUp = marked(this.props.children.toString(), {sanitize: true});
      return {__html: rawMarkUp};
    },
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        <span dangerouselySetInnerHTML={this.rawMarkUp()} />
      </div>
    );
  }
});

这是一个特殊的API,故意使得插入原始的HTML变得困难,但是对于marked而言,我们正好利用了这个后门。

记住:通过你使用这个特点,你要确保依赖于marked。在这个例子中,我们将{sanitize: true}传入,以便于marked逃脱任何HTML标记而并非……

与数据模型挂钩

目前为止,我们直接将comment插入到源码中。相反,我们需要需要渲染一个JSON数据到commentlist中。这个数据最终来源于服务器,但是现在,直接在你的代码中书写:

//tutorial8.js
var data = [
 {id: 1, author: "Pete Hunt", text: "This is one comment"},
 {id: 2, author: "Jordan Walke"}, text="This is *another* comment"}
]

我们需要将这些数据以模块的方式传入到CommentList中。修改CommentBox和ReactDOM.render函数,通过props的方法来将数据传入到CommentList中:

//tutorial9.js
var CommentBox = React.createClass({
  render: function() {
    return (
            <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.props.data} />
            <CommentForm />
        </div>
          );
   }
});
ReactDOM.render(
  <CommentBox data={data} />,
  document.getElementById('content')
);

现在,数据在CommetList中就可以获取到了,我们来动态的渲染评论:

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

就是这样!

从服务器获取数据

接下来,我们将使用来自服务器的动态数据来替换手写代码方式的数据。我们将会删除数据这个属性,而使用URL来获取数据:

//tutotial11.js
ReactDOM.render(
  <CommentBox url="/api/comments" />,
  document.getElementById('content')
);

这个组件之所以与之前的组件不同是因为,它必须重新渲染自己。在从服务器返回请求之前,组件将不会拥有任何数据,只有当服务器返回数据的时候,组件才需要渲染一些新的评论。

注意:这一步代码将不会产生效果。

响应状态

目前,依赖于其属性,每个组件都自我渲染了一次。props是不可改变的:他们由父级组件传入,并且“属于”父级组件。为了实现交互,我们将会在组件中引入可变化的state。this.state是组件私有的,并且可以通过调用this.setState()方法来进行改变。当state更新时,组件就会自我渲染。

render()方法….。框架确保了用户界面永远与输入一致。

当服务器返回数据时,我们就会改变我们的评论数据。我们来添加一个评论数据的数组到CommentBox中,作为其状态:

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

getInitialState()方法仅仅在组件的生命周期中执行一次,并且设置了组件的初始状态。

更新状态

当组件第一次创建出来时,我们希望能够从服务器获得一些JSON,并且更新状态并反映最新的数据。我们将会使用JQuery来从服务器获取异步请求。数据已经包含在你之前启动的服务器中了(基于comments.js文件),所以一旦数据返回,this.state.data就会是如下的样子:

[
  {"id":"1", "author":"Pete Hunt", "text":"This is a comment"},
  {"id":"2", "author":"Jordan Walke", "text":"This is *another* comment"}
]

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

这里,componentDidMount是一个会被React自动调用的方法,当组件首次渲染的时候。动态更新的关键就是this.setState()的调用。我们用来自服务器的新的评论数组来替换旧的数组,UI就会自动更新。由于这样的更新,添加实时更新就会是一个非常小的变化了。这里,我们将会采用简单的轮询,但是你可以使用WebSockets或者其他的技术来轻松地改变它。

//tutorial14.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.log(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>
    );
  }
});

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

我们在这里所做的仅仅就是,将AJAX请求分离出来成为一个单独的方法,当组件首次载入的时候调用,并且之后每2秒钟调用一次。试着在你的浏览器中运行这段代码,然后改变comments.json文件(和你的服务器在同一个目录下);2秒之内,变化就会展示出来。

添加新的评论

现在,是时候构建表单了。我们的CommentForm组件应当询问用户的用户名和评论信息,并且向服务器发送一个请求以保存评论。

//tutorial15.js
var CommentForm = React.createClass({
  render: function() {
    return (
      <form className="commentForm">
        <input type="text" placeholder="Your name" />
        <input type="text" placeholder="Say something..." />
        <input type="submit" value="post" />
      </form>
    );
  }
});
控制组件

对于传统DOM而言,input元素会被渲染并且由浏览器来管理其状态(它的渲染值)。其结果就是真实DOM的状态和组件的状态不同。由于展示出来的状态和组件的状态不同,这并不是完美的。在React中,组件应当总是代表着界面的状态,而并非初始化时候的状态。

因此,我们将使用this.state来保存用户的输入值。我们定义一个拥有author和text两个属性的初始state,然后将其设置为空字符串。在我们的input元素中,我们设置value属性来反映组件的状态,并且添加onChange句柄给它。这些拥有value的input元素集合就被成为控制组件。阅读更多关于控制组件的内容请查看表单文章

//tutorial16.js
var CommentForm = React.createClass({
  getInitialState: function() {
    return {author: '', text: ''};
  },
  handleAuthorChange: function(e) {
    this.setState({author: e.target.value});
  },
  handleTextChange: function(e) {
    this.setState({text: e.target.value});
  },
  render: function() {
    return (
      <Form className="commentForm">
        <input
          type="text"
          placeholder="Your name"
          value={this.state.author}
          onChange={this.handleAuthorChange} 
        />
        <input
          type="text"
          placeholder="Say something..."
          value={this.state.text}
          onChange={this.handleTextChange}
        />
        <input type="submit" value="post" />
      </form>
    );
  }
});
事件

React通过传统的驼峰命名方式来给组件添加事件处理器。我们给两个input元素都添加了onChange处理器。现在只要用户在input区域中输入文字,添加在input上的onChange回调函数就会被触发,进而,组件的state就会被修改。紧接着,input元素的渲染值就会被更新以反映当前组件的state。

提交表单

让我们一起让表单产生交互。当用户提交表单时,我们会清空表单,给服务器提交一个请求,刷新评论列表。首先,我们需要监听表单的提交事件并清空表单。

//tutorial17.js
var CommentForm = React.createClass({
  getInitialState: function() {
    return {author: '', text: ''};
  },
  handleAuthorChange: function(e) {
    this.setState({author: e.target.value});
  },
  handleTextChange: function(e) {
    this.setState({text: e.target.value});
  },
  handleSubmit: function(e) {
    e.preventDefault();
    var author = this.state.author.trim();
    var text = this.state.text.trim();
    if(!author || !text) {
      return;
    }
    //TODO: send request to the server
    this.setState({author: '', text: ''});
  },
  render: function() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit}>
        <input
          type="text"
          placeholder="Your name"
          value={this.state.author}
          onChange={this.handleAuthorChange}
        />
        <input
          type="text"
          placeholder="Say something..."
          value={this.state.text}
          onChange={this.handleTextChange}              
        />
        <input type="submit" value="post">
      </form>
    );
  }
});

我们给表格添加了一个onSubmit的处理器,当表单提交有效输入内容时,处理器会清空表单。

在onSubmit事件中调用preventDefault()是为了阻止表单提交时浏览器的默认行为。

作为属性的回调函数

当用户提交一个评论时,我们需要刷新评论列表来将其包含在内。在CommentBox中来处理这些逻辑是讲得通的,因为CommentBox拥有代表这些评论列表的state。

我们需要从子组件中返回数据给父级组件。我们通过在父级组件的render方法中传入一个新的回调函数(handleCommentSubmit)到子组件,在子组件中的onCommentSubmit事件构建这件事请的方式,来处理这件事情,不论什么时候,当事件触发时,回调函数就会执行:

//tutorial18.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.log(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>
    );
  }
});

现在,通过onCommentSubmit方法,CommentBox使得回调函数对于CommentForm是可见的,当用户提交表单时,CommentForm可以调用回调函数:

//tutorial19.js
var CommentForm = React.createClass({
  getInitialState: function(e) {
    return {author: '', text: ''};
  },
  handleAuthorChange: function(e) {
    this.setState({author: e.target.value});
  },
  handleTextChange: function(e) {
    this.setState({text: e.target.value});
  },
  handleSubmit: function(e) {
    e.preventDefault();
    var author = this.state.author.trim();
    var text = this.state.text.trim();
    if (!author || !text) {
      return;
    }
    this.props.onCommentSubmit({author: author, text: text});
    this.setState({author: '', text: ''});
  },
  render: function() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit}>
        <input
            type="text"
          placeholder="Your name"
          value={this.state.author}
          onChange={this.handleAuthorChange}
        />
        <input
          type="text"
          placeholder="Say something..."
          value={this.state.text}
          onChange={this.handleTextChange}
        />
        <input type="submit" value="Post" />
  </form>
    );
  }
});

现在,回调函数已经就位,我们需要做的就仅仅是提交给服务器并且刷新评论列表:

//tutorial20.js
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
  $.ajax({
    url: this.props.url,
    dataType: 'json',
    cache: false,
    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) {
  $.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)
  });
},
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>
  );
 }
});

优化:优化更新

我们的应用目前功能已经完成了,但是在你的评论出现在列表之前,你必须等待请求完成,这似乎有点慢。我们可以自信地将评论添加到列表中,以使得应用看起来更快。

//tutorial21.js
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
  $.ajax({
    url: this.props.url,
    dataType: 'json',
    cache: false,
    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) {
  var comments = this.state.data;
  // Optimistically set an id on the new comment. It will be replaced by an
  // id generated by the server. In a production application you would likely
  // not use Date.now() for this and would have a more robust system in place.
  comment.id = Date.now();
  var newComments = comments.concat([comment]);
  this.setState({data: newComments});
  $.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)
  });
},
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>
  );
 }
});

恭喜!

刚才你通过几个步骤就建立了一个评论框。了解更多关于为什么使用React,或者直接一头扎入API 参考开始hacking!祝你幸运!