Taro 小程序开发大型实战(四):使用 Hooks 版的 Redux 实现应用状态管理(上篇)

欢迎继续阅读《Taro 小程序开发大型实战》系列,前情回顾:css

若是你跟着敲到了这里,你必定会发现如今 的状态管理和数据流愈来愈臃肿,组件状态的更新很是复杂。在这一篇中,咱们将开始用 Redux 重构,由于这次重构涉及的改动文件有点多,因此这一步使用 Redux 重构咱们分两篇文章来说解,这篇是上篇。react

若是你不熟悉 Redux,推荐阅读咱们的《Redux 包教包会》系列教程:git

若是你但愿直接从这一步开始,请运行如下命令:github

git clone -b redux-start https://github.com/tuture-dev/ultra-club.git
cd ultra-club
本文所涉及的源代码都放在了 Github 上,若是您以为咱们写得还不错,但愿您能给❤️这篇文章点赞+Github仓库加星❤️哦~

双剑合璧:Hooks + Redux

写到这一步,咱们发现状态已经有点多了,并且 src/pages/mine/mine.jsx 文件是众多状态的顶层组件,好比咱们的普通登陆按钮 src/components/LoginButton/index.jsx 组件和咱们的 src/components/Footer/index.jsx 组件,咱们经过点击普通登陆按钮打开登陆弹窗的状态 isOpened 须要在 LoginButton 里面进行操做,而后进而影响到 Footer 组件内的 FloatLayout 弹窗组件,像这种涉及到多个子组件进行通讯,咱们将状态保存到公共父组件中的方式在 React 中叫作 ”状态提高“。npm

可是随着状态增多,状态提高的状态也随着增多,致使保存这些状态的父组件会臃肿不堪,并且每次状态的改变须要影响不少中间组件,带来极大的性能开销,这种状态管理的难题咱们通常交给专门的状态管理容器 Redux 来作,而让 React 专一于渲染用户界面。redux

Redux 不只能够保证状态的可预测性,还能保证状态的变化只和对应的组件相关,不影响到无关的组件,关于 Redux 的详细剖析的实战教程能够参考图雀社区的:Redux 包教包会系列文章小程序

在这一节中,咱们将结合 React Hooks 和 Redux 来重构咱们状态管理。segmentfault

安装依赖

首先咱们先来安装使用 Redux 必要的依赖:数组

$ yarn add redux @tarojs/redux @tarojs/redux-h5  redux-logger
# 或者使用 npm
$ npm install --save redux @tarojs/redux @tarojs/redux-h5 redux-logger

除了咱们熟悉的 redux 依赖,以及用来打印 Action 的中间件 redux-logger 外,还有两个额外的包,这是由于在 Taro 中,Redux 原绑定库 react-redux 被替换成了 @tarojs/redux@tarojs/redux-h5,前者用在小程序中,后者用在 H5 页面中,Taro 对原 react-redux 进行了封装并提供了与 react-redux API 几乎一致的包来让开发人员得到更加良好的开发体验。缓存

建立 Redux Store

Redux 的三大核心概念为:Store,Action,Reducers:

  • Store:保存着全局的状态,有着 ”数据的惟一真相来源之称“。
  • Action:发起修改 Store 中保存状态的动做,是修改状态的惟一手段。
  • Reducers:一个个的纯函数,用于响应 Action,对 Store 中的状态进行修改。

好的,复习了一下 Redux 的概念以后,咱们立刻来建立 Store,Redux 的最佳实践推荐咱们在将 Store 保存在 store 文件夹中,咱们在 src 文件夹下面建立 store 文件夹,并在其中建立 index.js 来编写咱们的 Store:

import { createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
import rootReducer from '../reducers'

const middlewares = [createLogger()]

export default function configStore() {
  const store = createStore(rootReducer, applyMiddleware(...middlewares))
  return store
}

能够看到,咱们导出了一个 configureStore 函数,并在其中建立并返回 Store,这里咱们用到了 redux-logger 中间件,用于在发起 Action 时,在控制台打印 Action 及其先后 Store 中的保存的状态信息。

这里咱们的 createstore 接收两个参数:rootReducerapplyMiddleware(...middlewares)

rootReducer 是响应 actionreducer,这里咱们导出了一个 rootReducer,表明组合了全部的 reducer ,咱们将在后面 "组合 User 和 Post Reducer“ 中讲到它。

createStore 函数的第二个参数咱们使用了 redux 为咱们提供的工具函数 applyMiddleware 来在 Redux 中注入须要使用的中间件,由于它接收的参数是 (args1, args2, args3, ..., argsn) 的形式,因此这里咱们用了数组展开运算符 ... 来展开 middlewares 数组。

编写 User Reducer

建立完 Store 以后,咱们接在来编写 Reducer。回到咱们的页面逻辑,咱们在底部有两个 Tab 栏,一个为 "首页",一个为 "个人",在 ”首页“ 里面主要是展现一列文章和容许添加文章等,在 ”个人“ 里面主要是容许用户进行登陆并展现登陆信息,因此总体上咱们的逻辑有两类,咱们分别将其命名为 postuser,接下来咱们将建立处理这两类逻辑的 reducers。

Reducer 的逻辑形如 (state, action) => newState,即接收上一步 state 以及修改 state 的动做 action,而后返回修改后的新的 state,它是一个纯函数,意味着咱们不能突变的修改 state。

推荐:

newState = { ...state, prop: newValue }

不推荐:

state.prop = newValue

Redux 推荐的最佳实践是建立独立的 reducers 文件夹,在里面保存咱们的一个个 reducer 文件。咱们在 src 文件夹下建立 reducers 文件夹,在里面建立 user.js 文件,并加入咱们的 User Reducer 相应的内容以下:

import { SET_LOGIN_INFO, SET_IS_OPENED } from '../constants/'

const INITIAL_STATE = {
  avatar: '',
  nickName: '',
  isOpened: false,
}

export default function user(state = INITIAL_STATE, action) {
  switch (action.type) {
    case SET_IS_OPENED: {
      const { isOpened } = action.payload

      return { ...state, isOpened }
    }

    case SET_LOGIN_INFO: {
      const { avatar, nickName } = action.payload

      return { ...state, nickName, avatar }
    }

    default:
      return state
  }
}

咱们在 user.js 中申明了 User Reducer 的初始状态 INITIAL_STATE,并将它赋值给 user 函数 state 的默认值,它接收待响应的 action,在 user 函数内部就是一个 switch 语句根据 action.type 进行判断,而后执行相应的逻辑,这里咱们主要有两个类型:SET_IS_OPENED 用于修改 isOpened 属性,SET_LOGIN_INFO 用于修改 avatarnickName 属性,当 switch 语句中没有匹配到任何 action.type 值时,它返回原 state。

提示

根据 Redux 最近实践,这里的 SET_IS_OPENEDSET_LOGIN_INFO 常量通常保存到 constants 文件夹中,咱们将立刻建立它。这里使用常量而不是直接硬编码字符串的目的是为了代码的可维护性。

接下来咱们来建立 src/reducer/user.js 中会用到的常量,咱们在 src 文件夹下建立 constants 文件夹,并在其中建立 user.js 文件,在其中添加内容以下:

export const SET_IS_OPENED = 'MODIFY_IS_OPENED'
export const SET_LOGIN_INFO = 'SET_LOGIN_INFO'

编写 Post Reducer

为了响应 post 逻辑的状态修改,咱们建立在 src/reducers 下建立 post.js,并在其中编写相应的内容以下:

import { SET_POSTS, SET_POST_FORM_IS_OPENED } from '../constants/'

import avatar from '../images/avatar.png'

const INITIAL_STATE = {
  posts: [
    {
      title: '泰罗奥特曼',
      content: '泰罗是奥特之父和奥特之母惟一的亲生儿子',
      user: {
        nickName: '图雀酱',
        avatar,
      },
    },
  ],
  isOpened: false,
}

export default function post(state = INITIAL_STATE, action) {
  switch (action.type) {
    case SET_POSTS: {
      const { post } = action.payload
      return { ...state, posts: state.posts.concat(post) }
    }

    case SET_POST_FORM_IS_OPENED: {
      const { isOpened } = action.payload

      return { ...state, isOpened }
    }

    default:
      return state
  }
}

能够看到,Post Reducer 的形式和 User Reducer 相似,咱们将以前须要多组件中共享的状态 postsisOpened 提取出来保存在 post 的状态里,这里的 post 函数主要响应 SET_POSTS 逻辑,用于添加新的 postposts 状态种,以及 SET_POST_FORM_IS_OPENED 逻辑,用户设置 isOpened 状态。

接下来咱们来建立 src/reducer/post.js 中会用到的常量,咱们在 src/constants 文件夹下建立 user.js 文件,在其中添加内容以下:

export const SET_POSTS = 'SET_POSTS'
export const SET_POST_FORM_IS_OPENED = 'SET_POST_FORM_IS_OPENED'

眼尖的同窗可能注意到了,咱们在 src/reducers/user.jssrc/reducers/post.js 中导入须要使用的常量时都是从 ../constants 的形式,那是由于咱们在 src/constants 文件夹下建立了一个 index.js 文件,用于统一导出全部的常量,这也是代码可维护性的一种尝试。

export * from './user'
export * from './post'

组合 User 和 Post Reducer

咱们在以前将整个全局的响应逻辑分别拆分到了 src/reducers/user.jssrc/reducers/post.js 中,这使得咱们能够把响应逻辑拆分到不少个很小的函数单元,极大增长了代码的可读性和可维护性。

但最终咱们仍是要将这些拆分的逻辑组合成一个逻辑树,并将其做为参数传给 createStore 函数来使用。

Redux 为咱们提供了 combineReducers 来组合这些拆分的逻辑,咱们在 src/reducers 文件夹下建立 index.js 文件,并在其中编写以下内容:

import { combineReducers } from 'redux'

import user from './user'
import post from './post'

export default combineReducers({
  user,
  post,
})

能够看到,咱们导入了 user.jspost.js,并使用对象简介写法传给 combineReducers 函数并导出,经过 combineReducers 将逻辑进行组合并导出为 rootReducer 做为参数在咱们的 src/store/index.jscreateStore 函数中使用。

这里的 combineReducers 函数主要完成两件事:

  • 组合 user Reducer 和 post Reducer 中的状态,并将其合并成一颗形如 { user, post } 的状态树,其中 user 属性保存这 user Reducer 的状态,post 属性保存着 post Reducer 的状态。
  • 分发 Action,当组件中 dispatch 一个 Action, combineReducers 会遍历 user Reducer 和 post Reducer,当匹配到任一 Reducer 的 switch 语句时,就会响应这个 Action。
提示

咱们将立刻在以后讲解如何在组件中 dispatch Action。

整合 Redux 和 React

当咱们编写了 reducers 建立了 store 以后,下一步要考虑的就是如何将 Redux 整合进 React,咱们打开 src/app.js,对其中的内容做出以下修改:

import Taro, { Component } from '@tarojs/taro'
import { Provider } from '@tarojs/redux'

import configStore from './store'
import Index from './pages/index'
import './app.scss'

// ...

const store = configStore()

class App extends Component {
  config = {
    // ...
  }

  render() {
    return (
      <Provider store={store}>
        <Index />
      </Provider>
    )
  }
}

Taro.render(<App />, document.getElementById('app'))

能够看到,上面的内容主要修改了三部分:

  • 咱们导入了 configureStore,并调用它获取 store
  • 接着咱们从 Redux 对应的 Taro 绑定库 @tarojs/redux 中导出 Provider,它架设起 Redux 和 React 交流的桥梁。
  • 最后咱们用 Provider 包裹咱们以前的根组件,并将 store 做为其属性传入,这样后续的组件就能够经过获取到 store 里面保存的状态。

Hooks 版的 Action 初尝鲜

准备好了 Store 和 Reducer,又整合了 Redux 和 React,是时候来体验一下 Redux 状态管理容器的先进性了,不过为了使用 Hooks 版本的 Action,这里咱们先来说一讲会用到的 Hooks。

useDispatch Hooks

这个 Hooks 返回 Redux store 的 dispatch 引用。你可使用它来 dispatch actions。

讲完 useDispatch Hooks,咱们立刻来实践一波,首先搞定咱们 ”普通登陆“ 的 Redux 化问题,让咱们打开 src/components/LoginButton/index.js,对其中内容做出相应的修改以下:

import Taro from '@tarojs/taro'
import { AtButton } from 'taro-ui'
import { useDispatch } from '@tarojs/redux'

import { SET_IS_OPENED } from '../../constants'

export default function LoginButton(props) {
  const dispatch = useDispatch()

  return (
    <AtButton
      type="primary"
      onClick={() =>
        dispatch({ type: SET_IS_OPENED, payload: { isOpened: true } })
      }
    >
      普通登陆
    </AtButton>
  )
}

能够看到,上面的内容主要有四块改动:

  • 首先咱们从 @tarojs/redux 中导出 useDispatch API。
  • 接着咱们从以前定义的常量文件中导出 SET_IS_OPENED 常量。
  • 而后,咱们在 LoginButton 函数式组件中调用 useDispatch Hooks 来返回咱们的 dispatch 函数,咱们能够用它来 dispatch action 来修改 Redux store 的状态
  • 最后咱们将 AtButtononClick 接收的回调函数进行替换,当按钮点击时,咱们发起一个 typeSET_IS_OPENED 的 action,并传递了一个 payload 参数,用于将 Redux store 里面对应的 user 属性中的 isOpened 修改成 true

搞定完 ”普通登陆“,咱们接着来收拾一下 ”微信登陆“ 的逻辑,打开 src/components/WeappLoginButton/index.js 文件,对文件的内容做出以下修改:

import Taro, { useState } from '@tarojs/taro'
import { Button } from '@tarojs/components'
import { useDispatch } from '@tarojs/redux'

import './index.scss'
import { SET_LOGIN_INFO } from '../../constants'

export default function WeappLoginButton(props) {
  const [isLogin, setIsLogin] = useState(false)

  const dispatch = useDispatch()

  async function onGetUserInfo(e) {
    setIsLogin(true)

    const { avatarUrl, nickName } = e.detail.userInfo

    await Taro.setStorage({
      key: 'userInfo',
      data: { avatar: avatarUrl, nickName },
    })

    dispatch({
      type: SET_LOGIN_INFO,
      payload: {
        avatar: avatarUrl,
        nickName,
      },
    })

    setIsLogin(false)
  }

  // return ...
}

能够看到,上面的改动和以前在 ”普通登陆“ 里面的改动相似:

  • 咱们导出了 useDispatch 钩子
  • 导出了 SET_LOGIN_INFO 常量
  • 而后咱们将以前调用父组件传下的 setLoginInfo 方法改为了 dispatch typeSET_LOGIN_INFO 的 action,由于咱们的 avatarnickName 状态已经在 store 中的 user 属性中定义了,因此咱们修改也是须要经过 dispatch action 来修改,最后咱们将以前定义在父组件中的 Taro.setStorage 设置缓存的方法移动到了子组件中,以保证相关信息的改动具备一致性。

最后咱们来搞定 ”支付宝登陆“ 的 Redux 逻辑,打开 src/components/AlipayLoginButton/index.js 对文件内容做出对应的修改以下:

import Taro, { useState } from '@tarojs/taro'
import { Button } from '@tarojs/components'
import { useDispatch } from '@tarojs/redux'

import './index.scss'
import { SET_LOGIN_INFO } from '../../constants'

export default function AlipayLoginButton(props) {
  const [isLogin, setIsLogin] = useState(false)
  const dispatch = useDispatch()

  async function onGetAuthorize(res) {
    setIsLogin(true)
    try {
      let userInfo = await Taro.getOpenUserInfo()

      userInfo = JSON.parse(userInfo.response).response
      const { avatar, nickName } = userInfo

      await Taro.setStorage({
        key: 'userInfo',
        data: { avatar, nickName },
      })

      dispatch({
        type: SET_LOGIN_INFO,
        payload: {
          avatar,
          nickName,
        },
      })
    } catch (err) {
      console.log('onGetAuthorize ERR: ', err)
    }

    setIsLogin(false)
  }

  // return ...
}

能够看到,上面的改动和以前在 ”微信登陆“ 里面的改动几乎同样,因此这里咱们就不在重复讲解啦 :)

useSelector Hooks 来捧场

一路跟下来的同窗可能有点明白咱们正在使用 Redux 咱们以前的代码,而咱们重构的思路也是先从 src/pages/mine/mine.jsx 中的 src/components/Header/index.jsx 开始,搞定完 Header.jsx 里面的全部登陆按钮以后,接下来应该就轮到 Header.jsx 内的最后一个组件 src/components/LoggedMine/index.jsx 了。

由于在 LoggedMine 组件中咱们要用到 useSelector Hooks,因此这里咱们先来说一下这个 Hooks。

useSelector Hooks

useSelector 容许你使用 selector 函数从一个 Redux Store 中获取数据。

Selector 函数大体至关于 connect 函数的 mapStateToProps 参数。Selector 会在组件每次渲染时调用。useSelector 一样会订阅 Redux store,在 Redux action 被 dispatch 时调用。

useSelector 仍是和 mapStateToProps 有一些不一样:

  • 不像 mapStateToProps 只返回对象同样,Selector 可能会返回任何值。
  • 当一个 action dispatch 时,useSelector 会把 selector 的先后返回值作一次浅对比,若是不一样,组件会强制更新。
  • Selector 函数不接受 ownProps 参数。但 selector 能够经过闭包访问函数式组件传递下来的 props。

