作为一个新手,绝不好高骛远。
而踏踏实实的一个重要表现,就是走一走要扭头看一看。
通过这个评论框练习,差不多结束入门阶段,特此总结。
注:此为融汇阶段,需要一定基础。
声明:转载请注明出处
我的一些话:并不是什么高手,有什么不完备的地方,请尽情轰炸我,蹂躏我吧!
文章相关——偏基础,偏练手性质,高手直接略过吧,不然是浪费时间呢
[TOC]——简书还没有目录么???
分析下这个小小的评论框
大概长这样
接下来我们来分析一下如何来做
抽象组件
整个评论系统可以看做一个大组件;评论显示列表框,评论输入框是第二层组件;评论显示列表中的每个评论是第三层组件。
- 评论系统
- 评论列表
- 评论
- 评论录入
- 评论列表
但是,在最初抽象组件时,可能这样搞
- 评论系统
- 评论
- 评论录入
由于组件渲染最外层,只能一个标签,所以,对于评论来说,多个评论放一块,也应当组成一个组件。
也就是说,在抽象过程中,组合也是抽象的一部分。
功能分析
一下子有序的分析所有的功能真的不容易,应由根及叶(就像react官网上教的一样。)
- 最根本的功能——
用户输入评论、
评论列表显示评论。
很简单的描述,并不具体,为了很好的实现它,我们需要更深层的分析- 用户输入评论
用户评论牵扯到评论录入表单组件,每一条评论应对应于一个数据对象
- 用户输入评论
{
id: '',
author: '',
text: ''
}
而这个数据对象,很明显,将是贯穿前后。
仅对于输入评论来说:输入用户名,输入评论内容,点击post按钮后,通过Ajax传输数据对象并请求服务器,服务器写入后台。
以上是基本功能,我们还需要一些更好的体验:
点击post按钮后,清空Input内的内容。
- 评论列表显示评论
显示评论,牵扯到评论列表组件和评论组件。依然是上边的数据对象
能想到的功能就是:
当页面打开时候,从后台拿到数据,并逐个创建评论并插入到评论列表;
当有用户更新评论时,能够实时显示;
当有新评论添加时,能实时显示。
一些更好的功能(react教程里的,我没想到这个):
可以识别markdown语法,并正确显示。
以上,就是所有的分析,然我们开始实现它!
具体实现
- 评论系统
- 评论列表
- 评论
- 评论录入
react标榜的是组件化开发,那么在依据react的开发过程也应当是基于组件开始的,那么就从组件开始吧。(按照react官网教程的结构,我觉得有必要加入我自己的理解,用更菜鸟一点的语言再说一说,这部分结束,后边有我自己从不同角度的理解)
- 评论列表
官网流程——布蕾布蕾版
开始
准备工作
官网教程中文
英文貌似没有了。
项目源码github地址
用git down下来,目录如图
别的不多说了,仅仅提一下server.js,模拟的后台,提供了一个api,
这里定义了服务端口号,本地3000端口
在script文件夹创建一个js文件,修改index.html中script的地址
最后,在项目目录运行
npm install
安装项目依赖,package.json中查看
node server.js
运行我们的后台
浏览器打开:http://localhost:3000/
准备工作结束
(接下来主要一步一步放代码,详情可以去官网看,不难)
以下代码,都写在自己新建的js
编写组件框架
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
Hello, world! I am a CommentBox.
</div>
);
}
});
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>
);
}
});
ReactDOM.render(
<CommentBox />,
document.getElementById('content')
);
串联组件
让我们建立组件之间的联系
// 修改评论系统组件,链接其他组件
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList />
<CommentForm />
</div>
);
}
});
//添加评论组件
var Comment = React.createClass({
render: function() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
{this.props.children}
</div>
);
}
});
//将评论组件添加到评论列表组件中
// 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>
);
}
});
//为了识别markdown语法,我们修改评论组件
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>
);
}
});
//由于react保护我们避免xss攻击,所以,以上修改没有实际效果,所以,
//对comment组件做如下修改
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>
);
}
});
数据
//通过CommentBox组件state挂载数据
var CommentBox = React.createClass({
getInitialState: function() {
return {data: []};
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm />
</div>
);
}
});
ReactDOM.render(
<CommentBox data={data} />,
document.getElementById('content')
);
//commentList中使用数据
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>
);
}
});
要从服务器获取数据了!
//写下我们的接口
ReactDOM.render(
<CommentBox url="/api/comments" />,
document.getElementById('content')
);
点开comments.json文件,会看到我们的`数据库`,此时,照着上边改点有个人特色的,人名或话
//编写请求数据方法
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);
//通过一个简单的定时器实现不停的检查,当然也可以使用WebSockets等技术
},
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')
);
修改一下comments.json中的数据,观察网页变化吧
表单组件
//完善表单
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>
);
}
});
//添加方法
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>
);
}
});
//提交表单事件
//在CommentForm组件中,添加或修改
//添加方法
handleSubmit: function(e) {
e.preventDefault();
var author = this.state.author.trim();
var text = this.state.text.trim();
if (!text || !author) {
return;
}
// TODO: 待会在这添加一个向服务器发送请求的方法
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>
);
}
});
//使用事件处理程序实现子到父组建通信
var CommentBox = React.createClass({
···
//添加
handleCommentSubmit: function(comment) {
// TODO: 等会再这编写提交服务器并刷新的具体实现
},
···
//部分修改
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>
);
}
});
//用户提交表单时,调用回调函数
var CommentForm = React.createClass({
···
//部分修改
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: ''});
},
···
});
//提交服务器,并刷新的功能实现
var CommentBox = React.createClass({
···
//添加方法实现
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)
});
},
···
});
优化
我们提交表单,再等刷新表单后获取数据,再显示到页面上,显然速度很慢。
我们做的优化是,本地提交后,根据本地缓存的评论数据对象(带本地添加的新平路)直接加载,等从服务器请求数据后,再进行一次加载。
因为react是否刷新页面取决于虚拟dom和真实dom的区别,如果从服务器请求数据后,再次加载时,发现没有变化,那么久不会刷新页面。这样做,性能显然就更高
var CommentBox = React.createClass({
···
//部分修改
handleCommentSubmit: function(comment) {
var comments = this.state.data;
// 这个id=Date.now(),其实就是模拟一个为评论添加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)
});
},
···
});
在数据库中,文章,用户,评论这些数据,都应该有一个id,
比如:一个文章对应一个作者,对应一堆评论。
在比如:一个评论,对应一个发布者,对应一篇文章
结束!
我的理解
我觉得理解一个项目,应当理解这几方面
- 通信(数据流向)
- Atom间通信(react的Atom是组件)
- 功能
- 事件
- 方法
他们并不是分立的,而是糅合在一起,我觉得清晰的理解他们是一个难点。
应该从根基着手,最终落脚于功能。比如react,从组件着手,落脚于设计组件,组合组件构成的各种功能。通信是针,数据是线,功能是目标。
对于这个评论框来说:
- 数据流向
评论(data)数据对象,贯穿上下前后(顶层组件到底层组件,前、后端到处窜)。
在顶层组件state保存,一层一层通过props传入底层组件,最后反应到屏幕上。
从form组件录入,传入后台保存。
通过轮询,从后台拿到最新数据,反应到屏幕上
这样的数据流向,与react的单向数据流特性有关。
- 功能方法
我们分析功能方法:
需要请求服务器数据,就有了loadCommentsFromServer()
方法,在首次加载和轮询时,我们都用它
需要提交数据,handleCommentSubmit()
被设置在父组件上,通过props传递引用,进入到表单组件。而子组件的handleSubmit()
在点击按钮时候,调用handleCommentSubmit()
,并清空表单。由于在调用时候并没有改变this,所以,此时handleCommentSubmit()
的上下文还是在父组件中,那么通过它调用this.setState实现优化。
handleSubmit()
的数据从哪来?从input的handleAuthorChange()``handleTextChange()
事件处理函数来。
复杂情况
然而,这个小项目,并不复杂,在很多复杂的情况下,我们要清楚数据流向和功能方法之间的关系,如何配合。
通信
React组件之间通信,主要由组件的关系决定
- 父组件——>子组件
- 子组件——>父组件
- 无嵌套关系
虽然只是初学者,但我也讲讲我的想法
父传子
- 利用props
太简单嘛,就像上边咱们的做法
但是这里有个问题,如果层数太多,怎么办。
可以想象,100层,一层一层往里边传,绝对是很慢(当然,只是我想当然,没测试过♪(∇*))。
怎么解决,还清楚
子传父
我觉得一定会有这种情况,比如购物网站,购物车显示当前购物车里商品个数。
如图
我们可以从子传给父,然后父render,那么怎么办呢。
事件处理函数(核心,就是传递事件处理程序的引用)!从上边的例子中,我们也能看出,没错,就是handleCommentSubmit()
方法
对于购物车例子我们可以这样
购物车组件:
某数量方法number(data)
子组件通过props引用方法,比如:子—setNumber={this.number}
在子上下文中,设置事件处理程序onChange={this.change},当数量改变时候,调用,setNumber(‘具体数字’)
传入具体数量。由于传递的是引用,number方法的this并没有变,所以,可以在number()中,根据传入的n,设置父组件的state,也就实现了子传父。
这里有一点要记录,就是react事件系统
它并不是用的原生事件,而是自己封装实现的react事件,(注意大写onClick,不是onclick,(#‵′)真坑,查了半个小时没查出来)
react事件系统可以查阅英文官网中文官网
其实上边那种方式,就像事件委托一样,仔细想想,复用事件处理程序其实也可这么写的。
不过,由于知识有限,目前还没有理解这里边的作用域链关系,有没有前辈不吝指导一下= ̄ω ̄=
无关系
似乎要用到设计模式。。。信号模式?发布订阅模式?
好吧,最直观还是兄弟1——>父亲——>兄弟2 o(╯□╰)o
关于发布订阅模式,我是这样理解的,有一个中继站,它可以注册话题,如果一个人在这个话题下发布(publish)文章,其他订阅(subscribe)话题的人,就能收到消息。但是这个中继站怎么做,我还不会,也没太研究过,不敢乱说。
es6也有传递信息的方式,es6没学精,不敢乱说,移步链接点我,ES6 中的生成器函数介绍
复杂情况就带此结束吧。
评论框结构总结
- 评论系统
<CommentBox url="/api/comments" pollInterval={3000} />
:
HTML结构:
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data}/>
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>
state:
data[] 评论数据
porps:
url 请求服务器api接口地址
pollInterval 循环检查变化的间隔时间
`loadCommentsFromServer`: 从服务器获取评论的方法
`handleCommentSubmit`: 点击提交的方法
- 评论列表
`<CommentList />`:
HTML结构:
<div className="commentList">
{commentNodes}
commentNodes[i]的HTML结构
<Comment author={item.author} key={item.id}>
{item.text}
</Comment>
</div>
props:
data 从父组件(CommentBox)state中的data数组而来,表示评论数据
`commentNode`: 一个个<Comment>组件构成的数组。
- 评论
`<Comment />`
HTML结构:
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
<span dangerouslySetInnerHTML={this.rawMarkup()}></span>
</div>
props:
author
key
这俩都是从父组件(CommentList)传过来,数据对象
rawMarkup: markdown的处理
- 评论录入
`<CommentForm />`:
HTML结构:
<form className="commentForm" onSubmit={this.handleSubmit}>
<input type="text" placeholder="Your name"
value={this.state.author}
onChange={this.handleAuthorChange}
/>
<input type="text" placeholder="说点啥呗..."
value={this.state.text}
onChange={this.handleTextChange}
/>
<input type="submit" value="post" />
</form>
props:
onCommentSubmit 父组件的handleCommentSubmit()传给了它。
state:
author: 用户名
text:评论内容
`handleAuthorChange`: 用户名输入框改变触发的方法
`handleTextChange`: 评论内容框改变触发的方法
`handleSubmit`: 点击提交后触发的方法
放一张在sublime上写的,看着清晰一些。
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/4765228-aaa4bccb57d7aca9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
## 后记
终于写完了,挺长,能看到最后不容易,大兄弟们握个手!
如有错误,请一定赐教
## 参考资料