--- id: tutorial-zh-CN title: 教程 prev: getting-started-zh-CN.html next: thinking-in-react-zh-CN.html --- 我们将建立一个你可以放进博客的简单却真实的评论框,一个 Disqus、LiveFyre 或 Facebook comments 提供的实时评论的基础版本。 我们将提供: * 一个所有评论的视图 * 一个用于提交评论的表单 * 为你提供制定后台的挂钩(Hooks) 同时也会有一些简洁的功能: * **优化的评论:** 评论在它们保存到服务器之前就显示在列表里,所以感觉很快。 * **实时更新:** 其他用户的评论被实时浮现到评论中。 * **Markdown格式化:** 用户可以用Markdown格式化它们的文字。 ### 想要跳过所有内容,只查看源代码? [全在 GitHub .](https://github.com/reactjs/react-tutorial) ### 运行服务器 为了开始本教程,我们将要需要一个运行着的服务器。这将是我们纯粹用来获取和保存数据的伺服终端。为了让这尽可能的容易,我们已经用许多不同的语言编写了简单的服务器,它正好完成我们需要的事。 **你可以[查看源代码](https://github.com/reactjs/react-tutorial/) 或者 [下载 zip 文件](https://github.com/reactjs/react-tutorial/archive/master.zip) 包括了所有你开始学习需要的东西** 为了简单起见,我们将要运行的服务器使用 `JSON` 文件作为数据库。你不会在生产环境运行这个,但是它让我们更容易模拟使用一个API时你可能会做的事。一旦你启动服务器,它将会支持我们的API终端,同时也将伺服我们需要的静态页面。 ### 开始 对于此教程,我们将使它尽可能的容易。被包括在上面讨论的服务器包里的是一个我们将在其中工作的 HTML 文件。在你最喜欢的编辑器里打开 `public/index.html`。它应该看起来像这样 (可能有一些小的不同,稍后我们将添加一个额外的 `
``` 在本教程剩余的部分,我们将在此 script 标签中编写我们的 JavaScript 代码。我们没有任何高级的实时加载所以在保存以后你需要刷新你的浏览器来观察更新。通过在浏览器打开 `http://localhost:3000` 关注你的进展。当你没有任何修改第一次加载时,你将看到我们将要准备建立的已经完成的产品。当你准备开始工作,请删除前面的 ` ``` 然后,让我们转换评论文本为 Markdown 并输出它: ```javascript{9} // tutorial6.js var Comment = React.createClass({ render: function() { return (

{this.props.author}

{marked(this.props.children.toString())}
); } }); ``` 我们在这里唯一做的就是调用 marked 库。我们需要把 从 React 的包裹文本来的 `this.props.children` 转换成 marked 能理解的原始字符串,所以我们显示地调用了`toString()`。 但是这里有一个问题!我们渲染的评论在浏览器里看起来像这样: "`

`This is ``another`` comment`

`" 。我们想让这些标签真正地渲染为 HTML。 那是 React 在保护你免受 [XSS 攻击](https://en.wikipedia.org/wiki/Cross-site_scripting)。有一个方法解决这个问题,但是框架会警告你别使用这种方法: ```javascript{4,10} // tutorial7.js var Comment = React.createClass({ rawMarkup: function() { var rawMarkup = marked(this.props.children.toString(), {sanitize: true}); return { __html: rawMarkup }; }, render: function() { return (

{this.props.author}

); } }); ``` 这是一个特殊的 API,故意让插入原始的 HTML 变得困难,但是对于 marked 我们将利用这个后门。 **记住:** 使用这个功能你会依赖于 marked 是安全的。既然如此,我们传递 `sanitize: true` 告诉 marked escape 源码里任何的 HTML 标记,而不是直接不变的让他们通过。 ### 挂钩数据模型 到目前为止我们已经完成了在源码里直接插入评论。作为替代,让我们渲染一团 JSON 数据到评论列表里。最终数据将会来自服务器,但是现在,写在你的源代码中: ```javascript // tutorial8.js var data = [ {author: "Pete Hunt", text: "This is one comment"}, {author: "Jordan Walke", text: "This is *another* comment"} ]; ``` 我们需要以一种模块化的方式将这个数据传入到 `CommentList`。修改 `CommentBox` 和 `React.render()` 方法,以通过 props 传入数据到 `CommentList`: ```javascript{7,15} // tutorial9.js var CommentBox = React.createClass({ render: function() { return (

Comments

); } }); React.render( , document.getElementById('content') ); ``` 既然现在数据在 `CommentList` 中可用了,让我们动态地渲染评论: ```javascript{4-10,13} // tutorial10.js var CommentList = React.createClass({ render: function() { var commentNodes = this.props.data.map(function (comment) { return ( {comment.text} ); }); return (
{commentNodes}
); } }); ``` 就是这样! ### 从服务器获取数据 让我们用一些来自服务器的动态数据替换硬编码的数据。我们将移除数据的prop,用获取数据的URL来替换它: ```javascript{3} // tutorial11.js React.render( , document.getElementById('content') ); ``` 这个组件不同于和前面的组件,因为它必须重新渲染自己。该组件将不会有任何数据,直到请求从服务器返回,此时该组件或许需要渲染一些新的评论。 注意: 此代码在这个阶段不会工作。 ### Reactive state 迄今为止,基于它自己的props,每个组件都渲染了自己一次。`props` 是不可变的:它们从父级传来并被父级“拥有”。为了实现交互,我们给组件引进了可变的 **state**。`this.state` 是组件私有的,可以通过调用 `this.setState()` 改变它。每当state更新,组件就重新渲染自己。 `render()` 方法被声明为一个带有 `this.props` 和 `this.state` 的函数。框架保证了 UI 总是与输入一致。 当服务器获取数据时,我们将会改变我们已有的评论数据。让我们给 `CommentBox` 组件添加一组评论数据作为它的状态: ```javascript{3-5,10} // tutorial12.js var CommentBox = React.createClass({ getInitialState: function() { return {data: []}; }, render: function() { return (

