# 減少樣板代碼
Redux 很大部分 [受到 Flux 的啟發](#),并且最常見的關于 Flux 抱怨是它如何使得你寫了一大堆的模板。在這個技巧中,我們將考慮 Redux 如何使得我們選擇我們的代碼會變得怎樣繁復,取決于個人樣式,團隊選項,長期可維護等等。
### Actions
Actions 是描述了在 app 中所發生的,以單獨方式描述對象變異意圖的服務的一個普通對象。很重要的一點是 **你必須分發的 action 對象并不是一個模板,而是 Redux 的一個[基本設計選項](#)**.
有些框架生成自己和 Flux 很像,不過缺少了 action 對象的概念。為了變得可預測,這是一個從 Flux or Redux 的倒退。如果沒有可串行的普通對象 action,便無法記錄或重放用戶會話,或者無法實現 [帶有時間旅行的熱重載](https://www.youtube.com/watch?v=xsSnOQynTHs)。如果你更喜歡直接修改數據,那么你并不需要 Redux 。
Action 一般長這樣:
~~~
{ type: 'ADD_TODO', text: 'Use Redux' }
{ type: 'REMOVE_TODO', id: 42 }
{ type: 'LOAD_ARTICLE', response: { ... } }
~~~
一個約定俗成的是 actions 擁有一個定值 type 幫助 reducer (或 Flux 中的 Stores ) 識別它們。我們建議的你使用 string 而不是 [Symbols](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 作為 action type ,因為 string 是可串行的,而使用 Symbols 的話你會把記錄和重演變得比所需要的更難。
在 Flux 中,傳統上認為你將每個 action type 定義為string定值:
~~~
const ADD_TODO = 'ADD_TODO';
const REMOVE_TODO = 'REMOVE_TODO';
const LOAD_ARTICLE = 'LOAD_ARTICLE';
~~~
這么做的優勢?**人們通常聲稱定值不是必要的,對于小的項目可能是正確的。** 對于大的項目,將action types定義為定值有如下好處:
- 幫助維護命名一致性,因為所有的 action type 匯總在同一位置。
- 有的時候,在開發一個新功能之前你想看到所有現存的 actions 。可能的情況是你的團隊里已經有人添加了你所需要的action,而你并不知道。
- Action types 列表在Pull Request中能查到所有添加,刪除,修改的記錄。這能幫助團隊中的所有人及時追蹤新功能的范圍與實現。
- 如果你在導入一個 Action 定值的時候拼寫錯誤,你會得到 `undefined` 。當你納悶 action 被分發出去而什么也沒發生的時候,一個拼寫錯誤更容易被發現。
你的項目的約定取決與你自己。你開始的時候可能用的是inline string,之后轉為定值,也許之后將他們歸為一個獨立文件。Redux 不會給予任何建議,選擇你自己最喜歡的。
### Action Creators
另一個約定是,你創建生成 action 對象的函數,而不是在你分發的時候內聯生成它們。
例如,用文字對象取代調用 `dispatch` :
~~~
// somewhere in an event handler
dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
});
~~~
你可以在單獨的文件中寫一個 action creator ,然后從 component 里導入:
#### `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 總被當作模板受到批評。好吧,其實你并不用把他們寫出來!**如果你覺得更適合你的項目你可以選用對象文字** 然而,你應該知道寫 action creators 是存在某種優勢的。
假設有個設計師看完我們的原型之后回來說,我們需要允許三個 todo 不能再多了。我們可以使用 [redux-thunk](https://github.com/gaearon/redux-thunk) 中間件添加一個提前退出,把我們的 action creator 重寫成回調形式:
~~~
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 的行為,對調用它的代碼完全不可見。**我們不用擔心去看每個添加 todo 的地方保證他們有了這個檢查** Action creator 讓你可以解耦額外的分發 action 邏輯與實際的 components 發送這些 actions,而且當你在重開發經常要改變需求的時候也會非常有用。
### 生成 Action Creators
某些框架如 [Flummox](https://github.com/acdlite/flummox) 自動從 action creator 函數定義生成 action type 定值。這個想法是說你不需要 `ADD_TODO` 定值和 `addTodo()` action creator兩個都自己定義。這樣的方法在底層也生成 action type 定值,但他們是隱式生成的,也就是間接級。
我們不建議用這樣的方法。如果你寫像這樣簡單的 action creator 寫煩了:
~~~
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
};
}
export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
};
}
~~~
你可以寫一個生成 action creator 的函數:
~~~
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');
~~~
參見 [redux-action-utils](https://github.com/insin/redux-action-utils) 和 [redux-actions](https://github.com/acdlite/redux-actions) 獲得更多介紹這樣的常用工具。
注意這樣的工具給你的代碼添加了魔法。魔法和間接聲明真的值得多寫一兩行代碼么?
### 異步 Action Creators
[中間件](#) 讓你注入一個定制邏輯,可以在每個 action 對象分發出去之前解釋。異步 actions 是中間件的最常見用例。
沒有中間件的話,[`dispatch`](#) 只能接收一個普通對象。所以我們在 components 里面進行 AJAX 調用:
#### `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) {
// 調用 React Redux `connect()` 注入 props :
let { dispatch, posts } = this.props;
if (posts[userId]) {
// 這里是被緩存的數據!啥也不做。
return;
}
// Reducer 可以通過設置 `isFetching` 反應這個 action
// 因此讓我們顯示一個 Spinner 控件。
dispatch(loadPostsRequest(userId));
// Reducer 可以通過填寫 `users` 反應這些 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);
~~~
然而,不久就需要再來一遍,因為不同的 components 從同樣的 API 端點請求數據。而且,我們想要在多個components 中重用一些邏輯(比如,當緩存數據有效的時候提前退出)。
**中間件讓我們寫的更清楚M的潛在的異步 action creators.** 它使得我們分發普通對象之外的東西,并且解釋它們的值。比如,中間件能 “捕捉” 到已經分發的 Promises 并把他們變為一對請求和成功/失敗 actions.
最簡單的中間件例子是 [redux-thunk](https://github.com/gaearon/redux-thunk). **“Thunk” 中間件讓你把 action creators 寫成 “thunks”,也就是返回函數的函數。** 這使得控制被反轉了: 你會像一個參數一樣取得 `dispatch` ,所以你也能寫一個多次分發的 action creator 。
> ##### 注意
> Thunk 只是中間件的一個例子。中間件不是關于 “讓你分發函數” 的:它是關于讓你分發你用的特定中間件知道如何處理的任何東西的。Thunk 中間件添加了一個特定的行為用來分發函數,但這實際上取決于你用的中間件。
考慮上面的代碼用 [redux-thunk](https://github.com/gaearon/redux-thunk) 重寫:
#### `actionCreators.js`
~~~
export function loadPosts(userId) {
// 用 thunk 中間件解釋:
return function (dispatch, getState) {
let { posts } = getState();
if (posts[userId]) {
// 這里是數據緩存!啥也不做。
return;
}
dispatch({
type: 'LOAD_POSTS_REQUEST',
userId
});
// 異步分發原味 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 比如從一個 “聰明的” `loadPosts` action creator 里用到的 `loadPostsSuccess` 。
**最后,你可以重寫中間件** 你可以把上面的模式泛化,然后代之以這樣的異步 action creators :
~~~
export function loadPosts(userId) {
return {
// 要在之前和之后發送的 action types
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
// 檢查緩存 (可選):
shouldCallAPI: (state) => !state.users[userId],
// 進行取:
callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
// 在 actions 的開始和結束注入的參數
payload: { userId }
};
}
~~~
解釋這個 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-調用 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 }
};
}
~~~
### Reducers
Redux 用函數描述邏輯更新減少了模版里大量的 Flux stores 。函數比對象簡單,比類更簡單得多。
考慮這個 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` 語句 *不是* 真正的模版。真正的 Flux 模版是概念性的:發送更新的需求,用 Dispatcher 注冊 Store 的需求,Store 是對象的需求 (當你想要一個哪都能跑的 App 的時候復雜度會提升)。
不幸的是很多人仍然靠文檔里用沒用 `switch` 來選擇 Flux 框架。如果你不愛用 `switch` 你可以用一個單獨的函數來解決,下面會演示。
### 生成 Reducers
讓我們寫一個函數使得我們將 reducers 表達為 action types 到 handlers 的映射對象。例如,在我們的 `todos` reducer 里這樣定義:
~~~
export const todos = createReducer([], {
[ActionTypes.ADD_TODO](state, action) {
let text = action.text.trim();
return [...state, text];
}
}
~~~
我們可以寫下面的幫忙函數來完成:
~~~
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action);
} else {
return state;
}
}
}
~~~
不難對吧?Redux 沒有默認提供這樣的幫忙函數,因為有好多種寫的方法。可能你想要自動把普通 JS 對象變成不可變對象通過濕化服務器狀態。可能你想合并返回狀態和當前狀態。有很多方法 “獲取所有” handler。這些都取決于你為你的團隊在特定項目中選擇的約定。
Redux reducer 的 API 是 `(state, action) => state`,但是怎么創建這些 reducers 由你來定。