好的,了解了 useSelector 的概念以后,咱们立刻来实操一下,打开 src/components/LoggedMine/index.jsx 文件,对其中的内容做出以下的修改:

import Taro from '@tarojs/taro'
import { View, Image } from '@tarojs/components'
import { useSelector } from '@tarojs/redux'
import { AtAvatar } from 'taro-ui'

import './index.scss'

export default function LoggedMine(props) {
  const nickName = useSelector(state => state.user.nickName)
  const avatar = useSelector(state => state.user.avatar)

  function onImageClick() {
    Taro.previewImage({
      urls: [avatar],
    })
  }

  return (
    <View className="logged-mine">
      {avatar ? (
        <Image src={avatar} className="mine-avatar" onClick={onImageClick} />
      ) : (
        <AtAvatar size="large" circle text="雀" />
      )}
      <View className="mine-nickName">{nickName}</View>
    </View>
  )
}

能够看到,咱们上面的代码主要有四处改动:

  • 首先咱们从 @tarojs/redux 中导出了 useSelector Hooks。
  • 接着咱们使用了两次 useSelector 分别从 Redux Store 里面获取了 nickNameavatar,它们位于 state.user 属性下。
  • 接着咱们将以前从 props 里面获取到的 nickNameavatar 替换成咱们从 Redux store 里面获取到状态,这里咱们为了用户体验,从 taro-ui 中导出了一个 AtAvatar 组件用于展现在没有 avatar 时的默认头像。
  • 最后,在点击头像进行预览的 onImageClick 方法里面,咱们使用从 Redux store 里面获取到的 avatar

