9

Redux 官方TODO示例解析

 3 years ago
source link: https://zhuanlan.zhihu.com/p/22022941
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Redux 官方TODO示例解析

Todos 示例几乎是所有类似框架示例的标配。对于Redux来说,它的TODO示例虽然代码量不大,但是涵盖了Redux的绝大部分思想在里面。

通过这个示例可以帮助理解:

  1. 如何定义state,以及如何更新state
  2. 如何把state和React的component连接起来

对于TODO解析的文章很多,包括官方文档里面有大量的篇幅在描述,所以这里我将尝试从另一个视角来解析TODO示例,也就是系统设计角度来分析TODO示例,借此机会看看基于Redux+React怎么分析设计系统。

TODO的功能

TODO示例实现的TODO List功能其实很简单,包含以下功能:

  • 添加任务项
  • 设置任务为完成状态
  • 根据完成状态分组显示任务列表
    • All: 显示所有任务
    • Active: 仅显示已完成任务
    • Completed: 仅显示未完成任务

如下图所示:

不支持gif动图请点击:http://d.pr/i/1bsGM/5SDh6Eov

6b6fa8461d55d33a3fd38d999b95b409_720w.jpg

系统设计

我们知道,React是基于stateJSX来生成Dom的,而redux则是帮助我们来管理state的。所以基于Redux + React来开发系统,在设计的时候需要想清楚几个问题:

  1. 如何定义state结构
  2. 如何定义component结构
  3. 如何将state和component连接起来
  4. 如何更新state数据

State结构

一般在定义state结构前,可以先理一下组成state的最基本的单位有哪些,这个TODO项目,最基本的单位有两个:

  1. 任务
    一个TODO应用的基本单位之一是任务,任务可以用js的object类型来描述,主要由任务的唯一ID、任务内容和是否完成。
todo: object // 任务对象
{
    id: number, // 唯一标识Id
    text: string, // 任务内容
    completed: bool // 是否完成,true: 完成; false: 未完成
}
  1. 当前分组(过滤)类型
    一共有三种分组类型:All, Active, Completed。可以简单的用字符串来表示:
visibilityFilter: string 
    // 当前分组类型
    // SHOW_ALL: 显示所有任务
    // SHOW_COMPLETED: 仅显示已完成任务
    // SHOW_ACTIVE: 仅显示未完成任务

有了基本单位,就可以组合出来最终的state结构。对于state的结构,Redux建议在应该尽可能地遵循范式,避免嵌套数据结构。如果出现了嵌套的对象,那么尽量通过 ID 来引用。也就是说对于任务列表,建议使用两个集合来存储。《Redux 核心概念》一文的State 结构设计一章也有详细描述,所以这里对于任务列表的最佳设计应该是拆成两个:todosById: { id -> todo } 和 array<id>,但在当前示例中,为了简化,还是直接用一个 todos: array<todo> 数组来表示。

Note on Relationships

In a more complex app, you’re going to want different entities to reference each other. We suggest that you keep your state as normalized as possible, without any nesting. Keep every entity in an object stored with an ID as a key, and use IDs to reference it from other entities, or lists. Think of the app’s state as a database. This approach is described in normalizr's documentation in detail. For example, keeping `todosById: { id -> todo }` and todos: `array` inside the state would be a better idea in a real app, but we’re keeping the example simple.

所以最终state结构如下:

{
	todos: array<todo>,
	visibilityFilter: string
}

根据定义好的state结构,尝试填充一些数据到state里面,如下表所示:

有没有一点数据库的感觉?事实上定义state结构,很多时候就像在设计数据库,只是state结构更灵活也更复杂,还是要根据实际情况灵活运用。

Component结构

React的Component结构是一个典型的树结构,根据界面还是很好拆分成Component树。

连接State和Component

现在有了state,有了component,还需要把它们连接起来,让component可以读取到state的数据,state的数据更新后,component会随之刷新。

Redux把component分成两类:展示组件(Presentational Components)和容器组件(Container Components)。在其官方文档(中文 | English)中针对这两类组件有详细的说明。其中的容器组件就是担任连接state和component的角色。

连接TodoList

回到最开始的需求说明,我们的todo列表是可以分类过滤的,例如选中Active,则todo列表仅显示未完成的。也就是说我们的TodoList组件的数据来源,是visibilityFilter和todos这两部分数据共同组成。

根据当前选中的分类,有三种可能:

  1. All:visibilityFilter=SHOW_ALL,显示所有todos。
  2. Active: visibilityFilter=SHOW_ACTIVE,显示所有未完成的todos。
  3. Completed: visibilityFilter=SHOW_COMPLETED,显示所有已经完成的todos。

所以TodoList所需的state数据,需要todos和visibilityFilter组合完成,如图所示:

示例中的相关代码:

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
  }
}

如果觉得ES6的代码不好懂,参考之前数据库的比喻,把选择Active的todos写成SQL应该是这样:

select * from todos where completed = false;

连接FilterLink

通过点击底部的三个链接,可以分组显示todos,同时当前选中的分组会显示成普通文字而不是链接。例如下图是选中Completed链接后效果:

这意味着我们需要将每一个Link和visibilityFilter连接起来,这样就能根据当前选中的visibilityFilter值和链接代表的分类,知道显示的是文字还是链接。

const mapStateToProps = (state, ownProps) => {
  return {
    active: ownProps.filter === state.visibilityFilter
  }
}

