[React]Tutorial译文
本文完全由博主自己翻译,几乎没有借助其他任何文章,仅仅作为自己入门React的切入点和学习的手段,有些内容的翻译可能会比较别扭,敬请原谅。后续还会继续调整部分翻译内容,以使得本文更加通顺
我们将构建一个由Disqus、LiveFyre或者Facebook Comment提供的,可以将之放入一个博客之中的,简单而真实的评论框。
我们将提供以下功能:
- 所有评论的展示
- 可以提交评论的表格
- 为你提供自定义后端的钩子(Hooks)
同时还具有一些其他的优良特性:
- 及时评论:在评论被保存到服务器之前就会展现在评论列表中,这样看起来更加快捷
- 随时更新:其他用户的评论会及时进入评论
- Markdown格式化:用户可以使用Markdown来格式化他们的文字
想要跳过这些直接看源码?
启动服务器
为了能够开始学习此教程,我们需要启动一个服务器。启动服务器单纯地当做一个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!祝你幸运!