是时候收割最后一波 ”韭菜“ 了,让咱们完全完成 Header/index.js 的 Redux 化,打开 src/components/Header/index.js ,对其中的内容作出相应的修改以下:

// ...
import { useSelector } from '@tarojs/redux'

// import 各类组件 ...

export default function Header(props) {
  const nickName = useSelector(state => state.user.nickName)

  // 双取反来构造字符串对应的布尔值,用于标志此时是否用户已经登陆
  const isLogged = !!nickName

  const isWeapp = Taro.getEnv() === Taro.ENV_TYPE.WEAPP
  const isAlipay = Taro.getEnv() === Taro.ENV_TYPE.ALIPAY

  return (
    <View className="user-box">
      <AtMessage />
      <LoggedMine />
      {!isLogged && (
        <View className="login-button-box">
          <LoginButton />
          {isWeapp && <WeappLoginButton />}
          {isAlipay && <AlipayLoginButton />}
        </View>
      )}
    </View>
  )
}

能够看到,上面的代码主要有五处主要的变更:

  • 首先咱们导出了 useSelector Hooks。
  • 接着咱们使用 useSelector 中取到咱们须要的 nickName 属性,用于进行双取反转换成布尔值 isLogged,表示是否登陆。
  • 接着咱们将以前从父组件获取的 props.isLogged 属性替换成新的从 isLogged
  • 接着,咱们去掉 ”普通登陆” 按钮上再也不须要的 handleClick 属性和 “微信登陆”、“支付宝登陆” 上面再也不须要的 setLoginInfo 属性。
  • 最后,咱们去掉 LoggedMine 组件上再也不须要的 userInfo 属性,由于咱们已经在组件内部从使用 useSelector Hooks 从组件内部获取了。

小结

在这一篇文章中,咱们讲解了 user 逻辑的状态管理的重构,受限于篇幅,咱们的 user 逻辑还剩下 Footer 部分没有讲解,在下一篇中,咱们将首先讲解使用 Hooks 版的 Redux 来重构 Footer 组件的状态管理,接着,咱们再来说解重构 post 部分的状态管理。

想要学习更多精彩的实战技术教程?来 图雀社区逛逛吧。

本文所涉及的源代码都放在了 Github 上,若是您以为咱们写得还不错,但愿您能给❤️这篇文章点赞+Github仓库加星❤️哦~

相关文章
相关标签/搜索