对于如何连接的细节,可以看看官方文档(中文 | English)和示例

更新state

如果看了Redux的官方文档(中文 | English),就能知道在 Redux 应用中,对于State的更新,是严格单向数据流。首先从Action发起,然后Store会把当前state和action两个参数传入reducer,根 reducer 把多个子 reducer 输出合并成一个新的 state 树。如图所示:

我们可以设想几个用户操作场景,然后看看在这些场景下,state是如何更新和变化的。假设我们初始state为空。

state = {
    todos: [],
    visibilityFilter: 'SHOW_ALL'
}

添加todo

用户输入任务内容"todo 1",点击Add Todo按钮,添加任务。

  1. 点击Add todo按钮后,调用addTodo,生成一个action
{
  type: 'ADD_TODO',
  id: 1,
  text: 'todo 1'
}
  1. store将这个action连同当前state按顺序传递给reducer
  2. 首先是todos这个reducer,它会接收到这个Action和state两个参数,由于它是一个子reducer,所以它的state参数只会接收根state的todos部分。当匹配到type为ADD_TODO的类型,它会生成一个新的todo对象,创建一个新的todos数组,将原有state的内容复制过去,并添加新的todo对象。
case 'ADD_TODO':
 return [
   ...state,
   todo(undefined, action)
 ]

返回新的state

todos: [{
    id: 1,
    text: 'todo 1',
    completed: false
}]
  1. 然后是visibilityFilter这个reducer,它接收Action和state两个参数,它的state参数对应的值是根state的visibilityFilter部分。没有匹配的type,所以它会返回原始的state。
visibilityFilter: 'SHOW_ALL'
  1. 根 reducer 返回的新的完整 state 树
state = {
    todos: [{
        id: 1,
        text: 'todo 1',
        completed: false
    }],
    visibilityFilter: 'SHOW_ALL'
}
  1. 由于state中的todos更新,和todos连接的TodoList组件会进行Re-render,界面刷新,新的Todo项显示在列表中。

设置任务为已完成

点击id为1的任务后,将任务状态设置为已完成。

  1. 点击id为1的任务后,调用toggleTodo,生成一个action
{
  type: 'TOGGLE_TODO',
  id: 1
}
  1. store将这个action连同当前state按顺序传递给reducer
  2. 首先是todos这个reducer,当匹配到type为TOGGLE_TODO的类型,它会根据id找到这个任务,然后复制出来一个新任务,将任务状态设置为已完成。
case 'TOGGLE_TODO':
 if (state.id !== action.id) {
   return state
 }

 return Object.assign({}, state, {
   completed: !state.completed
 })

返回新的state

todos: [{
    id: 1,
    text: 'todo 1',
    completed: true
}]
  1. 然后是visibilityFilter这个reducer,它接收Action和state两个参数,它的state参数对应的值是根state的visibilityFilter部分。没有匹配的type,所以它会返回原始的state。
visibilityFilter: 'SHOW_ALL'
  1. 根 reducer 返回的新的完整 state 树
state = {
    todos: [{
        id: 1,
        text: 'todo 1',
        completed: true
    }],
    visibilityFilter: 'SHOW_ALL'
}
  1. 由于todos中id为1的todo更新了,和这条todo连接的Todo组件会进行Re-render,显示删除线。由于todos和visibilityFilter都没有更新,所以与之连结的TodoList组件不会刷新。

选择“Completed”分组

点击底部的Completed链接,列表仅显示已完成任务。为了区分,我们假设有4条数据,其中任务1和任务3是已完成。所以当前state如下:

state = {
    todos: [{
        id: 1,
        text: 'todo 1',
        completed: true
    }, {
        id: 2,
        text: 'todo 2',
        completed: false
    }, {
        id: 3,
        text: 'todo 3',
        completed: true
    }, {
        id: 4,
        text: 'todo 4',
        completed: false
    }],
    visibilityFilter: 'SHOW_ALL'
}
  1. 点击Completed链接后,调用setVisibilityFilter,生成一个action
{
  type: 'SET_VISIBILITY_FILTER',
  filter: 'SHOW_COMPLETED'
}
  1. store将这个action连同当前state按顺序传递给reducer
  2. 首先是todos这个reducer,没有匹配到SET_VISIBILITY_FILTER,它会返回之前的todos。
  3. 然后是visibilityFilter这个reducer,它接收Action和state两个参数,它的state参数对应的值是根state的visibilityFilter部分。它匹配到type为SET_VISIBILITY_FILTER的类型,所以它会返回新的state。
visibilityFilter: 'SHOW_COMPLETED'
  1. 根 reducer 返回的新的完整 state 树
state = {
    todos: [{
        id: 1,
        text: 'todo 1',
        completed: true
    }, {
        id: 2,
        text: 'todo 2',
        completed: false
    }, {
        id: 3,
        text: 'todo 3',
        completed: true
    }, {
        id: 4,
        text: 'todo 4',
        completed: false
    }],
    visibilityFilter: 'SHOW_COMPLETED'
}
  1. 由于state中的visibilityFilter更新,和visibilityFilter连接的TodoList组件、FilterLink组件都会进行Re-render,界面刷新,分组后的Todo项显示在列表中,同时Completed由链接变成文字。

总结

随着对Redux的应用,会越来越发现,基于Redux的系统,最核心的几个要素还是:定义好state结构,组织和Component,根据需要将state和component连接起来。而这些,你都能从todo这个示例项目中学习到。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK