美文网首页从零到一搭建 React 项目
从零到一搭建 react 项目系列之(八)

从零到一搭建 react 项目系列之(八)

作者: 越前君 | 来源:发表于2020-08-30 23:45 被阅读0次

    为了方便使用, Redux 的作者封装了一个 React 专用的 React-Redux 库。

    本篇内容主要参考自阮一峰老师的文章

    React-Redux

    它将组件分成两类:UI 组件(presentational component)和容器组件(container component)。

    1. UI 组件
    const Title = <h3>Title</h3>
    

    主要有以下几个特征:

    • 只负责 UI 的呈现,不带有任何业务逻辑。
    • 没有状态。(即没有 this.state 这个变量)
    • 所有数据由参赛(this.props)提供。
    • 不使用任何 Redux 的 API。
    2. 容器组件

    与 UI 组件相反,它的特征主要有:

    • 负责管理数据和业务逻辑,不负责 UI 的呈现。
    • 带有内部状态。
    • 使用 Redux 的 API。
    3. 我们常见的是容器组件 + UI 组件

    记住就好了:UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。

    当同时存在 UI 组件和容器组件时,我们将采用容器组件包裹 UI 组件的策略,前者负责与外部通信,将数据传递给后者,由后者渲染出视图。

    React-Redux 规定,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它。

    React-Redux API

    1. connect()

    React-Redux 提供的 connect() 方法,用于从 UI 组件生成容器组件。从字面理解的话,就是将两种组件连起来。

    import { connect } from 'react-redux'
    const VisibleTodoList = connect()(TodoList)
    
    // TodoList 是 UI 组件
    // VisibleTodoList 是由 React-Redux 通过 `connect` 方法生成的容器组件
    

    上述案例,并没有定义业务逻辑,它没有任何意义。

    为了定义业务逻辑,需要给出两方面的信息

    (1) 输入逻辑: 外部的数据(即 state 对象)如何转换为 UI 组件的参数。
    (2) 输出逻辑: 用户发出的动作如何变为 Action 对象,从 UI 组件传出去。

    connect() 完整 API 如下:

    import { connect } from 'react-redux'
    const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList)
    
    // connect 接收两个参数:mapStateToProps 和 mapDispatchToProps。
    // 前者负责输入逻辑,即将 state 映射到 UI 组件的参数(props)。
    // 后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。
    
    2. mapStateToProps

    它是一个函数,作用是建立一个从(外部的) state 对象到(UI 组件的) props 对象的映射关系。

    作为函数,mapStateToProps 执行后应该返回一个对象,里面的每一个键值对就是一个映射。

    const mapStateToProps = (state, ownProps) => {
      return {
        // ...
      }
    }
    
    // mapStateToProps 接收两个参数,并且返回一个对象
    // state 就是我们 store 的全局状态
    // ownProps 是容器组件的 props 对象。使用它之后,如果容器组件的参数发送变化,也会引发 UI 组件重新渲染。
    

    mapStateToProps 会订阅 Store,每当 state 更新时,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。

    connect 方法可以省略 mapStateToProps 参数,这样的话,UI 组件就不会订阅 Store,即 Store 的更新不会引发 UI 组件的更新。

    3. mapDispatchToProps

    它是 connect 方法的第二个参数,用于建立 UI 组件的参数到 store.dispatch 方法的映射。即将 Action 绑定到 UI 组件的 props 对象上。

    (1)mapDispatchToProps 是函数时,会得到 dispatchownProps 两个参数。
    (2)mapDispatchToProps 是对象时,它每个键名对应 UI 组件的同名参数,键值应该是一个函数,会被当做 Action Creator,返回的 Action 会由 Redux 字段发出。

    // 函数
    const mapDispatchToProps = (
      dispatch,
      ownProps
    ) => {
      return {
        onClick: () => {
          dispatch({
            type: 'SET_VISIBILITY_FILTER',
            filter: ownProps.filter
          })
        }
      }
    }
    
    // 对象
    const mapDispatchToProps = {
      onClick: (filter) => {
        type: 'SET_VISIBILITY_FILTER',
        filter: filter
      }
    }
    

    <Provider> 组件

    connect() 方法生成容器组件以后,需要让容器组件拿到 state 对象,才能生成 UI 组件的参数。

    一种解决方法是将 state 对象作为参数,传入容器组件。但是,这样做比较麻烦,尤其是容器组件可能在很深的层级,一级一级将 state 传下去就很麻烦。

    React-Redux 提供了 Provider 组件,可以让容器组件拿到 state

    import { Provider } from 'react-redux'
    import { createStore } from 'redux'
    import todoApp from './reducers'
    import App from './components/App'
    
    let store = createStore(todoApp)
    
    render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    )
    

    上述代码中,Provider 在根组件外面包了一层,这样一来,App 的所有子组件就默认都可以拿到 state 了。

    它的原理是 React 组件的 context 属性,请看源码:

    class Provider extends Component {
      getChildContext() {
        return {
          store: this.props.store
        }
      }
      render() {
        return this.props.children
      }
    }
    
    Provider.childContextTypes = {
      store: React.PropTypes.object
    }
    

    上面的代码中,store 放在了上下文对象 context 上面。然后,子组件就可以从 context 拿到 store,代码大致如下:

    class VisibleTodoList extends Component {
      componentDidMount() {
        const { store } = this.context
        this.unsubscribe = store.subscribe(() =>
          this.forceUpdate()
        )
      }
    
      render() {
        const props = this.props
        const { store } = this.context
        const state = store.getState()
        // ...
      }
    }
    
    VisibleTodoList.contextTypes = {
      store: React.PropTypes.object
    }
    

    React-Redux 自动生成的容器组件的代码,就类似上面这样,从而拿到 store

    React-Redux 简单案例

    首先,上述的代码与本项目没有关联,其实只是为了讲解 React-Redux 相关 API 的使用。

    下面我们将结合我们项目来实现一个简单的案例。

    1. 安装 react-redux
    $ yarn add react-redux@7.1.3
    
    2. 使用 Provider 包裹我们的根组件 App
    // pages/Root.js
    import React from 'react'
    import { Provider } from 'react-redux'
    import App from './pages/App'
    import store from './store'
    
    const Root = () => {
      return (
        <Provider store={store}>
          <App />
        </Provider>
      )
    }
    
    export default Root
    
    3. 调整我们的 store 以及 reducer

    实际情况下,store 应该是一个对象,因为它存储的数据可能会很多很复杂。此前为了用最简单案例来讲解 store,所以我们将它设置为一个 Number 类型的值。

    下面我们修改一下,初始值设置为 { count: 0 },并修改 reducer 处理函数。

    // store/index.js
    import { createStore } from 'redux'
    
    // Reducer 处理函数
    const reducer = (prevState, action) => {
      const { type, payload } = action
      switch (type) {
        case 'ADD':
          // 一定要不能修改 state,而是返回一个新的副本
          // 倘若 state 是引用数据类型,一定要借助 Object.assign、对象展开运算符(...)、其他库的拷贝方法或者自己实现深拷贝方法,返回一个新副本
          return { ...prevState, count: prevState.count + payload }
        case 'SUB':
          return { ...prevState, count: prevState.count - payload }
        default:
          // default 或者未知 action 时,返回旧的 state
          return prevState
      }
    }
    
    // 初始化值
    const initialState = { count: 0 }
    
    // 创建 Store(也可以不传入 initialState 参数,而将 reducer 中的 state 设置一个初始值)
    const store = createStore(reducer, initialState)
    
    // 监听 state 变化
    // const unsubscribe = store.subscribe(() => {
    //   console.log('监听 state 变化', store.getState())
    // })
    
    // 解除监听
    // unsubscribe()
    
    export default store
    
    4. 我们在 Home 组件引入 connect 方法
    // pages/home/index.js
    import React, { Component } from 'react'
    import { connect } from 'react-redux';
    import store from '../../store'
    
    class Home extends Component {
      constructor(props) {
        super(props)
        this.state = {}
      }
    
      handle(type, val) {
        this.props.simpleDispatch(type, val)
        // 获取 State 快照
        console.log(`当前操作是 ${type},count 为:${store.getState().count}`)
      }
    
      render() {
        return (
          <div>
            <h3>Home Component!</h3>
            {/* 将 state 展示到页面上 */}
            <h5>count:{this.props.count}</h5>
            <button onClick={this.handle.bind(this, 'ADD', 1)}>加一</button>
            <button onClick={this.handle.bind(this, 'SUB', 1)}>减一</button>
          </div>
        )
      }
    }
    
    // 将 count 映射到 Home 组件的 props 属性上,通过 this.props.count 即可访问到它。
    const mapStateToProps = (state, ownProps) => {
      return { count: state.count }
    }
    
    // 同理,它将 simpleDispatch 映射到组件的 props 属性上,通过 this.props.simpleDispatch 访问并由 Redux 发出一个 Action。
    const mapDispatchToProps = (dispatch, ownProps) => {
      return {
        simpleDispatch: (type, payload) => {
          dispatch({ type, payload })
        }
      }
    }
    
    // 若忽略 mapStateToProps 参数,store 的更新将不会触发组件重新渲染
    // 若忽略 mapDispatchToProps 参数,默认情况下,store.dispatch 会注入组件 props 中。
    // 若指定了,你就不能通过 this.props.dispatch 来发出 Action 了。
    export default connect(mapStateToProps, mapDispatchToProps)(Home)
    
    5. 效果

    我们看到了 store 的变化,将会反映到页面上。

    🎉

    至此

    我们的 Redux 最简单的环境已经搭建好了,你学会了吗?但是,实际项目中,这可能远远不够...

    这里抛出几个问题:

    • 大型应用的 Reducer 不会那么简单,那么我们如何拆分呢?
    • 如何让 Reducer 在异步操作结束之后,自动执行呢?
    • 如何利用一些第三方库或者插件来观察 store 的变化?
    1. 拆分 Reducer 我们使用 Redux 提供的 combineReducers 来处理。

    2. 解决异步操作自动执行 Reducer 的中间件常用的用 redux-thunkredux-promiseredux-saga 等。我们项目将会采用 redux-sage,后续文章会讲解。

    3. 观察 store 可以利用 redux-logger 中间件或者 Redux DevTools 浏览器插件(Google Chrome 浏览器的话需要科学上网下载)。

    由于本文篇幅以及很长了,就下一篇接介绍吧。

    相关文章

      网友评论

        本文标题:从零到一搭建 react 项目系列之(八)

        本文链接:https://www.haomeiwen.com/subject/palesktx.html