我们来继续优化之前写的评论系统

功能实现CheckList

聚焦功能

组件参数验证

用户名记录

评论记录

发布时间显示

评论删除

代码块引入

聚焦功能

首先给评论系统增加一个聚焦功能,具体是,当页面加载完毕之后,会自动聚焦到评论框

React.js通过ref来获取已经挂载的元素的DOM节点

我们在textarea中使用refref是一个函数,当元素在页面上挂载完毕的时候,React.js 就会调用这个函数,并且把这个挂载以后的 DOM节点传给这个函数,之后我们就可以通过this.(元素名)来获取这个DOM元素

1
2
3
4
<textarea 
ref={(textarea) => this.textarea = textarea}
value={this.state.content}
onChange={this.handleContentChange.bind(this)} />

然后在class中加入ComponentDidMount生命周期

React.js生命周期方法

ComponentDidMount在第一次渲染后调用

1
2
3
componentDidMount () {
this.textarea.focus()
}

刷新页面后,可以看到已经完成了对评论框的聚焦

组件参数验证

Javascript是一种非常灵活的语言,灵活体现在其弱类型,高阶函数等语言特性,但是这也意味着特别容易出bug

这里我突然想起来一则笑话:程序员开发团队写了一个咖啡馆,然后测试员开始对咖啡馆进行测试,他们开始尝试从门口走入咖啡馆,爬窗进入,从下水道进入咖啡馆,坐着,躺着,站着喝咖啡,最后对这个咖啡馆表示满意,结果,咖啡馆上线的第一天,一位顾客进来点了一份炒饭,咖啡馆爆炸了

嗯,是这样的,你永远没法知道使用者会对组件传入什么奇怪的参数,强类型的语言可以一定程度上规避这个问题,弱类型语言由于限制规则少,安全性是很差的

我们在写评论组件的时候,传入的props是一个comment数组,如果使用组着的人传入一个数字1,页面不会有任何的报错,但是会显示不正常,所以我们需要定义一个props的类型

教程上是这么使用的

1
2
3
static propTypes = {
comment: PropTypes.object
}

我照着使用之后报错了

PropTypes 自 React v15.5 起被移除,使用 prop-types 第三方库来进行替换

安装这个库之后从prop-types中导入,用法是相同的

用户名记录

接下来实现一个功能,让用户在浏览器刷新之后保留上一次填写的用户名

我们首先监听失去焦点的事件onBlur

onBluronChange的区别:onBlur是光标焦点只要离开调用方法的文本框就会发生,而onChange则是内容改变才会发生

1
2
3
4
<input
value={this.state.username}
onBlur={this.handleUsernameBlur.bind(this)}
onChange={this.handleUsernameChange.bind(this)} />
1
2
3
4
5
6
7
_saveUsername (username) {
localStorage.setItem('username', username)
}

handleUsernameBlur (event) {
this._saveUsername(event.target.value)
}

然后我们在componentWillMount生命周期载入名字

1
2
3
4
5
6
7
8
9
10
componentWillMount () {
this._loadUsername()
}

_loadUsername () {
const username = localStorage.getItem('username')
if (username) {
this.setState({ username })
}
}

评论记录

然后我们用相同的方式把评论也持久化,在每次用户提交评论的时候保存评论列表数据,挂载的时候加载起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class CommentApp extends Component {
componentWillMount () {
this._loadComments()
}

_loadComments () {
let comments = localStorage.getItem('comments')
if (comments) {
comments = JSON.parse(comments)
this.setState({ comments })
}
}

_saveComments (comments) {
localStorage.setItem('comments', JSON.stringify(comments))
}

handleSubmitComment (comment) {
if (!comment) return
if (!comment.username) return alert('请输入用户名')
if (!comment.content) return alert('请输入评论内容')
const comments = this.state.comments
comments.push(comment)
this.setState({ comments })
this._saveComments(comments)
}
}

显示发布时间

我们记录发布评论的时间,通过和当前时间的差值来计算

1
2
3
4
5
6
7
8
9
10
11
handleSubmit () {
if (this.props.onSubmit) {
// const { username, content } = this.state
this.props.onSubmit({
username: this.state.username,
content: this.state.content,
createdTime: +new Date()
})
}
this.setState({ content: '' })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Comment extends Component {
constructor () {
super()
this.state = { timeString: '' }
}

componentWillMount () {
this._updateTimeString()
}

_updateTimeString () {
const comment = this.props.comment
const duration = (+Date.now() - comment.createdTime) / 1000
this.setState({
timeString: duration > 60
? `${Math.round(duration / 60)} 分钟前`
: `${Math.round(Math.max(duration, 1))} 秒前`
})
}

render () {
return (
<div className='comment'>
<div className='comment-user'>
<span>{this.props.comment.username} </span>:
</div>
<p>{this.props.comment.content}</p>
<span className='comment-createdtime'>
{this.state.timeString}
</span>
</div>
)
}
}

这样就能够显示评论是在几分钟前发布的,但是需要刷新页面才能够更新显示时间

我们希望评论的时间能自动刷新,所以我们用setInterval设置一个刷新时间

1
2
3
4
5
6
7
componentWillMount () {
this._updateTimeString()
this._timer = setInterval(
this._updateTimeString.bind(this),
5000
)
}

删除评论

我们接着给评论系统增加删除功能,首先在comment.js中增加一个删除按钮,在css中设置被鼠标cover时才能出现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 render () {
const { comment } = this.props
return (
<div className='comment'>
<div className='comment-user'>
<span className='comment-username'>
{comment.username}
</span>:
</div>
<p>{comment.content}</p>
<span className='comment-createdtime'>
{this.state.timeString}
</span>
<span className='comment-delete'>
删除
</span>
</div>
)
}

现在这个删除按钮时在Comment组件中的,但是存储评论是在CommentApp组件中,我们需要通过CommentList来传递删除的信息

我们在Comment以及CommentList的props中再设置一个参数onDeleteComment

1
2
3
4
5
handleDeleteComment () {
if (this.props.onDeleteComment) {
this.props.onDeleteComment(this.props.index)
}
}
1
2
3
4
5
<span
onClick={this.handleDeleteComment.bind(this)}
className='comment-delete'>
删除
</span>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
handleDeleteComment (index) {
if (this.props.onDeleteComment) {
this.props.onDeleteComment(index)
}
}

render() {
return (
<div>
{this.props.comments.map((comment, i) =>
<Comment
comment={comment}
key={i}
index={i}
onDeleteComment={this.handleDeleteComment.bind(this)} />
)}
</div>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
handleDeleteComment (index) {
const comments = this.state.comments
comments.splice(index, 1)
this.setState({ comments })
this._saveComments(comments)
}

render() {
return (
<div className='wrapper'>
<CommentInput onSubmit={this.handleSubmitComment.bind(this)} />
<CommentList
comments={this.state.comments}
onDeleteComment={this.handleDeleteComment.bind(this)} />
</div>
)
}

看看这些代码,会觉得其实非常套娃,当用户点击删除按钮的时候Comment组件会调用props.onDeleteComment,其对应CommentList中的handleDeleteComment,在这个函数中,调用了commentListprops.onDeleteComment,其对应的是CommentApp组件中的handleDeleteComment,这就把参数index逐层传递了上来

快乐删除,并得到了一个报错

这是因为我们评论的计时器没有删除掉,我们在Comment组件中新增生命周期commentWillUnmount,在组件销毁时清除定时器,类似于C++的析构函数

1
2
3
componentWillUnmount () {
clearInterval(this._timer)
}

删除功能完成