教程
我们会创建一个简单却真实的评论列表,你可以把它用在博客里,就像Disqus,LiveFyre,FaceBook里的实时评论功能一样,但它只是一个基础的版本.
我们会提供:
- 一个包含所有评论的视图
- 一个用于提交评论的表单
- 可以自定义后端接口的钩子
它还有一些新特性:
- 预显示评论: 新增评论会在被保存到服务器之前就显示在页面里,以获得更快的体验.
- 实时更新: 其他用户的评论会被实时的输出到评论视图里.
- 支持Markdown格式: 用户可以使用markdown语法来书写评论内容.
想要跳过本文直接看源码?
启动服务端
要开始这个教程,我们首先要启动一个服务端.我们只需要一个后端的API用来获取和保存数据.为了尽可能简化它,我们用脚本语言创建了一个简单的服务端,它刚好满足我们的需要.你可以查看源码或者下载zip压缩文件,这里面包含了启动所需要的一切程序.
为了更简化,我们要运行的服务端使用了一个 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/0.14.3/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/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.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/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 标签给删掉,然后就可以继续了.
注意: 为了等下方便发送ajax请求,我们在这里引入了jQuery,但React并不是强制使用jQuery才能工作的.
由于直接在html文件的script标签里写react的话,intelliJ不能识别JSX语法,所以最好还是写在外链的js文件里.
你的第一个组件
React都是模块化的, 由可编辑的组件组成. 我们的这个评论框例子由以下这些组件构成:
- CommentBox
- CommentList
- Comment
- CommentForm
让我们创建一个 CommentBox 组件,它只是一个简单的
// 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语法是可选的,但是使用它会比不使用要更简单.了解更多关于 JSX语法
继续下一步
我们把一些方法放在一个Javscript对象里,然后把它传给 React.createClass()
,以创建一个React组件.这些方法里最重要的就是 render
方法,它返回了React组件树,最后会渲染成HTML.
这里的 <div>
标签并不是真正的DOM元素;他们是React div
组件的实例.你可以想象成他们是某种标记或数据,React知道要如何去处理他们.React是安全的,我们并不是生成HTML字符串,所以默认已经阻止了XSS攻击.
你不需要返回所有的HTML.你可以返回一个你(或别人)创建的组件树.这就使得React是可重用的.可重用化是构建可维护的前端的一个重要原则.
ReactDOM.render()
实例化了根组件,开始构造组件内容,然后把内容注入到一个既存的原生DOM元素里.这个DOM元素可以通过第二个参数传入.
ReactDOM
模块暴露的是DOM特有的方法,而 React
类有一些核心的工具是可以在不同的使用React的平台上共享的.(比如 React Native)
很重要的一点是,可以在这个教程里看到, ReactDOM.render
方法是被放在script的最后调用的. React.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)
表达式,其他都不变.这是为了防止污染全局命名空间.
使用属性
让我们来创建 Comment
组件,它依赖于父组件传给它的数据. 从父组件传入的数据可以通过子组件的’属性’来获取. 这些’属性’可以通过 this.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表达式放在花括号里,可以插入文本或者在组件树里插入React组件.指定名字的属性值可以通过 this.props
对象中的对应的key来获取.嵌套的元素可以通过 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是一个简单的格式化文本的方法.举个栗子,用’*’包围的文字会变成强调部分(斜体).
在这个教程里,我们使用第三方的库 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库,我们需要把被React封装过的 this.props.children
文本转换成原始的字符串,这样marked才能识别.所以我们明确的调用 toString()
.
但这样做有个问题! 我们渲染出来的评论内容在浏览器里看上去是这样的: "<p>This is <em>another</em> comment</p>"
.我们希望的是这些标签能够真正地被转换成HTML.
这是React保护你免受 XXS攻击.有一个方法可以解决这个问题,但是框架会给你一个警告,叫你不要这样做:
// 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 dangerouslySetInnerHTML={this.rawMarkup()} />
</div>
);
}
});
这是一个特殊的API,有意让插入原始的HTML变得困难.但对于marked来说,刚好可以利用它的优势.
注意: 要使用这个API,你就必须仰赖于marked是安全的. 在这里,我们传入 sanitize:true
来告诉marked,把所有的HTML标记进行转义,而不是直接输出HTML.
解释一下:在你输入的内容是纯文字或者’*’等符号的时候,marked会把它转换成html文本,比如
<p>123<em>456</em></p>
,这个时候,你希望这些标签能变成HTML标签,而不是文字,所以你可以使用React的dangerouslySetInnerHTML
,但是如果用户本身在内容里输入了html文本,比如 ‘‘ 这样,如果不在marked里面设置sanitize:true
,那么用户输入的这些html文本也会变成HTML标签,这样就不安全.所以需要开启sanitize:true
,这样,首先marked会把用户输入的所有内容都转义成html文本,然后通过Markdown转换来把文本变成对应的HTML,最后React直接把HTML渲染.这样就确保了用户不能创建HTML插入,但是可以使用markdown所支持的语法.
连接数据模型
到目前为止,我们直接在源码里插入了评论内容.接下来,我们要用一堆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>
);
}
});
就系酱紫!
从服务端获取数据
让我们把硬编码的数据换成从服务端获取的动态数据.我们把data属性移除,换成一个获取数据的url属性:
// tutorial11.js
ReactDOM.render(
<CommentBox url="/api/comments" />,
document.getElementById('content')
);
这个组件和之前的组件不同,它必须重新渲染自己.这个组件在服务器没有返回数据之前是没有任何数据的,在得到数据的时候,他们需要渲染一些新的评论.
注意: 代码在这一步是不能运行的.
响应式状态
到目前为止,每个组件都会基于它的props属性渲染自己一次. props
是不可变的:他们是通过父组件传过来的,被父组件所’拥有’的.为了实现交互,我们引入可变的状态到组件里. this.state
是组件私有的,并且可以通过调用 this.setState()
来改变.当状态更新的时候,组件会重新渲染自己.
render()
方法用声明式的写法来使用 this.props
和 this.state
功能. 所以它确保了视图总是和输入保持一致.
所谓的声明式,就像css类名一样,只需要声明一个类名,对应的样式修改了,元素的样式也就改变了,这个概念用到这里就是说,
render()
方法里的this.props
和this.state
变化的时候,视图也会同步发生变化.
当服务器获取到数据的时候,评论的内容会变成新获取的数据.让我们添加一个评论的数组作为 CommentBox
组件的状态:
// 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()
方法在组件的生命周期里只会被执行一次,用于设置组件的初始状态.
更新状态
当组件第一次被创建后,我们想要通过GET请求从服务端去获取JSON数据,然后更新状态以映射最新获取的数据.我们使用jQuery向前面已经搭建好的服务器发送一个异步请求来获取我们需要的数据.数据已经被放在这个服务器端了(就是 comments.json
文件),所以当数据被获取后, this.state.data
看起来是这样的:
[
{"author": "Pete Hunt", "text": "This is one comment"},
{"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()
方法. 我们用从服务器获取的最新数据来替换原来的评论数组,然后视图会自动更新自己.由于它是响应式的,还需要加一个小改动来实现实时更新.我们这里只使用简单的轮询,你也可以很容易的使用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部分的代码放到一个单独的方法里,然后在组件第一次渲染完毕后执行一次,然后再每隔2s执行一次.试着在浏览器里运行它,然后修改 comment.json
文件(在你启动的那个项目文件夹里); 在2s以内,改变就会被呈现出来!
添加评论
现在可以开始创建表单了.我们的 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>
);
}
});
可控组件 (Controlled components)
对于传统的DOM元素, input
元素渲染后由浏览器管理它的状态(就是渲染出的value值).所以,真正的DOM元素的状态和对应的组件的状态就不一样了. 视图和组件的状态不同,这可不是理想的状态. 在React里,组件的状态应该永远和视图同步,而不仅仅是在初始化的时候.
因此,我们使用 this.state
在用户打字的时候保存他们输入的内容. 我们定义一个初始的 state
,它有两个属性 author
和 text
,一开始他们的值都是空字符串.在的 <input>
元素里,我们把 value
值设置成组件的 state
,然后给它们添加一个 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>
元素里输入内容的时候, 对应的 onChange
回调就会触发,组件的 state
就会被改变. 然后, input
元素所渲染的value值会被更新,映射组件当前的 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 (!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>
);
}
});
我们给表单添加一个 onSubmit
事件处理句柄,当里面的表单被提交,并且表单控件都通过验证时,清空表单.
在事件回调里执行 preventDefault()
来阻止浏览器的默认表单提交事件.
回调作为属性
当用户提交新评论的时候,我们需要更新评论列表来把新增的评论显示出来.应该把这个逻辑放在 CommentBox
里,因为 CommentBox
的状态对应的就是评论列表.
我们需要把数据从子组件再传回到它的父组件.通过在父组件的 render
方法里传递一个回调(handleCommentSubmit
)给子组件,绑定到子组件的 handleCommentSubmit
事件上.每当事件触发,回调就会被执行:
// 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>
);
}
});
当用户提交表单的时候,在 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;
// 乐观地给评论添加一个id.将来这个id会被服务器端生成的id替换.
// 在生产环境下你可能不会使用 Date.now() 作为id,而是使用更合适更健壮的id体系.
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文档,开始使用吧! Good luck!