歡迎繼續閱讀《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倉庫加星❤️哦~
寫到這一步,咱們發現狀態已經有點多了,並且 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,Action,Reducers:
好的,複習了一下 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
接收兩個參數:rootReducer
和 applyMiddleware(...middlewares)
。
rootReducer
是響應 action
的 reducer
,這裏咱們導出了一個 rootReducer
,表明組合了全部的 reducer
,咱們將在後面 "組合 User 和 Post Reducer「 中講到它。
createStore
函數的第二個參數咱們使用了 redux
爲咱們提供的工具函數 applyMiddleware
來在 Redux 中注入須要使用的中間件,由於它接收的參數是 (args1, args2, args3, ..., argsn)
的形式,因此這裏咱們用了數組展開運算符 ...
來展開 middlewares
數組。
建立完 Store 以後,咱們接在來編寫 Reducer。回到咱們的頁面邏輯,咱們在底部有兩個 Tab 欄,一個爲 "首頁",一個爲 "個人",在 」首頁「 裏面主要是展現一列文章和容許添加文章等,在 」個人「 裏面主要是容許用戶進行登陸並展現登陸信息,因此總體上咱們的邏輯有兩類,咱們分別將其命名爲 post
和 user
,接下來咱們將建立處理這兩類邏輯的 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
用於修改 avatar
和 nickName
屬性,當 switch
語句中沒有匹配到任何 action.type
值時,它返回原 state。
提示根據 Redux 最近實踐,這裏的
SET_IS_OPENED
和SET_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
邏輯的狀態修改,咱們建立在 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 相似,咱們將以前須要多組件中共享的狀態 posts
和 isOpened
提取出來保存在 post
的狀態裏,這裏的 post
函數主要響應 SET_POSTS
邏輯,用於添加新的 post
到 posts
狀態種,以及 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.js
和 src/reducers/post.js
中導入須要使用的常量時都是從 ../constants
的形式,那是由於咱們在 src/constants
文件夾下建立了一個 index.js
文件,用於統一導出全部的常量,這也是代碼可維護性的一種嘗試。
export * from './user' export * from './post'
咱們在以前將整個全局的響應邏輯分別拆分到了 src/reducers/user.js
和 src/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.js
和 post.js
,並使用對象簡介寫法傳給 combineReducers
函數並導出,經過 combineReducers
將邏輯進行組合並導出爲 rootReducer
做爲參數在咱們的 src/store/index.js
的 createStore
函數中使用。
這裏的 combineReducers
函數主要完成兩件事:
{ user, post }
的狀態樹,其中 user
屬性保存這 user Reducer 的狀態,post
屬性保存着 post Reducer 的狀態。dispatch
一個 Action, combineReducers
會遍歷 user Reducer 和 post Reducer,當匹配到任一 Reducer 的 switch
語句時,就會響應這個 Action。提示咱們將立刻在以後講解如何在組件中
dispatch
Action。
當咱們編寫了 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
。@tarojs/redux
中導出 Provider
,它架設起 Redux 和 React 交流的橋樑。Provider
包裹咱們以前的根組件,並將 store
做爲其屬性傳入,這樣後續的組件就能夠經過獲取到 store
裏面保存的狀態。準備好了 Store 和 Reducer,又整合了 Redux 和 React,是時候來體驗一下 Redux 狀態管理容器的先進性了,不過爲了使用 Hooks 版本的 Action,這裏咱們先來說一講會用到的 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 的狀態AtButton
的 onClick
接收的回調函數進行替換,當按鈕點擊時,咱們發起一個 type
爲 SET_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 type
爲 SET_LOGIN_INFO
的 action,由於咱們的 avatar
和 nickName
狀態已經在 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 ... }
能夠看到,上面的改動和以前在 」微信登陸「 裏面的改動幾乎同樣,因此這裏咱們就不在重複講解啦 :)
一路跟下來的同窗可能有點明白咱們正在使用 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
容許你使用 selector 函數從一個 Redux Store 中獲取數據。
Selector 函數大體至關於 connect
函數的 mapStateToProps
參數。Selector 會在組件每次渲染時調用。useSelector
一樣會訂閱 Redux store,在 Redux action 被 dispatch 時調用。
但 useSelector
仍是和 mapStateToProps
有一些不一樣:
mapStateToProps
只返回對象同樣,Selector 可能會返回任何值。useSelector
會把 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 裏面獲取了 nickName
和 avatar
,它們位於 state.user
屬性下。props
裏面獲取到的 nickName
和 avatar
替換成咱們從 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倉庫加星❤️哦~