Skip to main content

减少样板文件

¥Reducing Boilerplate

Redux 属于 灵感来自通量 部分,关于 Flux 最常见的抗诉是它让你编写大量样板文件。在本节中,我们将考虑 Redux 如何让我们根据个人风格、团队偏好、长期可维护性等来选择代码的详细程度。

¥Redux is in part inspired by Flux, and the most common complaint about Flux is how it makes you write a lot of boilerplate. In this recipe, we will consider how Redux lets us choose how verbose we'd like our code to be, depending on personal style, team preferences, longer term maintainability, and so on.

行动

¥Actions

操作是描述应用中发生的事情的普通对象,并且是描述改变数据意图的唯一方式。重要的是,作为必须分派的对象的操作不是样板文件,而是 Redux 的 基本设计选择 之一。

¥Actions are plain objects describing what happened in the app, and serve as the sole way to describe an intention to mutate the data. It's important that actions being objects you have to dispatch is not boilerplate, but one of the fundamental design choices of Redux.

有些框架声称与 Flux 类似,但没有操作对象的概念。就可预测性而言,这是 Flux 或 Redux 的倒退。如果没有可序列化的普通对象操作,则无法记录和重放用户会话,也无法实现 时间旅行热重载。如果你希望直接修改数据,则不需要 Redux。

¥There are frameworks claiming to be similar to Flux, but without a concept of action objects. In terms of being predictable, this is a step backwards from Flux or Redux. If there are no serializable plain object actions, it is impossible to record and replay user sessions, or to implement hot reloading with time travel. If you'd rather modify data directly, you don't need Redux.

动作看起来像这样:

¥Actions look like this:

{ type: 'ADD_TODO', text: 'Use Redux' }
{ type: 'REMOVE_TODO', id: 42 }
{ type: 'LOAD_ARTICLE', response: { ... } }

常见的约定是操作具有常量类型,可以帮助 reducer(或 Flux 中的存储)识别它们。我们建议你对操作类型使用字符串而不是 符号,因为字符串是可序列化的,并且通过使用符号,你会使记录和重放变得比实际需要的更加困难。

¥It is a common convention that actions have a constant type that helps reducers (or Stores in Flux) identify them. We recommend that you use strings and not Symbols for action types, because strings are serializable, and by using Symbols you make recording and replaying harder than it needs to be.

在 Flux 中,传统上认为将每个操作类型定义为字符串常量:

¥In Flux, it is traditionally thought that you would define every action type as a string constant:

const ADD_TODO = 'ADD_TODO'
const REMOVE_TODO = 'REMOVE_TODO'
const LOAD_ARTICLE = 'LOAD_ARTICLE'

为什么这有好处?人们经常声称常量是不必要的,对于小型项目来说,这可能是正确的。对于较大的项目,将操作类型定义为常量有一些好处:

¥Why is this beneficial? It is often claimed that constants are unnecessary, and for small projects, this might be correct. For larger projects, there are some benefits to defining action types as constants:

  • 它有助于保持命名一致,因为所有操作类型都聚集在一个位置。

    ¥It helps keep the naming consistent because all action types are gathered in a single place.

  • 有时你希望在开发新功能之前查看所有现有操作。团队中的某个人可能已经添加了你需要的操作,但你并不知道。

    ¥Sometimes you want to see all existing actions before working on a new feature. It may be that the action you need was already added by somebody on the team, but you didn't know.

  • 在拉取请求中添加、删除和更改的操作类型列表可帮助团队中的每个人跟踪新功能的范围和实现。

    ¥The list of action types that were added, removed, and changed in a Pull Request helps everyone on the team keep track of scope and implementation of new features.

  • 如果导入动作常量时出现拼写错误,你将得到 undefined。当调度这样的操作时,Redux 会立即抛出异常,你会更快发现错误。

    ¥If you make a typo when importing an action constant, you will get undefined. Redux will immediately throw when dispatching such an action, and you'll find the mistake sooner.

你可以为你的项目选择约定。你可以首先使用内联字符串,然后过渡到常量,然后可能将它们分组到单个文件中。Redux 在这里没有任何意见,所以请使用你的最佳判断。

¥It is up to you to choose the conventions for your project. You may start by using inline strings, and later transition to constants, and maybe later group them into a single file. Redux does not have any opinion here, so use your best judgment.

动作创作者

¥Action Creators

另一个常见的约定是,你可以创建生成它们的函数,而不是在分派操作的位置内联创建操作对象。

¥It is another common convention that, instead of creating action objects inline in the places where you dispatch the actions, you would create functions generating them.

例如,不要使用对象字面量调用 dispatch

¥For example, instead of calling dispatch with an object literal:

// somewhere in an event handler
dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
})

你可以在单独的文件中编写操作创建器,并将其导入到你的组件中:

¥You might write an action creator in a separate file, and import it into your component:

actionCreators.js

export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}

AddTodo.js

import { addTodo } from './actionCreators'

