您好, 欢迎来到 !    登录 | 注册 | | 设为首页 | 收藏本站

Reudx 缩减样板代码

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

Actions

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

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

Action 一般长这样:

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

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

在 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 中能查到所有,,的记录。这能帮助团队中的所有人及时追踪新的范围与实现。

如果你在 import Action 常量的时候拼写错了,你会得到 undefined 。在 dispatch 这个 action 的时候,R 会立即抛出这个,你也会马上发现。

你的项目约定取决与你自己。开始时,可能在刚开始用内联字符串(inline string),之后转为常量,也许再之后将他们归为独立。R 没有任何建议,选择你自己最喜欢的。

Action Creators

另约定俗成的做法是通过创建 action 对象,而不是在你 dispatch 的时候内联它们。

例如,不是使用对象字面量 dispatch :

// event handler 里的某处
dispatch({
  type: 'ADD_TODO',
  text: 'Use R'
})

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

export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}
import { addTodo } from './actionCreators'

// event handler 里的某处
dispatch(addTodo('Use R'))

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

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

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

export function addTodo(text) {
  // R 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 creator 定义 action type 常量。这个想法是说你不需要同时定义 ADD_TODO 常量和 addTodo() action creator 。这样的在底层也了 action type 常量,但他们是隐式的、间接级,会造成混乱。因此我们建议直接清晰地创建 action type 常量。

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

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 的:

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 ,例如  和  。这些库可以有效减少你的样板,并紧守例如  一类的标准。

异步 Action Creators

中间件让你在每个 action 对象 dispatch 出去之前,注入的逻辑来解释你的 action 对象。异步 action 是中间件的最常见用例。

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

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
  }
}
import { Component } from 'react'
import { connect } from 'react-r'
import {
  loadPostsRequest,
  loadPostsSuccess,
  loadPostsFailure
} from './actionCreators'

class Posts extends Component {
  loadData(userId) {
    //  React R `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.

中间件最简单的例子是 . “Thunk” 中间件让你可以把 action creators 写成 “thunks”,也就是返回的。 这使得控制被反转了: 你会像参数一样取得 dispatch ,所以你也能写多次分发的 action creator 。

Thunk 只是中间件的例子。中间件不仅仅是关于 “分发” 的:而是关于你可以使用特定的中间件来分发任何该中间件可以处理的东西。例子中的 Thunk 中间件了特定的行为用来分发,但这实际取决于你用的中间件。

用  重写上面的:

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
        })
    )
  }
}
import { Component } from 'react'
import { connect } from 'react-r'
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 :

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

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

这个 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

用了 R 之后,同样的逻辑更新可以被写成 reducing 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 样板是概念性的:发送更新的需求,用 Dispatcher Store 的需求,Store 是对象的需求 (当你想要哪都能跑的 App 的时候复杂度会提升)。

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

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

export const todos = createReducer([], {
  [ActionTypes.ADD_TODO]: (state, action) => {
    const 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
    }
  }
}

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

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


联系我
置顶