zoukankan      html  css  js  c++  java
  • 实战分析:评论功能(九)

    现在我们有三个 Dumb 组件,一个控制评论的 reducer。我们还缺什么?需要有人去 LocalStorage 加载数据,去控制新增、删除评论,去把数据保存到 LocalStorage 里面。之前这些逻辑我们都是零散地放在各个组件里面的(主要是 CommentApp 组件),那是因为当时我们还没对 Dumb 和 Smart 组件类型划分的认知,状态和视图之间也没有这么泾渭分明。

    而现在我们知道,这些逻辑是应该放在 Smart 组件里面的:

    了解 MVC、MVP 架构模式的同学应该可以类比过去,Dumb 组件就是 View(负责渲染),Smart 组件就是 Controller(Presenter),State 其实就有点类似 Model。其实不能完全类比过去,它们还是有不少差别的。但是本质上兜兜转转还是把东西分成了三层,所以说前端很喜欢炒别人早就玩烂的概念,这话果然不假。废话不多说,我们现在就把这些应用逻辑抽离到 Smart 组件里面。

    Smart CommentList

    对于 CommentList 组件,可以看到它接受两个参数:comments 和 onDeleteComment。说明需要一个 Smart 组件来负责把 comments 数据传给它,并且还得响应它删除评论的请求。我们新建一个 Smart 组件 src/containers/CommentList.js 来干这些事情:

    import React, { Component } from 'react'
    import PropTypes from 'prop-types'
    import { connect } from 'react-redux'
    import CommentList from '../components/CommentList'
    import { initComments, deleteComment } from '../reducers/comments'
    
    // CommentListContainer
    // 一个 Smart 组件,负责评论列表数据的加载、初始化、删除评论
    // 沟通 CommentList 和 state
    class CommentListContainer extends Component {
      static propTypes = {
        comments: PropTypes.array,
        initComments: PropTypes.func,
        onDeleteComment: PropTypes.func
      }
    
      componentWillMount () {
        // componentWillMount 生命周期中初始化评论
        this._loadComments()
      }
    
      _loadComments () {
        // 从 LocalStorage 中加载评论
        let comments = localStorage.getItem('comments')
        comments = comments ? JSON.parse(comments) : []
        // this.props.initComments 是 connect 传进来的
        // 可以帮我们把数据初始化到 state 里面去
        this.props.initComments(comments)
      }
    
      handleDeleteComment (index) {
        const { comments } = this.props
        // props 是不能变的,所以这里新建一个删除了特定下标的评论列表
        const newComments = [
          ...comments.slice(0, index),
          ...comments.slice(index + 1)
        ]
        // 保存最新的评论列表到 LocalStorage
        localStorage.setItem('comments', JSON.stringify(newComments))
        if (this.props.onDeleteComment) {
          // this.props.onDeleteComment 是 connect 传进来的
          // 会 dispatch 一个 action 去删除评论
          this.props.onDeleteComment(index)
        }
      }
    
      render () {
        return (
          <CommentList
            comments={this.props.comments}
            onDeleteComment={this.handleDeleteComment.bind(this)} />
        )
      }
    }
    
    // 评论列表从 state.comments 中获取
    const mapStateToProps = (state) => {
      return {
        comments: state.comments
      }
    }
    
    const mapDispatchToProps = (dispatch) => {
      return {
        // 提供给 CommentListContainer
        // 当从 LocalStorage 加载评论列表以后就会通过这个方法
        // 把评论列表初始化到 state 当中
        initComments: (comments) => {
          dispatch(initComments(comments))
        },
        // 删除评论
        onDeleteComment: (commentIndex) => {
          dispatch(deleteComment(commentIndex))
        }
      }
    }
    
    // 将 CommentListContainer connect 到 store
    // 会把 comments、initComments、onDeleteComment 传给 CommentListContainer
    export default connect(
      mapStateToProps,
      mapDispatchToProps
    )(CommentListContainer)

    代码有点长,大家通过注释应该了解这个组件的基本逻辑。有一点要额外说明的是,我们一开始传给 CommentListContainer 的 props.comments 其实是 reducer 里面初始化的空的 comments 数组,因为还没有从 LocalStorage 里面取数据。

    而 CommentListContainer 内部从 LocalStorage 加载 comments 数据,然后调用 this.props.initComments(comments) 会导致 dispatch,从而使得真正从 LocalStorage 加载的 comments 初始化到 state 里面去。

    因为 dispatch 了导致 connect 里面的 Connect 包装组件去 state 里面取最新的 comments 然后重新渲染,这时候 CommentListContainer 才获得了有数据的 props.comments

    这里的逻辑有点绕,大家可以回顾一下我们之前实现的 react-redux.js 来体会一下。

    Smart CommentInput

    对于 CommentInput 组件,我们可以看到它有三个参数:usernameonSubmitonUserNameInputBlur。我们需要一个 Smart 的组件来管理用户名在 LocalStorage 的加载、保存;用户还可能点击“发布”按钮,所以还需要处理评论发布的逻辑。我们新建一个 Smart 组件 src/containers/CommentInput.js 来干这些事情:

    import React, { Component } from 'react'
    import PropTypes from 'prop-types'
    import { connect } from 'react-redux'
    import CommentInput from '../components/CommentInput'
    import { addComment } from '../reducers/comments'
    
    // CommentInputContainer
    // 负责用户名的加载、保存,评论的发布
    class CommentInputContainer extends Component {
      static propTypes = {
        comments: PropTypes.array,
        onSubmit: PropTypes.func
      }
    
      constructor () {
        super()
        this.state = { username: '' }
      }
    
      componentWillMount () {
        // componentWillMount 生命周期中初始化用户名
        this._loadUsername()
      }
    
      _loadUsername () {
        // 从 LocalStorage 加载 username
        // 然后可以在 render 方法中传给 CommentInput
        const username = localStorage.getItem('username')
        if (username) {
          this.setState({ username })
        }
      }
    
      _saveUsername (username) {
        // 看看 render 方法的 onUserNameInputBlur
        // 这个方法会在用户名输入框 blur 的时候的被调用,保存用户名
        localStorage.setItem('username', username)
      }
    
      handleSubmitComment (comment) {
        // 评论数据的验证
        if (!comment) return
        if (!comment.username) return alert('请输入用户名')
        if (!comment.content) return alert('请输入评论内容')
        // 新增评论保存到 LocalStorage 中
        const { comments } = this.props
        const newComments = [...comments, comment]
        localStorage.setItem('comments', JSON.stringify(newComments))
        // this.props.onSubmit 是 connect 传进来的
        // 会 dispatch 一个 action 去新增评论
        if (this.props.onSubmit) {
          this.props.onSubmit(comment)
        }
      }
    
      render () {
        return (
          <CommentInput
            username={this.state.username}
            onUserNameInputBlur={this._saveUsername.bind(this)}
            onSubmit={this.handleSubmitComment.bind(this)} />
        )
      }
    }
    
    const mapStateToProps = (state) => {
      return {
        comments: state.comments
      }
    }
    
    const mapDispatchToProps = (dispatch) => {
      return {
        onSubmit: (comment) => {
          dispatch(addComment(comment))
        }
      }
    }
    
    export default connect(
      mapStateToProps,
      mapDispatchToProps
    )(CommentInputContainer)

    同样地,对代码的解释都放在了注释当中。这样就构建了一个 Smart 的 CommentInput

    Smart CommentApp

    接下来的事情都是很简单,我们用 CommentApp 把这两个 Smart 的组件组合起来,把 src/CommentApp.js 移动到 src/containers/CommentApp.js,把里面的内容替换为:

    import React, { Component } from 'react'
    import CommentInput from './CommentInput'
    import CommentList from './CommentList'
    
    export default class CommentApp extends Component {
      render() {
        return (
          <div className='wrapper'>
            <CommentInput />
            <CommentList />
          </div>
        )
      }
    }

    原本很复杂的 CommentApp 现在变得异常简单,因为它的逻辑都分离到了两个 Smart 组件里面去了。原来的 CommentApp 确实承载了太多它不应该承担的责任。分离这些逻辑对我们代码的维护和管理也会带来好处。

    最后一步,修改 src/index.js

    import React from 'react'
    import ReactDOM from 'react-dom'
    import { createStore } from 'redux'
    import { Provider } from 'react-redux'
    import CommentApp from './containers/CommentApp'
    import commentsReducer from './reducers/comments'
    import './index.css'
    
    const store = createStore(commentsReducer)
    
    ReactDOM.render(
      <Provider store={store}>
        <CommentApp />
      </Provider>,
      document.getElementById('root')
    );

    通过 commentsReducer 构建一个 store,然后让 Provider 把它传递下去,这样我们就完成了最后的重构。

    我们最后的组件树是这样的:

    文件目录:

    src
    ├── components
    │   ├── Comment.js
    │   ├── CommentInput.js
    │   └── CommentList.js
    ├── containers
    │   ├── CommentApp.js
    │   ├── CommentInput.js
    │   └── CommentList.js
    │   reducers
    │     └── comments.js
    ├── index.css
    └── index.js

    所有代码可以在这里找到: comment-app3

  • 相关阅读:
    转dhdhtmlxTree
    转Merge的用法
    解决SqlServer2008评估期过期
    借鉴一下对比算法
    Asp.Net异常:"由于代码已经过优化或者本机框架位于调用堆栈之上,无法计算表达式的值"的解决方法
    查看Windows下引起Oracle CPU占用率高的神器-qslice
    今日有奖活动一览
    【分享】给做技术的战友们推荐一个不错的微信公号解解闷
    Unreal Engine Plugin management
    当在ECLIPSE中import现存项目时,如遇到版本不符
  • 原文地址:https://www.cnblogs.com/hanmeimei/p/8855863.html
Copyright © 2011-2022 走看看