- 原文地址:Redux 4 Ways
- 原文做者:本篇文章已得到做者 Nader Dabit 受權
- 譯文出自:掘金翻譯計劃
- 譯者:reid3290
- 校對者:rccoder,xekri
在上一次的 React Native online meetup 活動中,筆者就 Thunk 、 Saga 以及 Redux Observable 之間的不一樣之處作了報告 (點擊此處獲取幻燈片) 。javascript
上述函數庫都提供了一些方法用以處理 Redux 應用中帶有反作用的或者是異步的 action。更多關於爲何要用到這些庫的介紹,請 點擊此處 。html
相較於僅僅是建立一個倉庫,而後查看和測試這些庫的實現方法,筆者但願更進一步,即一步步地弄清這些庫是如何解決異步在 Redux 中產生的反作用,並額外增長一種方案 —— Redux Promise Middleware 。java
筆者第一次接觸 Redux 的時候,就被這些異步的、帶有反作用的函數庫搞得「頭昏腦脹」。 雖然相關文檔還算齊全,但仍是但願可以結合實際項目去深刻理解這些函數庫是如何解決 Redux 中的異步問題。從而快速上手,避免浪費過多時間。react
在本教程中,筆者將應用上述函數庫,一步步地實現一個拉取數據並將數據存儲在 reducer 中的簡單例子。android
如圖所示,上述函數庫最通用的模式之一就是發起一個 API 請求,顯示加載圖標,數據返回後展現結果(若是出現錯誤則展現錯誤信息)。筆者將依次使用上述 4 個函數庫實現該功能。ios
在本例中筆者將使用 React Native,固然使用 React 也是徹底同樣的 —— 只須要把 View
替換爲 div
, 把 Text
替換爲 p
便可。 在本節中,筆者將僅僅實現一個簡單的 Redux 示例應用,以展現上述 4 個函數庫的用法。git
首先運行 react-native init 命令建立一個空項目:github
react-native init redux4ways複製代碼
固然也可使用 create-react-app:redux
create-react-app redux4ways複製代碼
而後進入項目目錄:c#
cd redux4ways複製代碼
安裝所需依賴:
yarn add redux react-redux redux-thunk redux-observable redux-saga rxjs redux-promise-middleware複製代碼
建立將要用到的相關目錄和文件:
mkdir reducers複製代碼
touch reducers/index.js reducers/dataReducer.js複製代碼
touch app.js api.js configureStore.js constants.js actions.js複製代碼
至此,全部依賴都已安裝完畢,相關文件業已新建穩當,能夠着手編碼開發了。
首先將 index.ios
(ios) 或 index.android.js
(android) 中的代碼更新以下:
import React from 'react'
import {
AppRegistry
} from 'react-native'
import { Provider } from 'react-redux'
import configureStore from './configureStore'
import App from './app'
const store = configureStore()
const ReduxApp = () => (
<Provider store={store}> <App /> </Provider>
)複製代碼
react-redux
中引入 Provider
。configureStore
,隨後將建立該文件。App
做爲本例應用中的入口組件。configureStore()
方法建立 store。App
包裹在 Provider
中並傳入上述 store。接着建立 actions 和 reducer 所涉及的相關常量,constants.js
文件內容以下:
export const FETCHING_DATA = 'FETCHING_DATA'
export const FETCHING_DATA_SUCCESS = 'FETCHING_DATA_SUCCESS'
export const FETCHING_DATA_FAILURE = 'FETCHING_DATA_FAILURE'複製代碼
再接着建立 dataReducer
,dataReducer.js
文件內容以下:
import { FETCHING_DATA, FETCHING_DATA_SUCCESS, FETCHING_DATA_FAILURE } from '../constants'
const initialState = {
data: [],
dataFetched: false,
isFetching: false,
error: false
}
export default function dataReducer (state = initialState, action) {
switch (action.type) {
case FETCHING_DATA:
return {
...state,
data: [],
isFetching: true
}
case FETCHING_DATA_SUCCESS:
return {
...state,
isFetching: false,
data: action.data
}
case FETCHING_DATA_FAILURE:
return {
...state,
isFetching: false,
error: true
}
default:
return state
}
}複製代碼
initialState
是一個對象,該對象由 1 個數組 data
和 3 個布爾類型的變量:dataFetched
、isFetching
以及 error
構成。FETCHING_DATA_SUCCESS
, 則將新數據添加到狀態對象中並將 isFetching
設爲 false
。接下來須要建立 reducer 的入口文件,在該文件中會對全部的 reducers 調用 combineReducers
方法(在本例中只有一個 reducer,即 dataReducer.js
)。
reducers/index.js
文件內容以下:
import { combineReducers } from 'redux'
import appData from './dataReducer'
const rootReducer = combineReducers({
appData
})
export default rootReducer複製代碼
以後則須要建立相應的 actions,actions.js
文件內容以下:
import { FETCHING_DATA, FETCHING_DATA_SUCCESS, FETCHING_DATA_FAILURE } from './constants'
export function getData() {
return {
type: FETCHING_DATA
}
}
export function getDataSuccess(data) {
return {
type: FETCHING_DATA_SUCCESS,
data,
}
}
export function getDataFailure() {
return {
type: FETCHING_DATA_FAILURE
}
}
export function fetchData() {}複製代碼
getData
、getDataSuccess
和 getDataFailure
)會直接返回 action,第 4 個(fetchData
)則會更新一個 thunk (具體實現見下文)。接着定義 configureStore:
import { createStore } from 'redux'
import app from './reducers'
export default function configureStore() {
let store = createStore(app)
return store
}複製代碼
./reducers
中引入 root reducer。最後, 對接頁面 UI 並綁定相應 props:
import React from 'react'
import { TouchableHighlight, View, Text, StyleSheet } from 'react-native'
import { connect } from 'react-redux'
import { fetchData } from './actions'
let styles
const App = (props) => {
const {
container,
text,
button,
buttonText
} = styles
return (
<View style={container}> <Text style={text}>Redux Examples</Text> <TouchableHighlight style={button}> <Text style={buttonText}>Load Data</Text> </TouchableHighlight> </View>
)
}
styles = StyleSheet.create({
container: {
marginTop: 100
},
text: {
textAlign: 'center'
},
button: {
height: 60,
margin: 10,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#0b7eff'
},
buttonText: {
color: 'white'
}
})
function mapStateToProps (state) {
return {
appData: state.appData
}
}
function mapDispatchToProps (dispatch) {
return {
fetchData: () => dispatch(fetchData())
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App)複製代碼
此處代碼不言自明 —— connect 方法用於將當前 Redux store 的狀態和引入的 actions 做爲 props 傳入目標展現性組件中,即此例中的 App
。
最後須要一個模擬的數據接口,該接口返回一個 promise,該 promise 會在 3 秒鐘後 reslove,並返回相應數據。對應文件 api.js
內容以下:
const people = [
{ name: 'Nader', age: 36 },
{ name: 'Amanda', age: 24 },
{ name: 'Jason', age: 44 }
]
export default () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
return resolve(people)
}, 3000)
})
}複製代碼
在該文件中,首先建立一個含有人員信息的數組,而後暴露一個實現了上述模擬接口功能的方法。
至此,Redux 已經和 React 鏈接了起來,接下來引入第一個異步函數庫 —— Redux Thunk。(branch)
首先須要建立一個 thunk
「Redux Thunk middleware 容許 action 建立函數返回一個函數而不是 action。 該中間件能夠用於延遲 action 的 dispatch 過程, 或僅當知足特定條件時才 dispatch action;其內部函數接受兩個參數:
dispatch
和getState
。 」 —— Redux Thunk 文檔
在 actions.js
文件中,更新函數 fetchData
並引入 api:
import { FETCHING_DATA, FETCHING_DATA_SUCCESS, FETCHING_DATA_FAILURE } from './constants'
import getPeople from './api'
export function getData() {
return {
type: FETCHING_DATA
}
}
export function getDataSuccess(data) {
return {
type: FETCHING_DATA_SUCCESS,
data,
}
}
export function getDataFailure() {
return {
type: FETCHING_DATA_FAILURE
}
}
export function fetchData() {
return (dispatch) => {
dispatch(getData())
getPeople()
.then((data) => {
dispatch(getDataSuccess(data))
})
.catch((err) => console.log('err:', err))
}
}
view raw複製代碼
此處 fetchData
函數是一個 thunk。當被調用時,fetchData 會返回一個函數;該函數首先會 dispatch getData
action,而後調用 getPeople
,在 getPeople
返回的 promise reslove 以後,會 dispatch getDataSuccess
action。
接下來,須要更新 configureStore
函數以引入 thunk 中間件:
import { createStore, applyMiddleware } from 'redux'
import app from './reducers'
import thunk from 'redux-thunk'
export default function configureStore() {
let store = createStore(app, applyMiddleware(thunk))
return store
}複製代碼
redux
引入 applyMiddleware。redux-thunk
引入 thunk
。applyMiddleware
做爲第二個參數傳遞給函數 createStore
。最後,更新 app.js
文件來使用上述 thunk:
import React from 'react'
import { TouchableHighlight, View, Text, StyleSheet } from 'react-native'
import { connect } from 'react-redux'
import { fetchData } from './actions'
let styles
const App = (props) => {
const {
container,
text,
button,
buttonText,
mainContent
} = styles
return (
<View style={container}> <Text style={text}>Redux Examples</Text> <TouchableHighlight style={button} onPress={() => props.fetchData()}> <Text style={buttonText}>Load Data</Text> </TouchableHighlight> <View style={mainContent}> { props.appData.isFetching && <Text>Loading</Text> } { props.appData.data.length ? ( props.appData.data.map((person, i) => { return <View key={i} > <Text>Name: {person.name}</Text> <Text>Age: {person.age}</Text> </View> }) ) : null } </View> </View>
)
}
styles = StyleSheet.create({
container: {
marginTop: 100
},
text: {
textAlign: 'center'
},
button: {
height: 60,
margin: 10,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#0b7eff'
},
buttonText: {
color: 'white'
},
mainContent: {
margin: 10,
}
})
function mapStateToProps (state) {
return {
appData: state.appData
}
}
function mapDispatchToProps (dispatch) {
return {
fetchData: () => dispatch(fetchData())
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App)複製代碼
此處代碼主要有如下幾個要點:
props.fetchData()
。props.appData.isFetching
的值是否爲 true, 若是是則返回正在加載的文字提示。props.appData.data.length
,若是該值存在且不爲 0,則遍歷該數組,展現人員姓名和年齡信息。至此,當按下按鈕 Load Data 後,首先會看到正在加載的提示文字,3 秒後會看到人員信息。
Redux Saga 組合使用 async await 和 Generators,使其函數接口簡單易用。(branch)
「經過使用 ES6 的新特性 Generators,涉及異步流程的代碼變得易於閱讀、編寫和測試。(若是你對此特性還不熟悉的話,點擊此處獲取入門介紹)。基於此,Javascript 的異步代碼看起來就和標準的同步代碼同樣(有點相似於
async
/await
,但 Generators 另外還有一些咱們所須要的極佳特性)。—— Redux Saga 文檔
爲了實現 Saga,首先須要更新 actions —— 刪除 actions.js
文件中除了以下代碼外的其它全部代碼:
import { FETCHING_DATA } from './constants'
export function fetchData() {
return {
type: FETCHING_DATA
}
}複製代碼
該 action 會觸發咱們即將建立的 saga。新建 saga.js
文件,寫入以下代碼:
import { FETCHING_DATA, FETCHING_DATA_SUCCESS, FETCHING_DATA_FAILURE } from './constants'
import { put, takeEvery } from 'redux-saga/effects'
import getPeople from './api'
function* fetchData (action) {
try {
const data = yield getPeople()
yield put({ type: FETCHING_DATA_SUCCESS, data })
} catch (e) {
yield put({ type: FETCHING_DATA_FAILURE })
}
}
function* dataSaga () {
yield takeEvery(FETCHING_DATA, fetchData)
}
export default dataSaga複製代碼
redux-saga/effects
中引入 put
和 takeEvery
。當調用 put
函數時,Reduc Sage 會指示中間件 dipatch 一個 action。takeEvery
函數則會監聽被 dispatch 了的 action(本例中即爲 FETCHING_DATA
),而後調用回調函數(本例中即爲 fetchData
)。fetchData
被調用後,代碼會等待函數 getPeople
的返回,若是返回成功則 dispatch FETCHING_DATA_SUCCCESS
action。最後更新 configureStore.js
文件,用 saga 替換 thunk。
import { createStore, applyMiddleware } from 'redux'
import app from './reducers'
import createSagaMiddleware from 'redux-saga'
import dataSaga from './saga'
const sagaMiddleware = createSagaMiddleware()
export default function configureStore() {
const store = createStore(app, applyMiddleware(sagaMiddleware))
sagaMiddleware.run(dataSaga)
return store
}複製代碼
在該文件中既引入了上述 saga,又從 redux-saga
中引入了 createSagaMiddleware
。在建立 store 時,傳入 sagaMiddleware
,而後在返回 store 以前調用 sagaMiddleWare.run
。
至此,能夠再次運行該程序並看到和使用 Redux Thunk 是一樣的效果!
注意:從 thunk 遷移到 saga 只改變了 3 個文件:
saga.js
、configureStore.js
以及actions.js
。
Redux Observable 使用 RxJS 和 observables 來爲 Redux 應用建立異步 action 和異步數據流。(branch)
「基於 RxJS 5 的 Redux 中間件。組合撤銷異步 actions 以產生反作用等。」 —— Redux Observable 文檔
首先仍是須要更新 actions.js 文件:
import { FETCHING_DATA, FETCHING_DATA_SUCCESS, FETCHING_DATA_FAILURE } from './constants'
export function fetchData () {
return {
type: FETCHING_DATA
}
}
export function getDataSuccess (data) {
return {
type: FETCHING_DATA_SUCCESS,
data
}
}
export function getDataFailure (error) {
return {
type: FETCHING_DATA_FAILURE,
errorMessage: error
}
}複製代碼
如上所示,將以前的 actions 更新爲最先的 3 個 actions。
接着建立所謂的 epic —— 輸入 action stream 並輸出 action stream 的函數。
新建 epic.js
文件並加入以下代碼:
import { FETCHING_DATA } from './constants'
import { getDataSuccess, getDataFailure } from './actions'
import getPeople from './api'
import 'rxjs'
import { Observable } from 'rxjs/Observable'
const fetchUserEpic = action$ =>
action$.ofType(FETCHING_DATA)
.mergeMap(action =>
Observable.fromPromise(getPeople())
.map(response => getDataSuccess(response))
.catch(error => Observable.of(getDataFailure(error)))
)
export default fetchUserEpic複製代碼
通常在 RxJS 中,變量名中的 $ 符號用以表示該變量是某 stream 的引用。
getDataSuccess
和 getDataFailure
函數。rxjs
和 Observable
。fetchUserEpic
。FETCHING_DATA
action 經過該 stream 以後,調用 mergeMap 函數, 從 getPeople
中返回 Observable.fromPromise
並將返回值映射到 getDataSuccess
函數中。最後,更新 configureStore,應用新中間件 —— epic。
configureStore.js
文件內容以下:
import { createStore, applyMiddleware } from 'redux'
import app from './reducers'
import { createEpicMiddleware } from 'redux-observable'
import fetchUserEpic from './epic'
const epicMiddleware = createEpicMiddleware(fetchUserEpic)
export default function configureStore () {
const store = createStore(app, applyMiddleware(epicMiddleware))
return store
}
view raw複製代碼
至此,能夠再次運行該程序並看到後以前同樣的效果!
Redux Promise Middleware 是一個用於 reslove 和 reject promise 的輕量級函數庫。 (branch)
「Redux Promise Middleware 使得 Redux 中的異步代碼更爲健壯,並使 optimistic updates 、dispatches pending 、fulfilled 和 rejected actions 成爲可能。 它也能夠和 redux-thunk 結合使用鏈式化異步 action」 —— Redux Promise Middleware 文檔
正如你將要看到的同樣,相比於上述幾個函數庫而言,Redux Promise Middleware 極大地減小了代碼量。
它也能夠和 Thunk 結合使用 以實現異步 action 的鏈式化。
相較於上述幾個函數庫,Redux Promise Middleware 有所不一樣 —— 它會接管你的 action 並基於 promise 狀態的不一樣在 action 類型名稱後添加 _PENDING
、_FULFILLED
或 _REJECTED
。
例如,若是調用以下函數:
function fetchData() {
return {
type: FETCH_DATA,
payload: getPeople()
}
}複製代碼
那麼就會自動地 dispatch FETCH_DATA_PENDING
action。
一旦 getPeople
promise resolved,基於返回結果的不一樣,會 dispatch FETCH_DATA_FULFILLED
或 FETCH_DATA_REJECTED
action。
讓咱們經過現有的例子來理解該特性:
首先須要更新 constants.js
,以使其匹配咱們將要用到的常量:
export const FETCH_DATA = 'FETCH_DATA'
export const FETCH_DATA_PENDING = 'FETCH_DATA_PENDING'
export const FETCH_DATA_FULFILLED = 'FETCH_DATA_FULFILLED'
export const FETCH_DATA_REJECTED = 'FETCH_DATA_REJECTED'複製代碼
接着將 actions.js
文件更新爲只有一個 FETCH_DATA
這一個 action。
import { FETCH_DATA } from './constants'
import getPeople from './api'
export function fetchData() {
return {
type: FETCH_DATA,
payload: getPeople()
}
}複製代碼
接着基於上面新定義的常量更新 dataReducer.js
文件:
import { FETCH_DATA_PENDING, FETCH_DATA_FULFILLED, FETCH_DATA_REJECTED } from '../constants'
const initialState = {
data: [],
dataFetched: false,
isFetching: false,
error: false
}
export default function dataReducer (state = initialState, action) {
switch (action.type) {
case FETCH_DATA_PENDING:
return {
...state,
data: [],
isFetching: true
}
case FETCH_DATA_FULFILLED:
return {
...state,
isFetching: false,
data: action.payload
}
case FETCH_DATA_REJECTED:
return {
...state,
isFetching: false,
error: true
}
default:
return state
}
}複製代碼
最後更新 configureStore
,應用 Redux Promise Middleware:
import { createStore, applyMiddleware } from 'redux'
import app from './reducers'
import promiseMiddleware from 'redux-promise-middleware';
export default function configureStore() {
let store = createStore(app, applyMiddleware(promiseMiddleware()))
return store
}複製代碼
至此,能夠再次運行該程序並看到後以前同樣的效果!
總的來講,筆者認爲 Saga 更適用於較爲複雜的應用,除此以外的其餘全部狀況 Redux Promise Middleware 都是十分合適的。筆者十分喜歡 Saga 中的 Generators 和 async-await,這些特性頗有趣; 同時筆者也喜歡 Redux Promise Middleware,由於它極大地減小了代碼量。
若是對 RxJS 更爲熟悉的話,筆者也許會偏向 Redux Observable;但仍是有不少筆者理解不透徹的地方,所以沒法自信地將其應用於生產環境中。
筆者 Nader Dabit,是一名專一於 React 和 React Native 開發和培訓的軟件開發者。
若是你也喜歡 React Native,歡迎查看我和 Gant Laborde Kevin Old Ali Najafizadeh 及 Peter Piekarczyk 在 Devchat.tv 的 podcast — React Native Radio。
同時,也歡迎查看筆者所著的 React Native in Action,該書目前能夠在 Manning Publications 購買。
若是你喜歡這篇文章,歡迎推薦和分享!謝謝!