我们在上一阶段的评论功能基础上加上以下功能需求:

  • 页面加载完成自动聚焦到评论输入框。
  • 把用户名持久化,存放到浏览器的 LocalStorage 中。页面加载时会把用户名加载出来显示到输入框,用户就不需要重新输入用户名了。
  • 把已经发布的评论持久化,存放到浏览器的 LocalStorage 中。页面加载时会把已经保存的评论加载出来,显示到页面的评论列表上。
  • 评论显示发布日期,如“1 秒前”,”30 分钟前”,并且会每隔 5 秒更新发布日期。
  • 评论可以被删除。
  • 类似 Markdown 的行内代码块显示功能,用户输入的用 console.log` 就会处理成 <code>console.log</code> 再显示到页面上。

在线演示地址

大家可以在原来的第一阶段代码的基础上进行修改,第一、二阶段评论功能代码可以在这里找到: 。可以直接使用最新的样式文件 index.css 覆盖原来的 index.css。

接下来可以分析如何利用第二阶段的知识来构建这些功能,在这个过程里面可能会穿插一些小技巧,希望对大家有用。我们回顾一下这个页面的组成:

我们之前把页面分成了四种不同的组件:分别是 CommentAppCommentInputCommentListComment。我们开始修改这个组件,把上面的需求逐个完成。

这个功能是很简单的,我们需要获取 textarea 的 DOM 元素然后调用 focus() API 就可以了。我们给输入框元素加上 ref 以便获取到 DOM 元素,修改 src/CommentInput.js 文件:

  1. class CommentInput extends Component {
  2. static propTypes = {
  3. onSubmit: PropTypes.func
  4. }
  5. constructor () {
  6. super()
  7. this.state = {
  8. content: ''
  9. }
  10. }
  11. componentDidMount () {
  12. this.textarea.focus()
  13. }

这个功能就完成了。现在体验还不是很好,接下来我们把用户名持久化一下,体验就会好很多。

大家可以注意到我们给原来的 props.onSubmit 参数加了组件参数验证,在这次实战案例中,我们都会给评论功能的组件加上 propTypes 进行参数验证,接下来就不累述。

用户输入用户名,然后我们把用户名保存到浏览器的 LocalStorage 当中,当页面加载的时候再从 LocalStorage 把之前保存的用户名显示到用户名输入框当中。这样用户就不用每次都输入用户名了,并且评论框是自动聚焦的,用户的输入体验就好很多。

我们监听用户名输入框失去焦点的事件 onBlur

handleUsernameBlur 中我们把用户的输入内容保存到 LocalStorage 当中:

  1. class CommentInput extends Component {
  2. constructor () {
  3. super()
  4. this.state = {
  5. username: '',
  6. content: ''
  7. }
  8. }
  9. componentDidMount () {
  10. this.textarea.focus()
  11. _saveUsername (username) {
  12. localStorage.setItem('username', username)
  13. }
  14. handleUsernameBlur (event) {
  15. this._saveUsername(event.target.value)
  16. }
  17. ...

handleUsernameBlur 中我们把用户输入的内容传给了 saveUsername 私有方法(所有私有方法都以 开头)。 会设置 LocalStorage 中的 username 字段,用户名就持久化了。这样就相当于每当用户输入完用户名以后(输入框失去焦点的时候),都会把用户名自动保存一次。

输入用户名,然后到浏览器里里面看看是否保存了:

然后我们组件挂载的时候把用户名加载出来。这是一种数据加载操作,我们说过,不依赖 DOM 操作的组件启动的操作都可以放在 componentWillMount 中进行,所以给 CommentInput 添加 componentWillMount 的组件生命周期:

componentWillMount 会调用 _loadUsername 私有方法,_loadUsername 会从 LocalStorage 加载用户名并且 setState 到组件的 state.username 中。那么组件在渲染的时候(render 方法)挂载的时候就可以用上用户名了。

这样体验就好多了,刷新页面,不需要输入用户名,并且自动聚焦到了输入框。我们 1、 2 需求都已经完成。

这里插入一些小贴示,大家可以注意到我们组件的命名和方法的摆放顺序其实有一定的讲究,这里可以简单分享一下个人的习惯,仅供参考。

组件的私有方法都用 _ 开头,所有事件监听的方法都用 handle 开头。把事件监听方法传给组件的时候,属性名用 on 开头。例如:

  1. <CommentInput
  2. onSubmit={this.handleSubmitComment.bind(this)} />

这样统一规范处理事件命名会给我们带来语义化组件的好处,监听(onCommentInputSubmit 事件,并且交给 this 去处理(handle)。这种规范在多人协作的时候也会非常方便。

另外,组件的内容编写顺序如下:

  • static 开头的类属性,如 defaultPropspropTypes
  • 构造函数,constructor
  • getter/setter(还不了解的同学可以暂时忽略)。
  • 组件生命周期。
  • _ 开头的私有方法。
  • 事件监听方法,handle*
  • render开头的方法,有时候 render() 方法里面的内容会分开到不同函数里面进行,这些函数都以 render 开头。
  • 方法。 如果所有的组件都按这种顺序来编写,那么维护起来就会方便很多,多人协作的时候别人理解代码也会一目了然。