本文將涉及如下三塊內容:html
在上一篇文章《React Redux與胖虎》 中咱們詳盡地介紹了 React Redux,也寫了一個簡單的計數器。前端
這篇文章叫《React Redux與胖虎他媽》,由於在哆啦A夢裏面,胖虎雖然很屌總是欺負大雄和小夫,可是在他媽面前沒少捱揍,胖虎他媽仍是他媽,因此這篇文章主要是介紹 React Redux 的一些進階用法。react
開發過程當中,咱們因爲業務或者功能的劃分,通常不一樣模塊的數據也是不一樣的,若是隻用一個 Reducer,那麼這個 Reducer 要處理全部模塊過來的事件,而後返回一個 state,全部的數據都糅合在這個 state 裏面,全部接收到這個 state 的模塊還得解析出其中跟本身有關的部分。android
因此單個 Reducer 並不能知足當下需求,多 Reducer 的出現有利於咱們模塊化開發,下降耦合度。git
redux
提供了 combineReducers
函數來組合 Reducer,注意不是 react-redux
庫。github
// reducers/index.js
import { combineReducers } from 'redux';
import firstReducer from './first-reducer';
import secondReducer from './second-reducer';
const reducers = combineReducers({
firstReducer,
secondReducer
});
複製代碼
注意上面 combineReducers
的參數使用 ES6 的語法,至關於:編程
const reducers = combineReducers({
firstReducer: firstReducer,
secondReducer: secondReducer
});
複製代碼
注意一點:每發出一個事件,全部 Reducer 都會收到。json
咱們知道,在 Reducer 只有一個的狀況下,容器組件的 mapStateToProps
函數接收到的 state 即爲惟一 Reducer 返回的對象。redux
而在 Reducer 有多個的狀況下,就會有多個返回值。這時候容器組件的 mapStateToProps
函數接收到的 state 實際上是包含全部 Reducer 返回值的對象。能夠用 key 值來區它們,這個 key 值就是咱們在 combineReducers
時傳入的。設計模式
const mapStateToProps = (state) => {
const firstReducer = state.firstReducer;
const secondReducer = state.secondReducer;
return {
value1: firstReducer.value,
value2: secondReducer.value
};
}
export default connect(mapStateToProps)(Counter);
複製代碼
固然,通常都是隻須要用其中一個 state,那麼咱們能夠寫成:
const mapStateToProps = ({ firstReducer }) => {
return {
value: firstReducer.value
};
}
//或者更加語義化地表示爲state
const mapStateToProps = ({ firstReducer: state }) => {
return {
value: state.value
};
}
複製代碼
這樣一來能夠有效地隔離各個模塊之間的影響,也方便多人協做開發。
(因爲胖虎他媽實在沒什麼表情,因此仍是用胖虎開涮吧)
網上對於中間件的解釋基本上都是「位於應用程序和操做系統之間的程序」之類,這只是一個基本的概述。在 React Redux 裏面,中間件的位置很明確,就是在 Action 到達 Reducer 以前作一些操做。
React Redux 的中間件其實是一個高階函數:
function middleware(store) {
return function wrapper(next) {
return function inner(action) {
...
}
}
}
複製代碼
其中最內層的函數接收的正是 Action。
中間件能夠多個疊加使用,在中間件內部使用 next
函數來將 Action 發送到下一個中間件讓其處理。若是沒有下一個中間件,那麼會將 Action 發送到 Reducer 去。
咱們看如何將中間件應用到 React Redux 應用中。
redux
提供了 applyMiddleware
, compose
函數來幫助添加中間件:
import { applyMiddleware, compose, createStore } from 'redux';
import api from '../middlewares/api';
import thunk from 'redux-thunk';
import reducers from "../reducers";
const withMiddleware = compose(
applyMiddleware(api),
)(createStore);
const store = withMiddleware(reducers);
export default store;
複製代碼
能夠看到 applyMiddleware
函數能夠將中間件引入,使用 compose
函數將多個函數整合成一個新的函數。
對於 applyMiddleware
, compose
, createStore
這三個函數的實現,能夠本身去參考源碼。
這裏說一下,這三個函數雖然代碼量不大,可是其實用了挺多函數式編程的思想和作法,一開始看會很抽象,特別是幾個箭頭符號連着用更是懵逼。可是看源碼老是好的,一旦你漸入佳境,定會發現新的天地。不過,這裏就只講用法了,說實話我也還沒認真去看(逃
咱們能夠實現一個炒雞簡單的中間件來看看效果,好比說,在事件到達 Reducer 以前,把事件打印出來。
export default store => next => action => {
console.log(action);
next(action);
}
複製代碼
emmmm,是挺簡單的....
在談複雜中間件時,咱們須要先說說同步事件、異步事件。
在 React Redux 應用中,咱們平時發出去的事件都是直接到達中間件(若是有中間件的話)而後到達 Reducer,乾淨利落絕不拖拉,這種事件咱們稱爲同步事件。
而異步事件,按照我我的理解,指的是,你發出去的事件,通過中間件時有了可觀的時間停留,並不會當即傳到 Reducer 裏面處理。也就是說,這個異步事件致使事件流通過中間件時發生了耗時操做,好比訪問網絡數據、讀寫文件等,在操做完成以後,事件才繼續往下流到 Reducer 那兒。
嗯...同步事件咱們都知道怎麼寫:
{
type: 'SYNC_ACTION',
...
}
複製代碼
異步事件的話,通常是定義成一個函數:
function asyncAction({dispatch, getState}) {
const action = {
type: 'ASYNC_ACTION',
api: {
url: 'www.xxx.com/api',
method: 'GET'
}
};
dispatch(action);
}
複製代碼
可是,如今咱們的異步事件是一個函數,你若是不做任何處理的話直接執行 dispatch(asyncAction)
,那麼會報錯,告訴你只能發送 plain object,即相似於同步事件那樣的對象。
咱們要在中間件搞些事情,讓函數類型的 Action 能夠用,簡單地可使用 redux-thunk 。
P.S. 雖然我不是專門搞前端的,雖然我是男的,可是做者 gaearon 真的好帥......
redux-thunk
的代碼量十分地少... 貼出來看看:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
複製代碼
emmmm,其實它作的事情就是判斷傳進來的 Action 是否是 function
類型的,若是是,就執行這個 action 而且把 store.dispatch
和 store.getState
傳給它;若是不是,那麼調用 next
將 Action 繼續往下發送就好了。
行吧... 那咱們仿照 redux-thunk
寫一箇中間件,整合進網絡請求的功能。
首先固然是容許 function 類型的 Action
export default store => next => action => {
if (typeof action === 'function') {
return action(store);
}
}
複製代碼
而後當 Action 是 plain object 並且沒有 api
字段時,當成同步事件處理
export default store => next => action => {
if (typeof action === 'function') {
return action(store);
}
const { type, api, isFetching, ...rest } = action;
if (!api) {
return next(action);
}
}
複製代碼
若是有 api
字段,那麼先發送一個事件,告訴下游的 Reducer 我先要開始來拿網絡數據了嘿嘿,即 isFetching
字段值爲 true
export default store => next => action => {
if (typeof action === 'function') {
return action(store);
}
const { type, api, isFetching, ...rest } = action;
if (!api) {
return next(action);
}
next({ type, api, isFetching: true, ...rest });
}
複製代碼
而後就開始進行異步操做,即網絡請求。而且請求成功、請求失敗和請求異常三種狀況都會發送不一樣的事件給下游的 Reducer
import fetch from 'isomorphic-fetch';
import React from "react";
export default store => next => action => {
if (typeof action === 'function') {
return action(store);
}
const { type, api, isFetching, ...rest } = action;
if (!api) {
return next(action);
}
next({ type, api, isFetching: true, ...rest });
fetch(api.url, {
method: api.method,
}).then(response => {
if (response.status !== 200) {
next({
type,
api,
status: 'error',
code: response.status,
response: {},
isFetching: false,
...rest
});
} else {
response.json()
.then(json => {
next({
type,
api,
status: 'success',
code: 200,
response: json.response,
isFetching: false,
...rest
});
})
}
}).catch(err => {
next({ type, api, status, code: 0, response: {}, isFetching: false, msg: err, ...rest });
});
}
複製代碼
到此爲止,一個比較複雜的帶有網絡請求的中間件就完成了。
還記得上一篇文章咱們說到「一個深度爲 100 的組件要去改變一個淺層次組件的文案」的例子嗎?咱們當時說,只要從深層次的組件裏面發送一個事件出來就能夠了,也就是使用 dispatch 函數來發送。
emmmm,咱們到如今好像還沒遇到過直接在組件裏面 dispatch 事件的狀況,咱們以前都是在容器組件的 mapDispatchToProps
裏面 dispatch 的。
因此在 UI 組件裏面不能拿到 dispatch 函數?
這裏先說明一點,咱們親愛的 dispatch 函數,是存在於 Store 中的,能夠用 Store.dispatch 調用。有些機靈的同窗已經想到,那咱們全局的 Store 引入 UI 組件不就好咯。
哦我親愛的上帝,瞧瞧這個優秀的答案,來,我親愛的湯姆斯·陳獨秀先生,這是你的獎盃...
是的沒錯,這是一種方式,可是我以爲這很不 React Redux。
在上一篇文章中,咱們說到引入了 Provider
組件來說 Store 做用於整個組件樹,那麼是否在每個組件中都能獲取到 Store 呢?
固然能夠,Store 是穿透到整個組件樹裏面的,這個特性依賴於 context
這個玩意,context
具體的介紹能夠參看 官方文檔 。
只須要在頂層的組件聲明一些方法就能夠實現穿透,這部分工做 Provider 組件內部已經幫咱們作好了。
不過在想使用 Store 的組件內部,也要聲明一些東西才能拿到:
import PropTypes from 'prop-types';
export default class DeepLayerComponent extends React.Component {
static contextTypes = {
store: PropTypes.object
}
componentDidMount() {
this.context.store.dispatch({type: 'DO_SOMETHING'});
}
}
複製代碼
這裏咱們聲明 contextTypes
的 store
字段,而後就能夠經過 this.context.store
來使用了。
注意,因爲 react
庫自帶的 PropTypes
在 15.5 版本以後抽離到 prop-types
庫中,須要自行引入才能使用。
可是若是每一個要使用 Store 的組件都這麼搞,不得累死,因此咱們考慮作一下封裝,建立一個能經過 this.store
就能拿到全局 Store 的組件。
import React from "react";
import PropTypes from 'prop-types';
export default class StoreAwareComponent extends React.Component {
static contextTypes = {
store: PropTypes.object
};
componentWillMount() {
this.store = this.context.store;
}
}
複製代碼
嘿嘿,而後你只要繼承這個組件就能夠輕鬆拿到全局 Store 了。
import React from "react";
import PropTypes from 'prop-types';
export default class DeepLayerComponent extends StoreAwareComponent {
componentDidMount() {
this.store.dispatch({type: 'DO_SOMETHING'});
}
}
複製代碼
這篇我就不做總結了。(逃
技術上的問題,歡迎討論。
我的博客:mindjet.github.io
最近在 Github 上維護的項目:
歡迎 star/fork/follow 提 issue 和 PR。