减少样板代码

Redux 很大部分 受到 Flux 的启发,而最常见的关于 Flux 的抱怨是必须写一大堆的样板代码。在这章中,我们将考虑 Redux 如何根据个人风格,团队偏好,长期可维护性等自由决定代码的繁复程度。

Actions

Actions 是用来描述在 app 中发生了什么的普通对象,并且是描述突变数据意图的唯一途径。很重要的一点是 不得不 dispatch 的 action 对象并非是一个样板代码,而是 Redux 的一个 基本设计原则.

不少框架声称自己和 Flux 很像,只不过缺少了 action 对象的概念。在可预测性方面,这是从 Flux 或 Redux 的倒退。如果没有可序列化的普通对象 action,便无法记录或重演用户会话,也无法实现 带有时间旅行的热重载。如果你更喜欢直接修改数据,那你并不需要使用 Redux 。

Action 一般长这样:

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

一个约定俗成的做法是,action 拥有一个不变的 type 帮助 reducer (或 Flux 中的 Stores ) 识别它们。我们建议你使用 string 而不是 符号(Symbols) 作为 action type ,因为 string 是可序列化的,并且使用符号会使记录和重演变得困难。

在 Flux 中,传统的想法是将每个 action type 定义为 string 常量:

  1. const ADD_TODO = 'ADD_TODO'
  2. const REMOVE_TODO = 'REMOVE_TODO'
  3. 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

  1. // event handler 里的某处
  2. dispatch({
  3. type: 'ADD_TODO',
  4. text: 'Use Redux'
  5. })

你其实可以在单独的文件中写一个 action creator ,然后从 component 里 import:

actionCreators.js

  1. export function addTodo(text) {
  2. return {
  3. type: 'ADD_TODO',
  4. text
  5. }
  6. }

AddTodo.js

  1. import { addTodo } from './actionCreators'
  2. // event handler 里的某处
  3. dispatch(addTodo('Use Redux'))

Action creators 总被当作样板代码受到批评。好吧,其实你并不非得把他们写出来!如果你觉得更适合你的项目,你可以选用对象字面量 然而,你应该知道写 action creators 是存在某种优势的。

假设有个设计师看完我们的原型之后回来说,我们最多只允许三个 todo 。我们可以使用 redux-thunk 中间件,并添加一个提前退出,把我们的 action creator 重写成回调形式:

  1. function addTodoWithoutCheck(text) {
  2. return {
  3. type: 'ADD_TODO',
  4. text
  5. }
  6. }
  7. export function addTodo(text) {
  8. // Redux Thunk 中间件允许这种形式
  9. // 在下面的 “异步 Action Creators” 段落中有写
  10. return function(dispatch, getState) {
  11. if (getState().todos.length === 3) {
  12. // 提前退出
  13. return
  14. }
  15. dispatch(addTodoWithoutCheck(text))
  16. }
  17. }

我们刚修改了 addTodo action creator 的行为,使得它对调用它的代码完全不可见。我们不用担心去每个添加 todo 的地方看一看,以确认他们有了这个检查 Action creator 让你可以解耦额外的分发 action 逻辑与实际发送这些 action 的 components。当你有大量开发工作且需求经常变更的时候,这种方法十分简便易用。

Action Creators 生成器

某些框架如 Flummox 自动从 action creator 函数定义生成 action type 常量。这个想法是说你不需要同时定义 ADD_TODO 常量和 addTodo() action creator 。这样的方法在底层也生成了 action type 常量,但他们是隐式生成的、间接级,会造成混乱。因此我们建议直接清晰地创建 action type 常量。

写简单的 action creator 很容易让人厌烦,且往往最终生成多余的样板代码:

  1. export function addTodo(text) {
  2. return {
  3. type: 'ADD_TODO',
  4. text
  5. }
  6. }
  7. export function editTodo(id, text) {
  8. return {
  9. type: 'EDIT_TODO',
  10. id,
  11. text
  12. }
  13. }
  14. export function removeTodo(id) {
  15. return {
  16. type: 'REMOVE_TODO',
  17. id
  18. }
  19. }

你可以写一个用于生成 action creator 的函数:

  1. function makeActionCreator(type, ...argNames) {
  2. return function(...args) {
  3. const action = { type }
  4. argNames.forEach((arg, index) => {
  5. action[argNames[index]] = args[index]
  6. })
  7. return action
  8. }
  9. }
  10. const ADD_TODO = 'ADD_TODO'
  11. const EDIT_TODO = 'EDIT_TODO'
  12. const REMOVE_TODO = 'REMOVE_TODO'
  13. export const addTodo = makeActionCreator(ADD_TODO, 'text')
  14. export const editTodo = makeActionCreator(EDIT_TODO, 'id', 'text')
  15. export const removeTodo = makeActionCreator(REMOVE_TODO, 'id')

一些工具库也可以帮助生成 action creator ,例如 redux-actredux-actions。这些库可以有效减少你的样板代码,并紧守例如 Flux Standard Action (FSA) 一类的标准。

异步 Action Creators

中间件 让你在每个 action 对象 dispatch 出去之前,注入一个自定义的逻辑。异步 action 是中间件的最常见的使用场景。

如果没有中间件,dispatch 只能接收一个普通对象。因此我们必须在 components 里面进行 AJAX 调用:

actionCreators.js

  1. export function loadPostsSuccess(userId, response) {
  2. return {
  3. type: 'LOAD_POSTS_SUCCESS',
  4. userId,
  5. response
  6. }
  7. }
  8. export function loadPostsFailure(userId, error) {
  9. return {
  10. type: 'LOAD_POSTS_FAILURE',
  11. userId,
  12. error
  13. }
  14. }
  15. export function loadPostsRequest(userId) {
  16. return {
  17. type: 'LOAD_POSTS_REQUEST',
  18. userId
  19. }
  20. }

UserInfo.js

  1. import { Component } from 'react'
  2. import { connect } from 'react-redux'
  3. import {
  4. loadPostsRequest,
  5. loadPostsSuccess,
  6. loadPostsFailure
  7. } from './actionCreators'
  8. class Posts extends Component {
  9. loadData(userId) {
  10. // 调用 React Redux `connect()` 注入的 props :
  11. const { dispatch, posts } = this.props
  12. if (posts[userId]) {
  13. // 这里是被缓存的数据!啥也不做。
  14. return
  15. }
  16. // Reducer 可以通过设置 `isFetching` 响应这个 action
  17. // 因此让我们显示一个 Spinner 控件。
  18. dispatch(loadPostsRequest(userId))
  19. // Reducer 可以通过填写 `users` 响应这些 actions
  20. fetch(`http://myapi.com/users/${userId}/posts`).then(
  21. response => dispatch(loadPostsSuccess(userId, response)),
  22. error => dispatch(loadPostsFailure(userId, error))
  23. )
  24. }
  25. componentDidMount() {
  26. this.loadData(this.props.userId)
  27. }
  28. componentDidUpdate(prevProps) {
  29. if (prevProps.userId !== this.props.userId) {
  30. this.loadData(this.props.userId)
  31. }
  32. }
  33. render() {
  34. if (this.props.isFetching) {
  35. return <p>Loading...</p>
  36. }
  37. const posts = this.props.posts.map(post => (
  38. <Post post={post} key={post.id} />
  39. ))
  40. return <div>{posts}</div>
  41. }
  42. }
  43. export default connect(state => ({
  44. posts: state.posts,
  45. isFetching: state.isFetching
  46. }))(Posts)

然而,不久就需要再来一遍,因为不同的 components 从同样的 API 端点请求数据。而且我们想要在多个 components 中重用一些逻辑(比如,当缓存数据有效的时候提前退出)。

中间件让我们能写表达更清晰的、潜在的异步 action creators。 它允许我们 dispatch 普通对象之外的东西,并且解释它们的值。比如,中间件能 “捕捉” 到已经 dispatch 的 Promises 并把他们变为一对请求和成功/失败的 action.

