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倉庫加星❤️哦~

相關文章
相關標籤/搜索