// somewhere in an event handler
dispatch(addTodo('Use Redux'))

动作创作者经常被批评为样板。好吧,你不必写它们!如果你觉得对象字面量更适合你的项目,则可以使用它。然而,你应该了解编写动作创建器的一些好处。

¥Action creators have often been criticized as boilerplate. Well, you don't have to write them! You can use object literals if you feel this better suits your project. There are, however, some benefits for writing action creators you should know about.

假设设计师在审查我们的原型后回到我们身边,并告诉我们最多需要允许三个待办事项。我们可以通过使用 redux-thunk 中间件将操作创建器重写为回调形式并添加提前退出来强制执行此操作:

¥Let's say a designer comes back to us after reviewing our prototype, and tells us that we need to allow three todos maximum. We can enforce this by rewriting our action creator to a callback form with redux-thunk middleware and adding an early exit:

function addTodoWithoutCheck(text) {
return {
type: 'ADD_TODO',
text
}
}

export function addTodo(text) {
// This form is allowed by Redux Thunk middleware
// described below in “Async Action Creators” section.
return function (dispatch, getState) {
if (getState().todos.length === 3) {
// Exit early
return
}
dispatch(addTodoWithoutCheck(text))
}
}

我们刚刚修改了 addTodo 动作创建者的行为方式,对调用代码完全不可见。我们不必担心查看添加待办事项的每个位置,以确保它们进行了此项检查。操作创建器可让你将分派操作的附加逻辑与发出这些操作的实际组件分离。当应用处于繁重的开发阶段并且需求经常变化时,它非常方便。

¥We just modified how the addTodo action creator behaves, completely invisible to the calling code. We don't have to worry about looking at each place where todos are being added, to make sure they have this check. Action creators let you decouple additional logic around dispatching an action, from the actual components emitting those actions. It's very handy when the application is under heavy development, and the requirements change often.

生成动作创作者

¥Generating Action Creators

某些框架(例如 Flummox)会根据操作创建器函数定义自动生成操作类型常量。这个想法是,你不需要同时定义 ADD_TODO 常量和 addTodo() 动作创建者。在幕后,此类解决方案仍然会生成操作类型常量,但它们是隐式创建的,因此这是一种间接级别,可能会导致混乱。我们建议显式创建操作类型常量。

¥Some frameworks like Flummox generate action type constants automatically from the action creator function definitions. The idea is that you don't need to both define ADD_TODO constant and addTodo() action creator. Under the hood, such solutions still generate action type constants, but they're created implicitly so it's a level of indirection and can cause confusion. We recommend creating your action type constants explicitly.

编写简单的动作创建器可能很烦人,并且通常最终会生成冗余的样板代码:

¥Writing simple action creators can be tiresome and often ends up generating redundant boilerplate code:

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
}
}

你始终可以编写一个生成动作创建者的函数:

¥You can always write a function that generates an action creator:

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')

还有一些实用程序库可以帮助生成动作创建器,例如 redux-actredux-actions。这些可以帮助减少样板代码并强制遵守 通量标准作用 (FSA) 等标准。

¥There are also utility libraries to aid in generating action creators, such as redux-act and redux-actions. These can help reduce boilerplate code and enforce adherence to standards such as Flux Standard Action (FSA).

异步动作创建者

¥Async Action Creators

中间件 允许你注入自定义逻辑,在分派每个操作对象之前对其进行解释。异步操作是中间件最常见的用例。

¥Middleware lets you inject custom logic that interprets every action object before it is dispatched. Async actions are the most common use case for middleware.

在没有任何中间件的情况下,dispatch 只接受普通对象,因此我们必须在组件内执行 AJAX 调用:

¥Without any middleware, dispatch only accepts a plain object, so we have to perform AJAX calls inside our components:

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) {
// Injected into props by React Redux `connect()` call:
const { dispatch, posts } = this.props

if (posts[userId]) {
// There is cached data! Don't do anything.
return
}

// Reducer can react to this action by setting
// `isFetching` and thus letting us show a spinner.
dispatch(loadPostsRequest(userId))

// Reducer can react to these actions by filling the `users`.
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)

然而,这很快就会变得重复,因为不同的组件从相同的 API 端点请求数据。此外,我们希望从许多组件中重用一些逻辑(例如,当有可用缓存数据时提前退出)。

¥However, this quickly gets repetitive because different components request data from the same API endpoints. Moreover, we want to reuse some of this logic (e.g., early exit when there is cached data available) from many components.

中间件让我们能够编写更具表现力的、潜在的异步动作创建器。它让我们可以调度普通对象以外的东西,并解释这些值。例如,中间件可以“捕获”已调度的 Promise,并将其转换为一对请求和成功/失败操作。

¥Middleware lets us write more expressive, potentially async action creators. It lets us dispatch something other than plain objects, and interprets the values. For example, middleware can “catch” dispatched Promises and turn them into a pair of request and success/failure actions.

