===================
原创翻译,有不当的地方欢迎指出。转载请指明出处。谢谢!
教程
我们来创建一个简单实用可以放到你的博客里面评论框,Disqus、LiveFyre、Facebook提供了最简单版本的实时评论。
我们会提供:
* 所有评论的展示
* 提交评论的表单
* 提供用户后台的hooks
还会有一些的巧妙的特性:
* **优化的评论:**评论会在保存到服务器之前就展示到列表里面,所以看起来更快。
* **实时更新:**其他用户的评论会实时弹入评论列表里。
* **Markdown格式:**用户可以采用Markdown来格式化文本。
跳过直接查看源代码? #
启一个server #
为了开始本教程,我们先来启一个server。这仅作为我们获取和保存数据的API端。为了尽可能简单,我们已经用许多脚本语言搭建了简单的server,能满足我们的需求。查看资源或者下载zip包,里面包含了运行所需的所有文件
为了简单,在服务端用JSON
文件作为数据库。在生产环境不会这么用,但这样可以轻松模拟你对API的各种使用。只要启动server,它就能支持我们的API端,同时也支持静态页面。
启动项目#
对于本教程,怎么简单怎么来。在前面讨论的server包里包含了我们将要处理的HTML文件。在常用的编辑器中打开public/index.html
。如下所示:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>React Tutorial</title>
`<script src="https://npmcdn.com/[email protected]/dist/react.js">`</script>
`<script src="https://npmcdn.com/[email protected]/dist/react-dom.js">`</script>
`<script src="https://npmcdn.com/[email protected]/browser.min.js">`</script>
`<script src="https://npmcdn.com/[email protected]/dist/jquery.min.js">`</script>
`<script src="https://npmcdn.com/[email protected]/dist/remarkable.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>
接下来的教程,我们开始在脚本标签中编写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 className="commentBox">
Hello, world! I am a CommentBox.
</div>
);
}
});
ReactDOM.render(
<CommentBox />,
document.getElementById('content')
);
请注意,原生HTML元素名以小写字母开头,而自定义的React类名以大写字母开头。
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语法比纯JavaScript更好用。阅读更多JSX语法文章。
接下来#
我们将JavaScript对象的一些方法加到React.createClass()
上,来创建一个新的React组件。其中,最重要的方法是render
,它返回一个React组件树并最终将其渲染成HTML。
<div>
标签并不是实际的DOM节点;它是Reactdiv
组件的实例。你可以把它看作是React知道如何处理的标记或者数据片段。React是安全的。我们不生成HTML字符串,所以默认就有XSS防护。
你不用返回基础的HTML。只需要返回你(或者别人)创建的组件树就行。这就使得React是可组合的:可维护性前端开发的宗旨。
ReactDOM.render()
实例化根组件,启动框架,将标记注入作为第二个参数提供的原始DOM元素中。
ReactDOM
模块暴露了DOM的特定方法,而React
具有不同平台(比如React Native)上React共享的核心工具。
把ReactDOM.render
留到本教程脚本的最后是很重要的。ReactDOM.render
只能在复合组件被定义好之后才能调用。
受控组件#
让我们来构建CommentList
和CommentForm
的架子,再次使用简单的div
。把这两个组件放到你的文件中,保持现有CommentBox
的声明和ReactDOM.render
的调用:
// tutorial2.js
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
:
// 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表达式(作为属性或者child),你可以把文本或者React组件放到树里面。我们通过this.props
上的键来访问传递到组件的命名属性,通过this.props.children
来访问嵌套的元素。
组件特性#
现在我们定义好了Comment
组件,想要传给它作者姓名和评论文字。这允许我们对于每条评论复用代码。现在,让我们在CommentList
中添加一些评论:
// tutorial5.js
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>
);
}
});
注意,我们已经从父组件CommentList
传递了一些数据给子组件Comment
了。例如,我们传了_Pete Hunt_(通过属性)以及_This is one comment_(通过类XML的子节点)给第一个Comment
。如上所述,Comment
组件可以通过this.props.author
和this.props.children
来访问这些“属性”。
添加Markdown #
Markdown是一种格式化文本的简单方法。例如,星号里的文字会被强调。
在本教程中,我们使用了第三方库remarkable,它可以把Markdown文本转换为原始HTML。我们已经将这个库和原始标记一起包含在页面中了,现在可以直接使用。让我们将评论文本转换为Markdown并输出:
// tutorial6.js
var Comment = React.createClass({
render: function() {
var md = new Remarkable();
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
{md.render(this.props.children.toString())}
</div>
);
}
});
在这里就是调用remarkable库。我们需要把React包装的文本this.props.children
转换为remarkable能读懂的原生字符串,所以这里显式调用了toString()
。
但是有个问题!在浏览器中渲染出来的评论看起来像这样"<p>
This is<em>
another</em>
comment</p>
"。我们希望这些标签都实际渲染成HTML。
React保护你不受XSS攻击。有一种方法可以避开,但是框架会警告你不要使用它:
// tutorial7.js
var Comment = React.createClass({
rawMarkup: function() {
var md = new Remarkable();
var rawMarkup = md.render(this.props.children.toString());
return { __html: rawMarkup };
},
render: function() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
<span dangerouslySetInnerHTML={this.rawMarkup()} />
</div>
);
}
});
这是一个特殊的API,它故意让插入原生HTML变得困难,但是对于remarkable,我们可以利用好这个后门。
**记住:**如果使用这个特性,依赖remarkable才是安全的。在这种情况下,remarkable会自动把HTML标记和不安全的链接从输出中剔除。
挂载数据模型#
到目前为止,我们已经直接把评论插入了源码中。现在让我们把JSON数据添加到评论列表中。数据最终从服务器获取,但现在,先写在源码里面:
// 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')
);
现在就可以在CommentList
中使用数据了,让我们动态渲染评论:
// tutorial10.js
var CommentList = React.createClass({
render: function() {
var commentNodes = this.props.data.map(function(comment) {
return (
<Comment author={comment.author} key={comment.id}>
{comment.text}
</Comment>
);
});
return (
<div className="commentList">
{commentNodes}
</div>
);
}
});
就这么简单!
获取服务器数据#
用从服务器获取的动态数据来替换写在代码里面的数据。我们删除假数据,用URL来获取:
// tutorial11.js
ReactDOM.render(
<CommentBox url="/api/comments" />,
document.getElementById('content')
);
现在这个组件和以前有点不同,因为它会重新渲染自身。该组件在服务器请求返回之前都没有数据,请求返回时组件需要渲染新的评论。
注意:代码在这步没有工作。
响应式state #
到目前为止,基于props,每个组件都渲染过一次。props
是不变的:它们是从父组件传来的,是父组件“所有”。为了实现交互,我们在组件中引入了可变的state。this.state
是组件私有的,可以通过调用this.setState()
来改变。当state更新了,组件自身就会重新渲染。
render()
方法是作为this.props
和this.state
函数的声明编写的。框架会保证UI始终和输入保持一致。
当从服务器获取了数据,我们会更新已有的评论数据。给CommentBox
组件添加一个评论数据的数组来作为它的state:
// tutorial12.js
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()
在组件的生命周期内仅执行一次,设置组件的初始状态。
更新state #
在组件首次创建的时候,我们希望从服务器GET一些JSON数据,并通过更新state来反映最新数据。使用jQuery来向服务器发异步请求获取需要的数据。数据已经包含在你启动的服务器上了(基于comments.json
文件),因此一旦获取到,this.state.data
就会如下所示:
[
{"id": "1", "author": "Pete Hunt", "text": "This is one 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.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自动调用的方法。动态更新的关键是调用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.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>
);
}
});
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
元素被渲染,是浏览器来管理它的状态(渲染它的value)。因此,实际DOM的状态与组件的状态是不同的。视图的状态和组件的状态是不同的,这并不理想。在React中,组件始终反映视图的state,而不仅仅只是在初始化的时候。
我们用this.state
来保存用户的输入。定义一个初始只包含author
和text
两个空字符串属性的state
。在<input>
元素中,我们设置了value
来反映组件的state
,然后给它们添加onChange
事件。这些具有value
集合的<input>
元素称为受控组件。在Forms article上阅读更多受控组件的文章。
// 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>
中输入文本时就会触发onChange
的回调,组件的state
就改变了。然后,input
元素的渲染值将被更新来反映当前组件的state
。
(精明的读者可能会奇怪事件句柄像描述那样工作,因为方法的引用并没有明确的绑定到this
。那是因为React.createClass(...)
会自动将每个方法绑定到组件的实例上,避免了显式绑定的需要。)
提交表单#
我们让表单能够有交互。当用户提交表单之后,应该清除它并向服务器提交请求,然后刷新评论列表。首先,监听表单的提交并且进行清除。
// 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 (!text || !author) {
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>
);
}
});
给form添加了一个onSubmit
,在表单进行有效提交的时候清除表单字段。
在事件处理中调用preventDefault()
来阻止浏览器提交表单的默认操作。
作为props的回调#
当用户提交评论时,我们需要刷新评论列表使之包含新的评论。在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.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>
);
}
});
现在CommentBox
已经通过prop的onCommentSubmit
使回调在CommentForm
中可用。CommentForm
可以在用户提交表单的时候调用回调函数:
// tutorial19.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 (!text || !author) {
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) {
this.setState({data: comments});
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!祝你好运!