Redux 整合笔记 五 组织redux store

140 阅读6分钟

本章将探索两种流行的关系数据构造策略的利弊:嵌套数据和规范化数据。 随着时间推移,最佳实践开始出现,规范化数据明显胜出。

如何在redux中存储数据

redux并不关心你选择的策略。当使用简单且通用的模式来存储任务相关的数据。你拥有含有任务对象列表的reducer,以及元数据isLoading和error。这些具体的属性名不重要。

对于这种模式,reducer像是在模仿RESTful API的逻辑。

使用这样的redux store并不难,但是一旦将资源之间的关系引入,情况就会开始变得让人担忧。

如服务器响应 /projects/1 GET请求,则希望至少接收到id、项目么以及与项目关联的任务列表。

{
	id: 1,
    title: 'Short-Term Goals',
    tasks: [
    	{id: 3, title: 'Learn Redux'},
        {id: 5, title: 'Defend shuffleBoard world championship title},
    ]
}

得到数据后,下一个逻辑是把它们装入project reducer中。

{
	project: {
      id: 1,
      title: 'Short-Term Goals',
      tasks: [
          {id: 3, title: 'Learn Redux'},
          {id: 5, title: 'Defend shuffleBoard world championship title'},
      ]
    },
    isLoading: false,
    error: null,
    searchTerm: '',
}

这种模式还存在一些缺点。首先,渲染任务数据的react组件可能需要安全防范措施。引用嵌套数据要求每个父级键名都存在。

当你需要更新任务时,就需要在项目对象中进行定位。一层嵌套通常是可以管理的,但是当想要列出每个任务指派的用户时,问题只会恶化。

{
	project: {
      id: 1,
      title: 'Short-Term Goals',
      tasks: [
          {
          	id: 3, 
          	title: 'Learn Redux',
            user: {
            	id: 1,
                name: 'Richard',
            }
          },
          {
          	id: 5, 
          	title: 'Defend shuffleBoard world championship title'
          },
      ]
    },
    isLoading: false,
    error: null,
    searchTerm: '',
}

这种模式最严重的缺点是:数据重复。如果Richard被分配了三个不同的任务,那么 在数据状态中他的用户数据将出现三次。如果要将Richard更新为John,则需要在三个位置更新用户对象。这不太理想。

如果可以像在关系数据库中那样处理任务和项目,就可单独存储它们,并使用外键ID来维护关系。

规范化数据介绍

规范化也可记为是将嵌套的数据结构扁平化。在扁平的层次结构中,每个领域都接收自己的顶级状态属性。任务可可项目独立管理,而不是作为项目的子属性来管理,用对象ID来表达关系。这种数据将被认为是规范化的。

这种扁平的redux store 类似于关系数据库中的结构,每种资源类型都有一个表。项目可任务在store中都有顶级的键名,而关系由外键链接。

{
	projects: {
    	items: {
        	'1': {
            	id: 1,
                name: 'Short-Term Goals',
                tasks: [1, 3]
            },
            '2': {
            	id: 2,
                name: 'Long-Term Goals',
                tasks: [2]
            }
        },
        isLoading: false,
        error: null,
    },
    tasks: {
    	items: {
        	'1': {id:1, projectId:1, ...},
            '2': {id:2, projectId:2, ...},
            '3': {id:3, projectId:1, ...}
        },
        isLoading: false,
        error: null
    }
}

现在项目和任务存储在由ID索引的对象中,而不是存储在数组中。这使得查找变得容易。例如,要更新任务,不必循环遍历整个数组直到找到正确的任务,通过ID就能立即找到任务。

规范化数据的一些重要好处如下:

  • 减小复制。如果任务可属于多个项目呢?使用嵌套数据的话,一个对象会而多个表现。要更新单个任务,就必须找到该任务的每个单独的表现。
  • 简化更新逻辑。规范化的扁平数据意味着只需要处理一层深度,而不是挖掘整个嵌套对象。
  • 性能更高。当任务位于状态树完全独立的部分时,无须触发store中不相关部分的变更就能更新它们。

使用嵌套数据实现项目

在store使用嵌套数据结构的主要缺点之一,就是更新嵌套数据的成功有些高。下面的代码中,你将了解使用数组存储对象列表的真正缺点。

src/reducers/index.js

...
switch(action.type){
  case CREATE_TASK_SUCCEEDED: {
      const {task} = action.payload;
      const projectIndex = state.items.findIndex(
          project => project.id === task.projectId,
      );
      const project = state.items[projectIndex];
      const nextProject = {
          ...project,
          tasks: project.tasks.concat(task)
      }
      
      return {
      	...state,
        items: [
        	...state.items.slice(0, projectIndex),
            nextProject,
            ...state.items.slice(projectIndex + 1)
        ]
      }
  }
}
...

reducer 的不可变性使得redux能够实现很多伟大特性,例如时间旅行,但也会使嵌套数据结构的更新变得更加困难。必须注意要始终创建对象的新副本,而不是就地修改项目的任务数组。使用数组存储对象列表还会带来额外的问题,为了找到要处理的项目,必须遍历整个列表。

保持不可变性的方式更新嵌套数据是如此棘手,而编辑任务将变得更九复杂,需要先找到正确的项目,再找到正确的任务才能更新所有内容,同时还要避免改变任何现有数据。

src/reducers/index.js

...
switch(action.type){
  case EDIT_TASK_SUCCEEDED: {
      const {task} = action.payload;
      const projectIndex = state.items.findIndex(
          project => project.id === task.projectId,
      );
      const project = state.items[projectIndex];
      const taskIndex = project.tasks.findIndex(t => t.id === task.id);
      
      const nextProject = {
      	...project,
        tasks: [
        	...project.tasks.slice(0, tasksIndex),
          	tasks,
            ...project.tasks.slice(tasksIndex+1),
        ]
      }
      
      return {
      	...state,
        items: [
        	...state.items.slice(0, projectIndex),
            nextProject,
            ...state.items.slice(projectIndex + 1)
        ]
      }
  }
}
...

这些代码逻辑是不是太密集?所有这些只是为了更新任务!当然,一些工具如Immutable.js能让嵌套数据结构的更新更加容易。

嵌套数据的更新会导致父级数据发生更改。使更新变得更困难。另一个大缺点是性能问题。

但是我们接下来将使用规范化的状态结构将这些逻辑完全消除。

规范化项目可任务

数据结构能够保持扁平,这意味着不必费心更新嵌套资源。还意味着可通过调整react组件的连接方式来提高性能。

要实现这一点。可使用流行的normalizr软件包。只要向normalize传递嵌套的API响应数据以及用户定义的模式,它就会返回规范化的对象。

规范化的结构

{
	projects: {
    	items: {
        	'1': {
            	id: 1,
                name: 'Short-term goals',
                tasks: [1,3]
            },
            '2': {
            	id: 2,
                name: 'Short-term goals',
                tasks: [2]
            }
        },
        isLouding: false,
        error: null
    },
    tasks: {
    	items: {
        	'1': {id:1, projectId: 1, ...},
            '2': {id:2, projectId: 2, ...},
            '3': {id:3, projectId: 1, ...},
        },
        isLouding: false,
        error: null
    },
    page: {
    	currentProjectId: 1
    }
}

来看看为什么以ID为键的对象会让查找更容易

const currentProject = state.projects.items.find(project=>
	project.id === action.payload.id
)

// VS

const currentProject = state.project.items[action.payload.id];

这不仅让查找更容易,还能通过去除非必要的循环来提高性能

定义normalize模式

规范化的第一步是定义模式。模式就是在使用mormalize函数处理API响应时,告诉normalizr返回结构的方式。下面代码让mormalizr知道了有两个顶级实体 - tasks和projects,同时tasks 从属于projects

import {normalize, schema} from 'normalizr';

const taskSchema = new schema.Entity('tasks');
const projectSchema = new schema.Entity('projects', {
	tasks: [taskSchema]
})

接下来,通过normalizr的normalize函数计算来自/projects端点的API响应,该函数接收一个对象和一个模式,并返回一个规范化的对象。

const normalizedData = normalize(projects, [projectSchema])