中间件最简单的例子是 redux-thunk。“Thunk”中间件允许你将动作创建者编写为“thunk”,即返回函数的函数。这会反转控制:你将得到 dispatch 作为参数,因此你可以编写一个多次分派的动作创建者。

¥The simplest example of middleware is redux-thunk. “Thunk” middleware lets you write action creators as “thunks”, that is, functions returning functions. This inverts the control: you will get dispatch as an argument, so you can write an action creator that dispatches many times.

注意

¥Note

Thunk 中间件只是中间件的示例之一。中间件并不是“让你分派函数”。它让你分派你使用的特定中间件知道如何处理的任何内容。Thunk 中间件在你分派函数时添加特定行为,但这实际上取决于你使用的中间件。

¥Thunk middleware is just one example of middleware. Middleware is not about “letting you dispatch functions”. It's about letting you dispatch anything that the particular middleware you use knows how to handle. Thunk middleware adds a specific behavior when you dispatch functions, but it really depends on the middleware you use.

考虑用 redux-thunk 重写上面的代码:

¥Consider the code above rewritten with redux-thunk:

actionCreators.js

export function loadPosts(userId) {
// Interpreted by the thunk middleware:
return function (dispatch, getState) {
const { posts } = getState()
if (posts[userId]) {
// There is cached data! Don't do anything.
return
}

dispatch({
type: 'LOAD_POSTS_REQUEST',
userId
})

// Dispatch vanilla actions asynchronously
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

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)

这样打字就少多了!如果你愿意,你仍然可以拥有像 loadPostsSuccess 这样的“普通”动作创建器,你可以从容器 loadPosts 动作创建器中使用它。

¥This is much less typing! If you'd like, you can still have “vanilla” action creators like loadPostsSuccess which you'd use from a container loadPosts action creator.

最后,你可以编写自己的中间件。假设你想要概括上面的模式并像这样描述你的异步操作创建者:

¥Finally, you can write your own middleware. Let's say you want to generalize the pattern above and describe your async action creators like this instead:

export function loadPosts(userId) {
return {
// Types of actions to emit before and after
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
// Check the cache (optional):
shouldCallAPI: state => !state.posts[userId],
// Perform the fetching:
callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
// Arguments to inject in begin/end actions
payload: { userId }
}
}

解释此类操作的中间件可能如下所示:

¥The middleware that interprets such actions could look like this:

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 调用操作创建器:

¥After passing it once to applyMiddleware(...middlewares), you can write all your API-calling action creators the same way:

export function loadPosts(userId) {
return {
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
shouldCallAPI: state => !state.posts[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.comments[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 }
}
}

Reducer

Redux 通过将更新逻辑描述为函数,大大减少了 Flux 存储的样板。函数比对象简单,比类简单得多。

¥Redux reduces the boilerplate of Flux stores considerably by describing the update logic as a function. A function is simpler than an object, and much simpler than a class.

考虑这个 Flux 存储:

¥Consider this Flux store:

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,相同的更新逻辑可以描述为 reducer 函数:

¥With Redux, the same update logic can be described as a reducer function:

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 的真正样板是概念性的:需要发出更新、需要向调度程序注册 Store、需要将 Store 作为一个对象(以及当你想要通用应用时出现的复杂情况)。

¥The switch statement is not the real boilerplate. The real boilerplate of Flux is conceptual: the need to emit an update, the need to register the Store with a Dispatcher, the need for the Store to be an object (and the complications that arise when you want a universal app).

不幸的是,许多人仍然根据文档中是否使用 switch 语句来选择 Flux 框架。如果你不喜欢 switch,你可以使用单个函数来解决此问题,如下所示。

¥It's unfortunate that many still choose Flux framework based on whether it uses switch statements in the documentation. If you don't like switch, you can solve this with a single function, as we show below.

生成 Reducer

¥Generating Reducers

让我们编写一个函数,让我们将化简器表示为从操作类型到处理程序的对象映射。例如,如果我们希望 todos reducer 定义如下:

¥Let's write a function that lets us express reducers as an object mapping from action types to handlers. For example, if we want our todos reducers to be defined like this:

export const todos = createReducer([], {
[ActionTypes.ADD_TODO]: (state, action) => {
const text = action.text.trim()
return [...state, text]
}
})

我们可以编写以下助手来完成此任务:

¥We can write the following helper to accomplish this:

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 对象以补充服务器状态。也许你想将返回的状态与当前状态合并。“捕获所有”处理程序可能有不同的方法。所有这些都取决于你为特定项目的团队选择的约定。

¥This wasn't difficult, was it? Redux doesn't provide such a helper function by default because there are many ways to write it. Maybe you want it to automatically convert plain JS objects to Immutable objects to hydrate the server state. Maybe you want to merge the returned state with the current state. There may be different approaches to a “catch all” handler. All of this depends on the conventions you choose for your team on a specific project.

Redux reducer API 是 (state, action) => newState,但如何创建这些 reducer 取决于你。

¥The Redux reducer API is (state, action) => newState, but how you create those reducers is up to you.