Redux 是近年來提出的 Flux 思想的一種實踐方案芦缰,在它之前也有 reflux 蓄坏、 fluxxor 等高質(zhì)量的作品价捧,但短短幾個月就在 GitHub 上獲近萬 star 的成績讓這個后起之秀逐漸成為 Flux 的主流實踐方案。
正如 Redux 官方所稱涡戳,React 禁止在視圖層直接操作 DOM 和異步行為 ( removing both asynchrony and direct DOM manipulation )结蟋,來拆開異步和變化這一對冤家。但它依然把狀態(tài)的管理交到了我們手中渔彰。Redux 就是我們的狀態(tài)管理小管家嵌屎。
安利的話先暫時說到這,本次我們聊聊 React-Redux 在滬江前端團隊中的實踐恍涂。
0. 放棄
你沒有看錯宝惰,在開始之前我們首先談論一下什么情況下不應該用 Redux。
所謂殺雞焉用宰牛刀再沧,任何技術(shù)方案都有其適用場景尼夺。作為一個思想的實踐方案,Redux 必然會為實現(xiàn)思想立規(guī)矩、鋪基礎汞斧,放在復雜的 React 應用里夜郁,它會是“金科玉律”,而放在結(jié)構(gòu)不算復雜的應用中粘勒,它只會是“繁文縟節(jié)”竞端。
如果我們將要構(gòu)建的應用無需多層組件嵌套,狀態(tài)變化簡單庙睡,數(shù)據(jù)單一事富,那么就應放棄 Redux ,選用單純的 React 庫 或其他 MV* 庫乘陪。畢竟统台,沒有人愿意雇傭一個收費比自己收入還高的財務顧問。
1. 思路
首先啡邑,我們回顧一下 Redux 的基本思路
當用戶與界面交互時贱勃,交互事件的回調(diào)函數(shù)會觸發(fā) ActionCreators ,它是一個函數(shù)谤逼,返回一個對象贵扰,該對象攜帶了用戶的動作類型和修改 Model 必需的數(shù)據(jù),這個對象也被我們稱作 Action 流部。
以 TodoList 為例戚绕,添加一個 Todo 項的 ActionCreator 函數(shù)如下所示(如果不熟悉 ES6 箭頭函數(shù)請移步這里):
const addTodo = text => ({
type: 'ADD_TODO',
text
});
在上例中,addTodo 就是 ActionCreator 函數(shù)枝冀,該函數(shù)返回的對象就是 Action 舞丛。
其中 type 為 Redux 中約定的必填屬性,它的作用稍后我們會講到果漾。而 text 則是執(zhí)行 “添加 Todo 項“ 這個動作必需的數(shù)據(jù)球切。
當然,不同動作所需要的數(shù)據(jù)也不盡相同绒障,如 “刪除Todo” 動作欧聘,我們就需要知道 todo 項的 id,“拉取已有的Todo項” 動作端盆,我們就需要傳入一個數(shù)組( todos )怀骤。形如 text 、 id 焕妙、 todos 這類屬性蒋伦,我們習慣稱呼其為 “ payload ” 。
現(xiàn)在焚鹊,我們得到了一個 “栩栩如生” 的動作痕届。它足夠簡潔韧献,但擔任 Model 的 store 暫時還不知道如何感知這個動作從而改變數(shù)據(jù)結(jié)構(gòu)。
為了處理這個關(guān)鍵問題研叫,Reducer 巧然登場锤窑。它仍然是一個函數(shù),而且是沒有副作用的純函數(shù)嚷炉。它只接收兩個參數(shù):state 和 action 渊啰,返回一個 newState 。
沒錯申屹,state 就是你在 React 中熟知的 state绘证,但根據(jù) Redux 三原則 之一的 “單一數(shù)據(jù)源” 原則,Reducer 幽幽地說:“你的 state 被我承包了哗讥∪履牵”
于是,單一數(shù)據(jù)源規(guī)則實施起來杆煞,是規(guī)定用 React 的頂層容器組件( Container Components )的 state 來存儲單一對象樹魏宽,同時交給 Redux store 來管理。
這里區(qū)分一下 state 和 Redux store:state 是真正儲存數(shù)據(jù)的對象樹决乎,而 Redux store 是協(xié)調(diào) Reducer湖员、state、Action 三者的調(diào)度中心瑞驱。
而如此前所說,Reducer 此時手握兩個關(guān)鍵信息:舊的數(shù)據(jù)結(jié)構(gòu)(state)窄坦,還有改變它所需要的信息 (action)唤反,然后聰明的 Reducer 算盤一敲,就能給出一個新的 state 鸭津,從而更新數(shù)據(jù)彤侍,響應用戶。下面依然拿 TodoList
舉例(不熟悉 “...” ES6 rest/spread 語法請先看這里):
//整個 todoList 最原始的數(shù)據(jù)結(jié)構(gòu)逆趋。
const initState = {
filter: 'ALL',
todos: []
};
//Reducer 識別動作類型為 ADD_TODO 時的處理函數(shù)
const handleAddTodo = (state, text) => {
const todos = state.todos;
const newState = {...state, {
todos: [
...todos, {
text,
completed: false
}]
}};
return newState;
};
//Reducer 函數(shù)
const todoList = (state = initState, action) => {
switch (action.type) {
case 'ADD_TODO':
return handleAddTodo(state, action.text);
default:
return state;
}
}
當接收到一個 action 時盏阶,Reducer 從 action.type 識別出該動作是要添加 Todo 項,然后路由到相應的處理方案闻书,接著根據(jù) action.text 完成了處理名斟,返回一個 newState 。過程之間魄眉,整個應用的 state 就從 state => newState 完成了狀態(tài)的變更砰盐。
這個過程讓我們很自然地聯(lián)想到去銀行存取錢的經(jīng)歷,顯然我們應該告訴柜臺操作員要存取錢坑律,而不是遙望著銀行的金庫自言自語岩梳。
Reducer 為我們梳理了所有變更 state 的方式,那么 Redux store 從無到有,從有到變都應該與 Reducer 強關(guān)聯(lián)冀值。
因此也物,Redux 提供了 createStore 函數(shù),他的第一個參數(shù)就是 Reducer 列疗,用以描繪 state 的更改方式滑蚯。第二個是可選參數(shù) initialState ,此前我們知道作彤,這個 initialState 參數(shù)也可以傳給 Reducer 函數(shù)膘魄。放在這里做可選參數(shù)的原因是為同構(gòu)應用提供便捷。
//store.js
import reducer from './reducer';
import { createStore } from 'redux';
export default createStore(reducer);
createStore 函數(shù)最終返回一個對象竭讳,也就是我們所說的 store 對象创葡。主要提供三個方法:getState、dispatch 和 subscribe绢慢。 其中 getState() 獲得 state 對象樹灿渴。dispatch(actionCreator) 用以執(zhí)行 actionCreators,建起從 action 到 store 的橋梁胰舆。
僅僅完成狀態(tài)的變更可不算完骚露,我們還得讓視圖層跟上 store 的變化,于是 Redux 還為 store 設計了 subscribe 方法缚窿。顧名思義棘幸,當 store 更新時,store.subscribe() 的回調(diào)函數(shù)會更新視圖層倦零,以達到 “訂閱” 的效果误续。
在 React 中,有 react-redux 這樣的橋接庫為 Redux 的融入鋪平道路扫茅。所以蹋嵌,我們只需為頂層容器組件外包一層 Provider 組件、再配合 connect 函數(shù)處理從 store 變更到 view 渲染的相關(guān)過程葫隙。
import store from './store';
import {connect, Provider} from 'react-redux';
import React from 'react';
import ReactDOM from 'react-dom';
import Page from '../components/page'; //業(yè)務組件
// 把 state 映射到 Container 組件的 props 上的函數(shù)
const mapStateToProps = state => {
return {
...state
}
}
const Container = connect(mapStateToProps)(Page); //頂層容器組件
ReactDOM.render(
<Provider store={store}>
<Container />
</Provider>,
document.getElementById("root")
);
而頂層容器組件往下的子組件只需憑借 props 就能一層層地拿到 store 數(shù)據(jù)結(jié)構(gòu)的數(shù)據(jù)了栽烂。就像這樣:
至此,我們走了一遍完整的數(shù)據(jù)流恋脚。然而腺办,在實際項目中,我們面臨的需求更為復雜糟描,與此同時菇晃,redux 和 react 又是具有強大擴展性的庫,接下來我們將結(jié)合以上的主體思路蚓挤,談談我們在實際開發(fā)中會遇到的一些細節(jié)問題磺送。
2. 細節(jié)
應用目錄
清晰的思路須輔以分工明確的文件模塊驻子,才能讓我們的應用達到更佳的實踐效果,同時估灿,統(tǒng)一的結(jié)構(gòu)也便于腳手架生成模板崇呵,提高開發(fā)效率。
以下的目錄結(jié)構(gòu)為團隊伙伴多次探討和改進而來(限于篇幅馅袁,這里只關(guān)注 React 應用的目錄域慷。):
appPage
├── components
│ └── wrapper
│ ├── component-a
│ │ ├── images
│ │ ├── index.js
│ │ └── index.scss
│ ├── component-a-a
│ ├── component-a-b
│ ├── component-b
│ └── component-b-a
├── react
│ ├── reducer
│ │ ├── index.js
│ │ ├── reducerA.js
│ │ └── reducerB.js
│ ├── action.js
│ ├── actionTypes.js
│ ├── bindActions.js
│ ├── container.js
│ ├── model.js
│ ├── param.js
│ └── store.js
└── app.js
入口文件 app.js 與頂層組件 react/container.js
這塊我們基本上保持和之前思路上的一致,用 react-redux 橋接庫提供的 Provider 與函數(shù) connect 完成 Redux store 到 React state 的轉(zhuǎn)變汗销。
細心的你會在 Provider 的源碼中發(fā)現(xiàn)犹褒,它最終返回的還是子組件(本例中就是頂層容器組件 “Container“ )。星星還是那個星星弛针,Container 還是那個 Container叠骑,只是多了一個 Redux store 對象。
而 Contaier 作為 業(yè)務組件 Wrapper 的 高階組件 削茁,負責把 Provider 賦予它的 store 通過 store.getState() 獲取數(shù)據(jù)宙枷,轉(zhuǎn)而賦值給 state 芝雪。然后又根據(jù)我們定義的 mapStateToProps 函數(shù)按一定的結(jié)構(gòu)將 state 對接到 props 上屯远。 mapStateToProps 函數(shù)我們稍后詳說。如下所見套鹅,這一步主要是 connect 函數(shù)干的活兒瘾杭。
//入口文件:app.js
import store from './react/store';
import Container from './react/container';
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
ReactDOM.render(
<Provider store={store}>
<Container />
</Provider>,
document.getElementById("root")
);
//頂層容器組件:react/container.js
import mapStateToProps from './param';
import {connect} from 'react-redux';
import Wrapper from '../components/wrapper';
export default connect(mapStateToProps)(Wrapper);
業(yè)務組件 component/Wrapper.js 與 mapStateToProps
這兩個模塊是整個應用很重要的業(yè)務模塊诅病。作為一個復雜應用,將 state 上的數(shù)據(jù)和 actionCreator 合理地分發(fā)到各個業(yè)務組件中粥烁,同時要易于維護贤笆,是開發(fā)的關(guān)鍵。
首先页徐,我們設計 mapStateToProps 函數(shù)。需要謹記一點:拿到的參數(shù)是 connect 函數(shù)交給我們的根 state银萍,返回的對象是最終 this.props 的結(jié)構(gòu)变勇。
和 Redux 官方示例不同的是,我們?yōu)榱丝勺x性贴唇,將分發(fā) action 的函數(shù)也囊括進這個結(jié)構(gòu)中搀绣。這也是得益于 bindActions 模塊,稍后我們會講到戳气。
//mapStateToProps:react/param.js
import bindActions from './bindActions';
const mapStateToProps = state => {
let {demoAPP} = state; // demoAPP 也是 reducer 中的同名函數(shù)
// 分發(fā) action 的函數(shù)
let {initDemoAPP, setDemoAPP} = bindActions;
// 分發(fā) state 上的數(shù)據(jù)
let {isLoading, dataForA, dataForB} = demoAPP;
let {dataForAA1, dataForAA2, dataForAB} = dataForA;
// 返回的對象即為 Wrapper 組件的 this.props
return {
initDemoAPP, // Wrapper 組件需要發(fā)送一個 action 初始化頁面數(shù)據(jù)
isLoading, // Wrapper 組件需要 isLoading 用于展示
paramsComponentA: {
dataForA, // 組件 A 需要 dataForA 用于展示
paramsComponentAA: {
setDemoAPP, // 組件 AA 需要發(fā)送一個 action 修改數(shù)據(jù)
dataForAA1,
dataForAA2
},
paramsComponentAB: {
dataForAB
}
},
paramsComponentB: {
dataForB,
paramsComponentBA: {}
}
}
}
export default mapStateToProps;
這樣链患,我們這個函數(shù)就準備好履行它分發(fā)數(shù)據(jù)和組件行為的職責了。那么瓶您,它又該如何 “服役” 呢麻捻?
敏銳的你一定察覺到剛才我們設計的結(jié)構(gòu)中纲仍,以 “ params ” 開頭的屬性既沒起到給組件展示數(shù)據(jù)的作用,又沒有為組件發(fā)送 action 的功能贸毕。它們便是我們分發(fā)以上兩種功能屬性的關(guān)鍵郑叠。
我們先來看看業(yè)務組件 Wrapper :
//業(yè)務組件組件:components/wrapper.js
import React, { Component } from 'react';
import ComponentA from '../component-a';
import ComponentB from '../component-b';
export default class Example extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.initDemoAPP(); //拉取業(yè)務數(shù)據(jù)
}
render() {
let {paramsComponentA, paramsComponentB, isLoading} = this.props;
if (isLoading) {
return (<span>App is loading ...</span>);
}
return (
<div>
{/* 為組件分發(fā)參數(shù) */}
<ComponentA {...paramsComponentA}/>
<ComponentB {...paramsComponentB}/>
</div>
);
}
}
現(xiàn)在,param 屬性們?yōu)槲覀冋故玖怂缪莸慕巧涸诮M件中實際分發(fā)數(shù)據(jù)和方法的快遞小哥明棍。這樣乡革,即使項目越變越大,組件嵌套越來越多摊腋,我們也能在 param.js 模塊中沸版,清晰地看到我們的組件結(jié)構(gòu)。需求更改的時候兴蒸,我們也能快速地定位和修改视粮,而不用對著堆積如山的組件模塊梳理父子關(guān)系。
相信你應該能猜到剩下的子組件們怎么取到數(shù)據(jù)了类咧,這里限于篇幅就不貼出它們的代碼了馒铃。
Action 模塊: react/action.js、react/actionType.js 和 react/bindActions.js
在前面的介紹中痕惋,我們提到:一個 ActionCreator 長這樣:
const addTodo = text => ({
type: 'ADD_TODO',
text
});
而在 Redux 中区宇,真正讓其分發(fā)一個 action ,并讓 store 響應該 action值戳,依靠的是 dispatch 方法议谷,即:
store.dispatch(addTodo('new todo item'));
交互動作一多,就會變成:
store.dispatch(addTodo('new todo item1'));
store.dispatch(deleteTodo(0));
store.dispatch(compeleteTodo(1));
store.dispatch(clearTodos());
//...
而容易想到:抽象出一個公用函數(shù)來分發(fā) action (這里粗略寫一下我的思路堕虹,簡化方式并不唯一)
const {dispatch} = store;
const dispatcher = (actionCreators, dispatch) => {
// ...校驗參數(shù)
let bounds = {};
let keys = Object.keys(actionCreators);
for (let key of keys) {
bounds[key] = (...rest) => {
dispatch(actionCreators[key].apply(null, rest));
}
}
return bounds;
}
//簡化后的使用方式
const disp = dispatcher({
addTodo,
deleteTodo,
compeleteTodo
//...
}, dispatch);
disp.addTodo('new todo item1');
disp.deleteTodo(0);
//...
而細心的 Redux 已經(jīng)為我們提供了這個方法 —— bindActionCreator
所以卧晓,我們的 bindActions.js 模塊就借用了 bindActionCreator 來簡化 action 的分發(fā):
// react/bindActions.js
import store from './store.js';
import {bindActionCreators} from 'redux';
import * as actionCreators from './action';
let {dispatch} = store;
export default bindActionCreators({ ...actionCreators}, dispatch);
不難想象,action 模塊里就是一個個 actionCreator :
// react/action.js
import * as types from '/actionType.js';
export const setDemoAPP = payload => ({
type: types.SET_DEMO_APP,
payload
});
// 其他 actionCreators ...
為了更好地合作赴捞,我們單獨為 action 的 type 劃分了一個模塊 —— actionTypes.js 里面看起來會比較無聊:
// react/actionTypes.js
export const SET_DEMO_APP = "SET_DEMO_APP";
// 其他 types ...
react/reducers/ 和 react/store.js
前面我們說到逼裆,reducer 的作用就是區(qū)別 action type 然后更新 state ,這里不再贅述赦政∈び睿可上手實際項目的時候,你會發(fā)現(xiàn) action 類型和對應處理方式多起來會讓單個 reducer 迅速龐大恢着。
為此桐愉,我們就得想方設法將其按業(yè)務邏輯拆分,以免難以維護掰派。但是如何把拆分后的 Reducer 組合起來呢 Redux 再次為我們提供便捷 —— combineReducers 从诲。
只有單一 Reducer 時,想必代碼結(jié)構(gòu)你也了然:
import * as actionTypes from '../actionTypes';
let initState = {
isLoading: true
};
// 對應 state.demoAPP
const demoAPP = (state = initState, action) => {
switch (action.type) {
case actionTypes.SET_DEMO_APP:
return {
isLoading: false,
...action.payload
};
default:
return state;
}
}
export default demoAPP; // 把它轉(zhuǎn)交給 createStore 函數(shù)
我們最終得到的 state 結(jié)構(gòu)是:
- state
- demoAPP
當有多個 reducer 時:
import * as actionTypes from '../actionTypes';
import { combineReducers } from 'redux';
let initState = {
isLoading: true
};
// 對應 state.demoAPP
const demoAPP = (state = initState, action) => {
switch (action.type) {
case actionTypes.SET_DEMO_APP:
return {
isLoading: false,
...action.payload
};
default:
return state;
}
}
// 對應 state.reducerB
const reducerB = (state = {}, action) => {
switch (action.type) {
case actionTypes.SET_REDUCER_B:
return {
isLoading: false,
...action.payload
};
default:
return state;
}
}
const rootReducer = combineReducers({demoAPP,reducerB});
export default rootReducer;
我們最終得到的 state 結(jié)構(gòu)是:
- state
- demoAPP
- reducerB
想必你已經(jīng)想到更進一步靡羡,把這些 Reducer 拆分到相應的文件模塊下:
// react/reducers/index.js
import demoAPP from './demoAPP.js';
import reducerB from './reducerB.js';
const rootReducer = combineReducers({demoAPP,reducerB});
export default rootReducer;
接著系洛,我們來看 store 模塊:
// react/store.js
import rootReducer from './reducers';
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
const initialState = {};
const finalCreateStore = compose(
applyMiddleware(thunk)
)(createStore);
export default finalCreateStore(rootReducer, initialState);
怎么和想象的不一樣俊性?不應該是這樣嗎:
// react/store.js
import rootReducer from './reducers';
import { createStore } from 'redux';
export default createStore(rootReducer);
這里引入 redux 中間件的概念,你只需知道 redux 中間件的作用就是 在 action 發(fā)出以后碎罚,給我們一個再加工 action 的機會 就可以了磅废。
為什么要引入 redux-thunk 這個中間件呢?
要知道荆烈,我們此前所討論的都是同步過程拯勉。實際項目中,只要遇到請求接口的場景(當然不只有這種場景)就要去處理異步過程憔购。
前面我們知道宫峦,dispatch 一個 ActionCreator 會立即返回一個 action 對象,用以更新數(shù)據(jù)玫鸟,而中間件賦予我們再處理 action 的機會导绷。
試想一下,如果我們在這個過程中屎飘,發(fā)現(xiàn) ActionCreator 返回的并不是一個 action 對象妥曲,而是一個函數(shù),然后通過這個函數(shù)請求接口钦购,響應就緒后檐盟,我們再 dispatch 一個 ActionCreator ,這次我們真的返回一個 action 押桃,然后攜帶接口返回的數(shù)據(jù)去更新 state 葵萎。 這樣一來不就解決了我們的問題嗎?
當然唱凯,這只是基本思路羡忘,關(guān)于 redux 的中間件設計,又是一個有趣的話題磕昼,有興趣我們可以再開一篇專門討論卷雕,這里點到為止。
回到我們的話題票从,經(jīng)過
const finalCreateStore = compose(
applyMiddleware(thunk)
)(createStore);
export default finalCreateStore(rootReducer, initialState);
這樣包裝一遍 store 后漫雕,我們就可以愉快地使用異步 action 了:
// react/action.js
import * as types from './actionType.js';
import * as model from './model.js';
// 同步 actionCreator
export const setDemoAPP = payload => ({
type: types.SET_DEMO_APP,
payload
});
// 異步 actionCreator
export const initDemoAPP = () => dispatch => {
model.getBaseData().then(response => {
let {status, data} = response;
if (status === 0) {
//請求成功且返回數(shù)據(jù)正常
dispatch(setDemoAPP(data));
}
}, error => {
// 處理請求異常的情況
});
}
這里我們用 promise 方式來處理請求,model.js 模塊如你所想是一些接口請求 promise纫骑,就像這樣:
export const getBaseData () => {
return $.getJSON('/someAPI');
}
你也可以參閱我們往期介紹的其他方式蝎亚。
最后九孩,我們再來完善一下之前的流程:
3.結(jié)語
Redux 的 API 一只手都能數(shù)得完先馆,源碼更是精煉,加起來不超過500行躺彬。但它給我們帶來的煤墙,不啻是一套復雜應用解決方案梅惯,更是 Flux 思想的精簡表達。此外仿野,你還可以從中體會到函數(shù)式編程的樂趣铣减。
一千個觀眾心中有一千個哈姆萊特,你腦海里的又是哪一個呢脚作?
參考
《Redux 官方文檔》
《深入 React 技術(shù)椇》