Comments

); } }); ``` `getInitialState()` 在生命周期里只执行一次,并设置组件的初始状态。 #### 更新状态 当组件第一次创建时,我们想从服务器获取一些 JSON 并且更新状态以反映最新的数据。我们将用 jQuery 来发送一个异步请求到我们刚才启动的服务器以获取我们需要的数据。看起来像这样: ```json [ {"author": "Pete Hunt", "text": "This is one comment"}, {"author": "Jordan Walke", "text": "This is *another* comment"} ] ``` ```javascript{6-18} // 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.error(this.props.url, status, err.toString()); }.bind(this) }); }, render: function() { return (

Comments

); } }); ``` 这里, `componentDidMount` 是一个当组件被渲染时被React自动调用的方法。动态更新的关键是对 `this.setState()` 的调用。我们用新的从服务器来的替换掉旧的评论组,然后UI自动更新自己。因为这种反应性,仅是一个微小的变化就添加了实时更新。我们这里将用简单的轮询,但是你可以容易的使用 WebSockets 或者其他技术。 ```javascript{3,15,20-21,35} // 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.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 (

Comments

); } }); React.render( , document.getElementById('content') ); ``` 我们在这里做的全部事情是把 AJAX 调用移动到独立的方法里,然后在组件第一次加载时及其后每2秒 调用它。试着在你的浏览器里运行它并且改变 `comments.json` 文件(在你的服务器的相同目录);2秒内,变化将会显现! ### 添加新评论 现在是时候建立表单了,我们的 `CommentForm` 组件应该询问用户他们的名字和评论文本然后发送一个请求到服务器来保存评论. ```javascript{5-9} // tutorial15.js var CommentForm = React.createClass({ render: function() { return (
); } }); ``` 让我们做一个交互式的表单。当用户提交表单时,我们应该清空它,提交一个请求给服务器,和刷新评论列表。要开始,让我们监听表单的提交事件并清空它。 ```javascript{3-14,16-19} // tutorial16.js var CommentForm = React.createClass({ handleSubmit: function(e) { e.preventDefault(); var author = React.findDOMNode(this.refs.author).value.trim(); var text = React.findDOMNode(this.refs.text).value.trim(); if (!text || !author) { return; } // TODO: send request to the server React.findDOMNode(this.refs.author).value = ''; React.findDOMNode(this.refs.text).value = ''; return; }, render: function() { return (
); } }); ``` ##### 事件 React使用驼峰命名规范(camelCase)给组件绑定事件处理器。我们给表单绑定一个`onSubmit`处理器,它在表单提交了合法的输入后清空表单字段。 在事件中调用`preventDefault()`来阻止浏览器提交表单的默认行为。 ##### Refs 我们利用`ref`属性给子组件赋予名字,`this.refs`-引用组件。我们可以在组件上调用 `React.findDOMNode(component)` 获取原生的浏览器DOM元素。 ##### 回调函数作为属性 当用户提交评论时,我们需要刷新评论列表来包含这条新评论。在`CommentBox`中完成所有逻辑是有道理的,因为`CommentBox` 拥有代表了评论列表的状态(state)。 我们需要从子组件传回数据到它的父组件。我们在父组件的`render`方法中以传递一个新的回调函数(`handleCommentSubmit`)到子组件完成这件事,绑定它到子组件的 `onCommentSubmit` 事件上。无论事件什么时候触发,回调函数都将被调用: ```javascript{16-18,31} // tutorial17.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) { // 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 (

Comments

); } }); ``` 当用户提交表单时,让我们从 `CommentForm` 调用这个回调函数: ```javascript{10} // tutorial18.js var CommentForm = React.createClass({ handleSubmit: function(e) { e.preventDefault(); var author = React.findDOMNode(this.refs.author).value.trim(); var text = React.findDOMNode(this.refs.text).value.trim(); if (!text || !author) { return; } this.props.onCommentSubmit({author: author, text: text}); React.findDOMNode(this.refs.author).value = ''; React.findDOMNode(this.refs.text).value = ''; return; }, render: function() { return (
); } }); ``` 既然现在回调函数已经就绪,我们所需要做的就是提交到服务器然后刷新列表: ```javascript{17-28} // tutorial19.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 (

Comments

); } }); ``` ### 优化: 优化的更新 我们的应用现在已经功能完备,但是它感觉很慢,因为在评论出现在列表前必须等待请求完成。我们可以优化添加这条评论到列表以使应用感觉更快。 ```javascript{17-19} // 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) { var comments = this.state.data; 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 (

Comments

); } }); ``` ### 祝贺! 你刚刚通过几个简单的步骤建立了一个评论框。学习更多关于[为什么使用 React](/react/docs/why-react-zh-CN.html), 或者深入 [API 参考](/react/docs/top-level-api.html) 开始钻研!祝你好运!