减少样板代码
最后更新于:2022-04-01 05:00:41
# 减少样板代码
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 由你来定。