中间件最简单的例子是 redux-thunk. “Thunk” 中间件让你可以把 action creators 写成 “thunks”,也就是返回函数的函数。 这使得控制被反转了:你可以通过参数拿到 dispatch ,所以你也能 dispatch 多次 action creator。

注意

Thunk 只是一个中间件的例子。中间件不仅仅只能处理 “dispatch 函数”:而是关于你可以使用特定的中间件来 dispatch 任何该中间件可以处理的东西。例子中的 Thunk 中间件添加了一个特定的行为用来 dispatch 函数,但这实际做什么取决于你用的中间件。

redux-thunk 重写上面的代码:

actionCreators.js

  1. export function loadPosts(userId) {
  2. // 用 thunk 中间件解释:
  3. return function(dispatch, getState) {
  4. const { posts } = getState()
  5. if (posts[userId]) {
  6. // 这里是数据缓存!啥也不做。
  7. return
  8. }
  9. dispatch({
  10. type: 'LOAD_POSTS_REQUEST',
  11. userId
  12. })
  13. // 异步分发原味 action
  14. fetch(`http://myapi.com/users/${userId}/posts`).then(
  15. response =>
  16. dispatch({
  17. type: 'LOAD_POSTS_SUCCESS',
  18. userId,
  19. response
  20. }),
  21. error =>
  22. dispatch({
  23. type: 'LOAD_POSTS_FAILURE',
  24. userId,
  25. error
  26. })
  27. )
  28. }
  29. }

UserInfo.js

  1. import { Component } from 'react'
  2. import { connect } from 'react-redux'
  3. import { loadPosts } from './actionCreators'
  4. class Posts extends Component {
  5. componentDidMount() {
  6. this.props.dispatch(loadPosts(this.props.userId))
  7. }
  8. componentDidUpdate(prevProps) {
  9. if (prevProps.userId !== this.props.userId) {
  10. this.props.dispatch(loadPosts(this.props.userId))
  11. }
  12. }
  13. render() {
  14. if (this.props.isFetching) {
  15. return <p>Loading...</p>
  16. }
  17. const posts = this.props.posts.map(post => (
  18. <Post post={post} key={post.id} />
  19. ))
  20. return <div>{posts}</div>
  21. }
  22. }
  23. export default connect(state => ({
  24. posts: state.posts,
  25. isFetching: state.isFetching
  26. }))(Posts)

这样打得字少多了!如果你喜欢,你还是可以保留 “原味” action creators 比如从一个容器 loadPosts action creator 里用到的 loadPostsSuccess

最后,你可以编写你自己的中间件 你可以把上面的模式泛化,然后代之以这样的异步 action creators :

  1. export function loadPosts(userId) {
  2. return {
  3. // 要在之前和之后发送的 action types
  4. types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
  5. // 检查缓存 (可选):
  6. shouldCallAPI: state => !state.users[userId],
  7. // 进行取:
  8. callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
  9. // 在 actions 的开始和结束注入的参数
  10. payload: { userId }
  11. }
  12. }

解释这个 actions 的中间件可以像这样:

  1. function callAPIMiddleware({ dispatch, getState }) {
  2. return next => action => {
  3. const { types, callAPI, shouldCallAPI = () => true, payload = {} } = action
  4. if (!types) {
  5. // Normal action: pass it on
  6. return next(action)
  7. }
  8. if (
  9. !Array.isArray(types) ||
  10. types.length !== 3 ||
  11. !types.every(type => typeof type === 'string')
  12. ) {
  13. throw new Error('Expected an array of three string types.')
  14. }
  15. if (typeof callAPI !== 'function') {
  16. throw new Error('Expected callAPI to be a function.')
  17. }
  18. if (!shouldCallAPI(getState())) {
  19. return
  20. }
  21. const [requestType, successType, failureType] = types
  22. dispatch(
  23. Object.assign({}, payload, {
  24. type: requestType
  25. })
  26. )
  27. return callAPI().then(
  28. response =>
  29. dispatch(
  30. Object.assign({}, payload, {
  31. response,
  32. type: successType
  33. })
  34. ),
  35. error =>
  36. dispatch(
  37. Object.assign({}, payload, {
  38. error,
  39. type: failureType
  40. })
  41. )
  42. )
  43. }
  44. }

