原文链接 译者:jella77
教程Tutorial
我们提供:
- 可以看到所有评论的视图
- 提交评论的表单
- 通过Hooks可以自定义后端
它有如下特点:
- 优化的评论: 评论将会在保存到服务器上之前就出现在列表中,这样看上去非常快。
- 实时更新: 其他用户的评论会实时的被放到评论界面。
- 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/15.1.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/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>
本教程余下的部份,我们将通过JavaScript语言完成<script>标签中的内容。我们没有任何高级的livereload(自动重新加载),因此你需要在保存修改刷新浏览器。启动服务后在浏览器中打开http://localhost:3000。当你未做任何修改加载在这个页面时,你将看到我们想要构建的成品。删掉前缀为
<script>的标签就可以开始了。
注意:
我们在这里用到了jQuery因为我们想要简化ajax调用,但这在React中不是必要的。
你的第一个组件
React就是将各种组件模块化组合到一起。我们的评论框例子中需要以下组件结构:
- 评论框CommentBox
- 评论列表CommentList
- 每条评论Comment
- 可提交的评论表单CommentForm
首先,构建评论框组件,这就是个简单的<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')
);
注意纯TML元素名都是以小写字母开头,然而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节点,他们是React的div组件实例。你可以把这些当作React知道如何处理的数据或者标识。Re
act是安全的。我们并不产生HTML字符串,所以XSS保护是默认的。
你可以不返回基本的HTML,可以返回你或者他人建立的一个组件树。这使得React是可组合的(可维护前端的一个关键原则)。
ReactDOM.render()
实例化根节点组件,开始框架,将标识注入到原生的DOM中,作为第二个参数。
通过在不同平台上共享的React核心工具,ReactDOM方法显示出特定的
DOM方法 (e.g., React Native)。
需要注意的是在本教程中ReactDOM.render
要在js文件的底部。ReactDOM.render只有在符合组件被定义后才可以被调用。
复合组件
下面我们构建一个评论列表和评论表单的框架,用到的同样是简单的<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>
);
}
});
接下来,评论框使用这两个新组件:
// 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(标签名)表达式中而不管其他的,这将避免全局命名空间被污染。
使用属性
我们创建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>
);
}
});
通过将JavaScript表达式包含在JSX中(作为属性或者子节点),你可以将文本或者React组件放在树中。我们通过传给组件的this.props
及其他像this.props.children的键来获取值。我们获取传递给组件的属性传递给组件,比如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 (通过author属性) 和评论内容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库。我们需要把this.props.children从
React包裹的文本转化为一个字符串,因此我们显式的调用toString()。
但仍然有问题!我们渲染好的评论在显示器中这样显示:”<p>
This is<em>
another</em>
comment</p>
“。我们想把这些标签真正的渲染成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 dangerouslySetInnerHTML={this.rawMarkup()} />
</div>
);
}
});
这个特殊的API使得插入纯HTML变得困难,但是为了marked也不得不使用这个伎俩。
记住: 使用这个要确保marked是安全的。在这里我们传递sanitize: true
来告诉marked不保留任何的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()
调用来将数据通过属性传入到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是
不可改变的: 它们来自父节点并且属于父节点。为了实现交互,我们为组件引入一个可变的状态。 this.state是组件私有的并且可以通过调用this.setState()被改变。当状态
state更新时,组件也会重新渲染。
render()方法就像
this.props
和this.state函数一样以声明形式写入。框架却表UI始终与输入一致。
当服务器端取数据,我们可以修改我们已有的评论。下面我们为 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()在组件生命周期中被执行并且建立组件初始状态。
更新状态
当组件一开始被创建时,我们想要从服务器端获取JSON,并且更新状态反映到最新的数据中。我们将使用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可以自动更新。
由于这个反应特性,添加实时更新只需要有也很小的改变。我们在这里使用的是简单的轮询,但是你可以使用WebSocket或者其他技术。
// 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调用移到一个单独的方法中,并且在组件第一次被加载时以及之后每隔两秒时被调用。区运行你的浏览器并且修改 comments.json文件
(在你服务端的同一个目录下),两秒钟后你就可以看到变化!
增加新评论
现在我们建立表单。我们的 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来保存用户的输入。我们定义初始状态
state,并赋予两个属性作者名
author和评论文本
text并置空。在我们的
<input>元素中,我们使用
value属性并反映组件的状态
state,另外附
上onChange
事件。 这些 含有待赋值的<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被激活,组件状态被改变。随后,<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 (!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
)给子组件,将它绑定到子组件的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
已经让 CommentForm通过onCommentSubmit属性获得回调,当用户提交表单时
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 reference。好运!
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/114829.html