# 縮減樣板代碼
Redux 很大部分 [受到 Flux 的啟發](../introduction/PriorArt.md),而最常見的關于 Flux 的抱怨是必須寫一大堆的樣板代碼。在這章中,我們將考慮 Redux 如何根據個人風格,團隊偏好,長期可維護性等自由決定代碼的繁復程度。
## Actions
Actions 是用來描述在 app 中發生了什么的普通對象,并且是描述突變數據意圖的唯一途徑。很重要的一點是 **不得不 dispatch 的 action 對象并非是一個樣板代碼,而是 Redux 的一個 [基本設計選擇](../introduction/ThreePrinciples.md)**.
不少框架聲稱自己和 Flux 很像,只不過缺少了 action 對象的概念。在可預測性方面,這是從 Flux 或 Redux 的倒退。如果沒有可序列化的普通對象 action,便無法記錄或重演用戶會話,也無法實現 [帶有時間旅行的熱重載](https://www.youtube.com/watch?v=xsSnOQynTHs)。如果你更喜歡直接修改數據,那你并不需要使用 Redux 。
Action 一般長這樣:
```javascript
{ type: 'ADD_TODO', text: 'Use Redux' }
{ type: 'REMOVE_TODO', id: 42 }
{ type: 'LOAD_ARTICLE', response: { ... } }
```
一個約定俗成的做法是,action 擁有一個不變的 type 幫助 reducer (或 Flux 中的 Stores ) 識別它們。我們建議你使用 string 而不是 [符號(Symbols)](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 作為 action type ,因為 string 是可序列化的,并且使用符號會使記錄和重演變得困難。
在 Flux 中,傳統的想法是將每個 action type 定義為 string 常量:
```javascript
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 中能查到所有添加,刪除,修改的記錄。這能幫助團隊中的所有人及時追蹤新功能的范圍與實現。
- 如果你在 import 一個 Action 常量的時候拼寫錯了,你會得到 `undefined` 。在 dispatch 這個 action 的時候,Redux 會立即拋出這個錯誤,你也會馬上發現錯誤。
你的項目約定取決與你自己。開始時,可能在剛開始用內聯字符串(inline string),之后轉為常量,也許再之后將他們歸為一個獨立文件。Redux 在這里沒有任何建議,選擇你自己最喜歡的。
## Action Creators
另一個約定俗成的做法是通過創建函數生成 action 對象,而不是在你 dispatch 的時候內聯生成它們。
例如,不是使用對象字面量調用 `dispatch` :
```javascript
// event handler 里的某處
dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
})
```
你其實可以在單獨的文件中寫一個 action creator ,然后從 component 里 import:
#### `actionCreators.js`
```javascript
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}
```
#### `AddTodo.js`
```javascript
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 重寫成回調形式:
```javascript
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 邏輯與實際發送這些 action 的 components 。當你有大量開發工作且需求經常變更的時候,這種方法十分簡便易用。
### Action Creators 生成器
某些框架如 [Flummox](https://github.com/acdlite/flummox) 自動從 action creator 函數定義生成 action type 常量。這個想法是說你不需要同時定義 `ADD_TODO` 常量和 `addTodo()` action creator 。這樣的方法在底層也生成了 action type 常量,但他們是隱式生成的、間接級,會造成混亂。因此我們建議直接清晰地創建 action type 常量。
寫簡單的 action creator 很容易讓人厭煩,且往往最終生成多余的樣板代碼:
```javascript
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}
export function editTodo(id, text) {
return {
type: 'EDIT_TODO',
id,
text
}
}
export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
}
}
```
你可以寫一個用于生成 action creator 的函數:
```javascript
function makeActionCreator(type, ...argNames) {
return function(...args) {
const action = { type }
argNames.forEach((arg, index) => {
action[argNames[index]] = args[index]
})
return action
}
}
const ADD_TODO = 'ADD_TODO'
const EDIT_TODO = 'EDIT_TODO'
const REMOVE_TODO = 'REMOVE_TODO'
export const addTodo = makeActionCreator(ADD_TODO, 'text')
export const editTodo = makeActionCreator(EDIT_TODO, 'id', 'text')
export const removeTodo = makeActionCreator(REMOVE_TODO, 'id')
```
一些工具庫也可以幫助生成 action creator ,例如 [redux-act](https://github.com/pauldijou/redux-act) 和 [redux-actions](https://github.com/acdlite/redux-actions) 。這些庫可以有效減少你的樣板代碼,并緊守例如 [Flux Standard Action (FSA)](https://github.com/acdlite/flux-standard-action) 一類的標準。
## 異步 Action Creators
[中間件](../Glossary.html#middleware) 讓你在每個 action 對象 dispatch 出去之前,注入一個自定義的邏輯來解釋你的 action 對象。異步 action 是中間件的最常見用例。
如果沒有中間件,[`dispatch`](../api/Store.md#dispatch) 只能接收一個普通對象。因此我們必須在 components 里面進行 AJAX 調用:
#### `actionCreators.js`
```javascript
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`
```javascript
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 :
const { 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)
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.loadData(this.props.userId)
}
}
render() {
if (this.props.isFetching) {
return <p>Loading...</p>
}
const posts = this.props.posts.map(post => (
<Post post={post} key={post.id} />
))
return <div>{posts}</div>
}
}
export default connect(state => ({
posts: state.posts,
isFetching: state.isFetching
}))(Posts)
```
然而,不久就需要再來一遍,因為不同的 components 從同樣的 API 端點請求數據。而且我們想要在多個 components 中重用一些邏輯(比如,當緩存數據有效的時候提前退出)。
**中間件讓我們能寫表達更清晰的、潛在的異步 action creators。** 它允許我們 dispatch 普通對象之外的東西,并且解釋它們的值。比如,中間件能 “捕捉” 到已經 dispatch 的 Promises 并把他們變為一對請求和成功/失敗的 action.
中間件最簡單的例子是 [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`
```javascript
export function loadPosts(userId) {
// 用 thunk 中間件解釋:
return function(dispatch, getState) {
const { posts } = getState()
if (posts[userId]) {
// 這里是數據緩存!啥也不做。
return
}
dispatch({
type: 'LOAD_POSTS_REQUEST',
userId
})
// 異步分發原味 action
fetch(`http://myapi.com/users/${userId}/posts`).then(
response =>
dispatch({
type: 'LOAD_POSTS_SUCCESS',
userId,
response
}),
error =>
dispatch({
type: 'LOAD_POSTS_FAILURE',
userId,
error
})
)
}
}
```
#### `UserInfo.js`
```javascript
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))
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.props.dispatch(loadPosts(this.props.userId))
}
}
render() {
if (this.props.isFetching) {
return <p>Loading...</p>
}
const posts = this.props.posts.map(post => (
<Post post={post} key={post.id} />
))
return <div>{posts}</div>
}
}
export default connect(state => ({
posts: state.posts,
isFetching: state.isFetching
}))(Posts)
```
這樣打得字少多了!如果你喜歡,你還是可以保留 “原味” action creators 比如從一個容器 `loadPosts` action creator 里用到的 `loadPostsSuccess` 。
**最后,你可以編寫你自己的中間件** 你可以把上面的模式泛化,然后代之以這樣的異步 action creators :
```js
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 的中間件可以像這樣:
```javascript
function callAPIMiddleware({ dispatch, getState }) {
return next => action => {
const { types, callAPI, shouldCallAPI = () => true, payload = {} } = action
if (!types) {
// Normal action: pass it on
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 callAPI 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,
type: successType
})
),
error =>
dispatch(
Object.assign({}, payload, {
error,
type: failureType
})
)
)
}
}
```
在傳給 [`applyMiddleware(...middlewares)`](../api/applyMiddleware.md) 一次以后,你能用相同方式寫你的 API 調用 action creators :
```javascript
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 reducer 用函數描述邏輯更新減少了樣板代碼里大量的 Flux stores 。函數比對象簡單,比類更簡單得多。
這個 Flux store:
```javascript
const _todos = []
const TodoStore = Object.assign({}, EventEmitter.prototype, {
getAll() {
return _todos
}
})
AppDispatcher.register(function(action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
const text = action.text.trim()
_todos.push(text)
TodoStore.emitChange()
}
})
export default TodoStore
```
用了 Redux 之后,同樣的邏輯更新可以被寫成 reducing function:
```js
export function todos(state = [], action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
const 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 里這樣定義:
```javascript
export const todos = createReducer([], {
[ActionTypes.ADD_TODO]: (state, action) => {
const text = action.text.trim()
return [...state, text]
}
})
```
我們可以編寫下面的輔助函數來完成:
```javascript
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 對象變成 Immutable 對象,以填滿服務器狀態的對象數據。可能你想合并返回狀態和當前狀態。有多種多樣的方法來 “獲取所有” handler,具體怎么做則取決于項目中你和你的團隊的約定。
Redux reducer 的 API 是 `(state, action) => newState`,但是怎么創建這些 reducers 由你來定。
- 自述
- 介紹
- 動機
- 核心概念
- 三大原則
- 先前技術
- 學習資源
- 生態系統
- 示例
- 基礎
- Action
- Reducer
- Store
- 數據流
- 搭配 React
- 示例:Todo List
- 高級
- 異步 Action
- 異步數據流
- Middleware
- 搭配 React Router
- 示例:Reddit API
- 下一步
- 技巧
- 配置 Store
- 遷移到 Redux
- 使用對象展開運算符
- 減少樣板代碼
- 服務端渲染
- 編寫測試
- 計算衍生數據
- 實現撤銷重做
- 子應用隔離
- 組織 Reducer
- Reducer 基礎概念
- Reducer 基礎結構
- Reducer 邏輯拆分
- Reducer 重構示例
- combineReducers 用法
- combineReducers 進階
- State 范式化
- 管理范式化數據
- Reducer 邏輯復用
- 不可變更新模式
- 初始化 State
- 結合 Immutable.JS 使用 Redux
- 常見問題
- 綜合
- Reducer
- 組織 State
- 創建 Store
- Action
- 不可變數據
- 代碼結構
- 性能
- 設計哲學
- React Redux
- 其它
- 排錯
- 詞匯表
- API 文檔
- createStore
- Store
- combineReducers
- applyMiddleware
- bindActionCreators
- compose
- react-redux 文檔
- API
- 排錯