在传给 applyMiddleware(...middlewares) 一次以后,你能用相同方式写你的 API 调用 action creators :

  1. export function loadPosts(userId) {
  2. return {
  3. types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
  4. shouldCallAPI: state => !state.users[userId],
  5. callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
  6. payload: { userId }
  7. }
  8. }
  9. export function loadComments(postId) {
  10. return {
  11. types: [
  12. 'LOAD_COMMENTS_REQUEST',
  13. 'LOAD_COMMENTS_SUCCESS',
  14. 'LOAD_COMMENTS_FAILURE'
  15. ],
  16. shouldCallAPI: state => !state.posts[postId],
  17. callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`),
  18. payload: { postId }
  19. }
  20. }
  21. export function addComment(postId, message) {
  22. return {
  23. types: [
  24. 'ADD_COMMENT_REQUEST',
  25. 'ADD_COMMENT_SUCCESS',
  26. 'ADD_COMMENT_FAILURE'
  27. ],
  28. callAPI: () =>
  29. fetch(`http://myapi.com/posts/${postId}/comments`, {
  30. method: 'post',
  31. headers: {
  32. Accept: 'application/json',
  33. 'Content-Type': 'application/json'
  34. },
  35. body: JSON.stringify({ message })
  36. }),
  37. payload: { postId, message }
  38. }
  39. }

Reducers

Redux 用函数描述逻辑更新减少了 Flux stores 里的大量样板代码。函数比对象简单,比类更简单得多。

这个 Flux store:

  1. const _todos = []
  2. const TodoStore = Object.assign({}, EventEmitter.prototype, {
  3. getAll() {
  4. return _todos
  5. }
  6. })
  7. AppDispatcher.register(function(action) {
  8. switch (action.type) {
  9. case ActionTypes.ADD_TODO:
  10. const text = action.text.trim()
  11. _todos.push(text)
  12. TodoStore.emitChange()
  13. }
  14. })
  15. export default TodoStore

用了 Redux 之后,同样的逻辑更新可以被写成 reducing function:

  1. export function todos(state = [], action) {
  2. switch (action.type) {
  3. case ActionTypes.ADD_TODO:
  4. const text = action.text.trim()
  5. return [...state, text]
  6. default:
  7. return state
  8. }
  9. }

switch 语句 不是 真正的样板代码。真正的 Flux 样板代码是概念性的:发送更新的需求,用 Dispatcher 注册 Store 的需求,Store 是对象的需求 (当你想要一个哪都能跑的 App 的时候复杂度会提升)。

不幸的是很多人仍然靠文档里用没用 switch 来选择 Flux 框架。如果你不爱用 switch 你可以用一个单独的函数来解决,下面会演示。

Reducers 生成器

写一个函数将 reducers 表达为 action types 到 handlers 的映射对象。例如,如果想在 todos reducer 里这样定义:

  1. export const todos = createReducer([], {
  2. [ActionTypes.ADD_TODO]: (state, action) => {
  3. const text = action.text.trim()
  4. return [...state, text]
  5. }
  6. })

我们可以编写下面的辅助函数来完成:

  1. function createReducer(initialState, handlers) {
  2. return function reducer(state = initialState, action) {
  3. if (handlers.hasOwnProperty(action.type)) {
  4. return handlers[action.type](state, action)
  5. } else {
  6. return state
  7. }
  8. }
  9. }

不难对吧?鉴于写法多种多样,Redux 没有默认提供这样的辅助函数。可能你想要自动地将普通 JS 对象变成 Immutable 对象,以填满服务器状态的对象数据。可能你想合并返回状态和当前状态。有多种多样的方法来 “获取所有” handler,具体怎么做则取决于项目中你和你的团队的约定。

Redux reducer 的 API 是 (state, action) => newState,但是怎么创建这些 reducers 由你来定。