Redux 很大部分 受到 Flux 的啟發(fā),并且最常見(jiàn)的關(guān)于 Flux 抱怨是它如何使得你寫了一大堆的模板。在這個(gè)技巧中,我們將考慮 Redux 如何使得我們選擇我們的代碼會(huì)變得怎樣繁復(fù),取決于個(gè)人樣式,團(tuán)隊(duì)選項(xiàng),長(zhǎng)期可維護(hù)等等。
Actions 是描述了在 app 中所發(fā)生的,以單獨(dú)方式描述對(duì)象變異意圖的服務(wù)的一個(gè)普通對(duì)象。很重要的一點(diǎn)是 你必須分發(fā)的 action 對(duì)象并不是一個(gè)模板,而是 Redux 的一個(gè)基本設(shè)計(jì)選項(xiàng).
有些框架生成自己和 Flux 很像,不過(guò)缺少了 action 對(duì)象的概念。為了變得可預(yù)測(cè),這是一個(gè)從 Flux or Redux 的倒退。如果沒(méi)有可串行的普通對(duì)象 action,便無(wú)法記錄或重放用戶會(huì)話,或者無(wú)法實(shí)現(xiàn) 帶有時(shí)間旅行的熱重載。如果你更喜歡直接修改數(shù)據(jù),那么你并不需要 Redux 。
Action 一般長(zhǎng)這樣:
{ type: 'ADD_TODO', text: 'Use Redux' }
{ type: 'REMOVE_TODO', id: 42 }
{ type: 'LOAD_ARTICLE', response: { ... } }
一個(gè)約定俗成的是 actions 擁有一個(gè)定值 type 幫助 reducer (或 Flux 中的 Stores ) 識(shí)別它們。我們建議的你使用 string 而不是 Symbols 作為 action type ,因?yàn)?string 是可串行的,而使用 Symbols 的話你會(huì)把記錄和重演變得比所需要的更難。
在 Flux 中,傳統(tǒng)上認(rèn)為你將每個(gè) action type 定義為string定值:
const ADD_TODO = 'ADD_TODO';
const REMOVE_TODO = 'REMOVE_TODO';
const LOAD_ARTICLE = 'LOAD_ARTICLE';
這么做的優(yōu)勢(shì)?人們通常聲稱定值不是必要的,對(duì)于小的項(xiàng)目可能是正確的。 對(duì)于大的項(xiàng)目,將action types定義為定值有如下好處:
undefined
。當(dāng)你納悶 action 被分發(fā)出去而什么也沒(méi)發(fā)生的時(shí)候,一個(gè)拼寫錯(cuò)誤更容易被發(fā)現(xiàn)。你的項(xiàng)目的約定取決與你自己。你開(kāi)始的時(shí)候可能用的是inline string,之后轉(zhuǎn)為定值,也許之后將他們歸為一個(gè)獨(dú)立文件。Redux 不會(huì)給予任何建議,選擇你自己最喜歡的。
另一個(gè)約定是,你創(chuàng)建生成 action 對(duì)象的函數(shù),而不是在你分發(fā)的時(shí)候內(nèi)聯(lián)生成它們。
例如,用文字對(duì)象取代調(diào)用 dispatch
:
// somewhere in an event handler
dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
});
你可以在單獨(dú)的文件中寫一個(gè) action creator ,然后從 component 里導(dǎo)入:
actionCreators.js
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
};
}
AddTodo.js
import { addTodo } from './actionCreators';
// event handler 里的某處
dispatch(addTodo('Use Redux'))
Action creators 總被當(dāng)作模板受到批評(píng)。好吧,其實(shí)你并不用把他們寫出來(lái)!如果你覺(jué)得更適合你的項(xiàng)目你可以選用對(duì)象文字 然而,你應(yīng)該知道寫 action creators 是存在某種優(yōu)勢(shì)的。
假設(shè)有個(gè)設(shè)計(jì)師看完我們的原型之后回來(lái)說(shuō),我們需要允許三個(gè) todo 不能再多了。我們可以使用 redux-thunk 中間件添加一個(gè)提前退出,把我們的 action creator 重寫成回調(diào)形式:
function addTodoWithoutCheck(text) {
return {
type: 'ADD_TODO',
text
};
}
export function addTodo(text) {
// Redux Thunk 中間件允許這種形式
// 在下面的 “異步 Action Creators” 段落中有寫
return function (dispatch, getState) {
if (getState().todos.length === 3) {
// 提前退出
return;
}
dispatch(addTodoWithoutCheck(text));
}
}
我們剛修改了 addTodo
action creator 的行為,對(duì)調(diào)用它的代碼完全不可見(jiàn)。我們不用擔(dān)心去看每個(gè)添加 todo 的地方保證他們有了這個(gè)檢查 Action creator 讓你可以解耦額外的分發(fā) action 邏輯與實(shí)際的 components 發(fā)送這些 actions,而且當(dāng)你在重開(kāi)發(fā)經(jīng)常要改變需求的時(shí)候也會(huì)非常有用。
某些框架如 Flummox 自動(dòng)從 action creator 函數(shù)定義生成 action type 定值。這個(gè)想法是說(shuō)你不需要 ADD_TODO
定值和 addTodo()
action creator兩個(gè)都自己定義。這樣的方法在底層也生成 action type 定值,但他們是隱式生成的,也就是間接級(jí)。
我們不建議用這樣的方法。如果你寫像這樣簡(jiǎn)單的 action creator 寫煩了:
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
};
}
export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
};
}
你可以寫一個(gè)生成 action creator 的函數(shù):
function makeActionCreator(type, ...argNames) {
return function(...args) {
let action = { type };
argNames.forEach((arg, index) => {
action[argNames[index]] = args[index];
});
return action;
}
}
export const addTodo = makeActionCreator('ADD_TODO', 'todo');
export const removeTodo = makeActionCreator('REMOVE_TODO', 'id');
參見(jiàn) redux-action-utils 和 redux-actions 獲得更多介紹這樣的常用工具。
注意這樣的工具給你的代碼添加了魔法。魔法和間接聲明真的值得多寫一兩行代碼么?
中間件 讓你注入一個(gè)定制邏輯,可以在每個(gè) action 對(duì)象分發(fā)出去之前解釋。異步 actions 是中間件的最常見(jiàn)用例。
沒(méi)有中間件的話,dispatch
只能接收一個(gè)普通對(duì)象。所以我們?cè)?components 里面進(jìn)行 AJAX 調(diào)用:
actionCreators.js
export function loadPostsSuccess(userId, response) {
return {
type: 'LOAD_POSTS_SUCCESS',
userId,
response
};
}
export function loadPostsFailure(userId, error) {
return {
type: 'LOAD_POSTS_FAILURE',
userId,
error
};
}
export function loadPostsRequest(userId) {
return {
type: 'LOAD_POSTS_REQUEST',
userId
};
}
UserInfo.js
import { Component } from 'react';
import { connect } from 'react-redux';
import { loadPostsRequest, loadPostsSuccess, loadPostsFailure } from './actionCreators';
class Posts extends Component {
loadData(userId) {
// 調(diào)用 React Redux `connect()` 注入 props :
let { dispatch, posts } = this.props;
if (posts[userId]) {
// 這里是被緩存的數(shù)據(jù)!啥也不做。
return;
}
// Reducer 可以通過(guò)設(shè)置 `isFetching` 反應(yīng)這個(gè) action
// 因此讓我們顯示一個(gè) Spinner 控件。
dispatch(loadPostsRequest(userId));
// Reducer 可以通過(guò)填寫 `users` 反應(yīng)這些 actions
fetch(`http://myapi.com/users/${userId}/posts`).then(
response => dispatch(loadPostsSuccess(userId, response)),
error => dispatch(loadPostsFailure(userId, error))
);
}
componentDidMount() {
this.loadData(this.props.userId);
}
componentWillReceiveProps(nextProps) {
if (nextProps.userId !== this.props.userId) {
this.loadData(nextProps.userId);
}
}
render() {
if (this.props.isLoading) {
return <p>Loading...</p>;
}
let posts = this.props.posts.map(post =>
<Post post={post} key={post.id} />
);
return <div>{posts}</div>;
}
}
export default connect(state => ({
posts: state.posts
}))(Posts);
然而,不久就需要再來(lái)一遍,因?yàn)椴煌?components 從同樣的 API 端點(diǎn)請(qǐng)求數(shù)據(jù)。而且,我們想要在多個(gè)components 中重用一些邏輯(比如,當(dāng)緩存數(shù)據(jù)有效的時(shí)候提前退出)。
中間件讓我們寫的更清楚M的潛在的異步 action creators. 它使得我們分發(fā)普通對(duì)象之外的東西,并且解釋它們的值。比如,中間件能 “捕捉” 到已經(jīng)分發(fā)的 Promises 并把他們變?yōu)橐粚?duì)請(qǐng)求和成功/失敗 actions.
最簡(jiǎn)單的中間件例子是 redux-thunk. “Thunk” 中間件讓你把 action creators 寫成 “thunks”,也就是返回函數(shù)的函數(shù)。 這使得控制被反轉(zhuǎn)了: 你會(huì)像一個(gè)參數(shù)一樣取得 dispatch
,所以你也能寫一個(gè)多次分發(fā)的 action creator 。
注意
Thunk 只是中間件的一個(gè)例子。中間件不是關(guān)于 “讓你分發(fā)函數(shù)” 的:它是關(guān)于讓你分發(fā)你用的特定中間件知道如何處理的任何東西的。Thunk 中間件添加了一個(gè)特定的行為用來(lái)分發(fā)函數(shù),但這實(shí)際上取決于你用的中間件。
考慮上面的代碼用 redux-thunk 重寫:
actionCreators.js
export function loadPosts(userId) {
// 用 thunk 中間件解釋:
return function (dispatch, getState) {
let { posts } = getState();
if (posts[userId]) {
// 這里是數(shù)據(jù)緩存!啥也不做。
return;
}
dispatch({
type: 'LOAD_POSTS_REQUEST',
userId
});
// 異步分發(fā)原味 actions
fetch(`http://myapi.com/users/${userId}/posts`).then(
response => dispatch({
type: 'LOAD_POSTS_SUCCESS',
userId,
respone
}),
error => dispatch({
type: 'LOAD_POSTS_FAILURE',
userId,
error
})
);
}
}
UserInfo.js
import { Component } from 'react';
import { connect } from 'react-redux';
import { loadPosts } from './actionCreators';
class Posts extends Component {
componentDidMount() {
this.props.dispatch(loadPosts(this.props.userId));
}
componentWillReceiveProps(nextProps) {
if (nextProps.userId !== this.props.userId) {
this.props.dispatch(loadPosts(nextProps.userId));
}
}
render() {
if (this.props.isLoading) {
return <p>Loading...</p>;
}
let posts = this.props.posts.map(post =>
<Post post={post} key={post.id} />
);
return <div>{posts}</div>;
}
}
export default connect(state => ({
posts: state.posts
}))(Posts);
這樣打得字少多了!如果你喜歡,你還是可以保留 “原味” action creators 比如從一個(gè) “聰明的” loadPosts
action creator 里用到的 loadPostsSuccess
。
最后,你可以重寫中間件 你可以把上面的模式泛化,然后代之以這樣的異步 action creators :
export function loadPosts(userId) {
return {
// 要在之前和之后發(fā)送的 action types
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
// 檢查緩存 (可選):
shouldCallAPI: (state) => !state.users[userId],
// 進(jìn)行?。? callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
// 在 actions 的開(kāi)始和結(jié)束注入的參數(shù)
payload: { userId }
};
}
解釋這個(gè) actions 的中間件可以像這樣:
function callAPIMiddleware({ dispatch, getState }) {
return function (next) {
return function (action) {
const {
types,
callAPI,
shouldCallAPI = () => true,
payload = {}
} = action;
if (!types) {
// 普通 action:傳走
return next(action);
}
if (
!Array.isArray(types) ||
types.length !== 3 ||
!types.every(type => typeof type === 'string')
) {
throw new Error('Expected an array of three string types.');
}
if (typeof callAPI !== 'function') {
throw new Error('Expected fetch to be a function.');
}
if (!shouldCallAPI(getState())) {
return;
}
const [requestType, successType, failureType] = types;
dispatch(Object.assign({}, payload, {
type: requestType
}));
return callAPI().then(
response => dispatch(Object.assign({}, payload, {
response: response,
type: successType
})),
error => dispatch(Object.assign({}, payload, {
error: error,
type: failureType
}))
);
};
};
}
在傳給 applyMiddleware(...middlewares)
一次以后,你能用相同方式寫你的 API-調(diào)用 action creators :
export function loadPosts(userId) {
return {
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
shouldCallAPI: (state) => !state.users[userId],
callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
payload: { userId }
};
}
export function loadComments(postId) {
return {
types: ['LOAD_COMMENTS_REQUEST', 'LOAD_COMMENTS_SUCCESS', 'LOAD_COMMENTS_FAILURE'],
shouldCallAPI: (state) => !state.posts[postId],
callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`),
payload: { postId }
};
}
export function addComment(postId, message) {
return {
types: ['ADD_COMMENT_REQUEST', 'ADD_COMMENT_SUCCESS', 'ADD_COMMENT_FAILURE'],
callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ message })
}),
payload: { postId, message }
};
}
Redux 用函數(shù)描述邏輯更新減少了模版里大量的 Flux stores 。函數(shù)比對(duì)象簡(jiǎn)單,比類更簡(jiǎn)單得多。
考慮這個(gè) Flux store:
let _todos = [];
export default const TodoStore = assign({}, EventEmitter.prototype, {
getAll() {
return _todos;
}
});
AppDispatcher.register(function (action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
let text = action.text.trim();
_todos.push(text);
TodoStore.emitChange();
}
});
用了 Redux 之后,同樣的邏輯更新可以被寫成 reducing function:
export function todos(state = [], action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
let text = action.text.trim();
return [...state, text];
default:
return state;
}
}
switch
語(yǔ)句 不是 真正的模版。真正的 Flux 模版是概念性的:發(fā)送更新的需求,用 Dispatcher 注冊(cè) Store 的需求,Store 是對(duì)象的需求 (當(dāng)你想要一個(gè)哪都能跑的 App 的時(shí)候復(fù)雜度會(huì)提升)。
不幸的是很多人仍然靠文檔里用沒(méi)用 switch
來(lái)選擇 Flux 框架。如果你不愛(ài)用 switch
你可以用一個(gè)單獨(dú)的函數(shù)來(lái)解決,下面會(huì)演示。
讓我們寫一個(gè)函數(shù)使得我們將 reducers 表達(dá)為 action types 到 handlers 的映射對(duì)象。例如,在我們的 todos
reducer 里這樣定義:
export const todos = createReducer([], {
[ActionTypes.ADD_TODO](state, action) {
let text = action.text.trim();
return [...state, text];
}
}
我們可以寫下面的幫忙函數(shù)來(lái)完成:
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action);
} else {
return state;
}
}
}
不難對(duì)吧?Redux 沒(méi)有默認(rèn)提供這樣的幫忙函數(shù),因?yàn)橛泻枚喾N寫的方法??赡苣阆胍詣?dòng)把普通 JS 對(duì)象變成不可變對(duì)象通過(guò)濕化服務(wù)器狀態(tài)??赡苣阆牒喜⒎祷貭顟B(tài)和當(dāng)前狀態(tài)。有很多方法 “獲取所有” handler。這些都取決于你為你的團(tuán)隊(duì)在特定項(xiàng)目中選擇的約定。
Redux reducer 的 API 是 (state, action) => state
,但是怎么創(chuàng)建這些 reducers 由你來(lái)定。
更多建議: