资讯专栏INFORMATION COLUMN

Redux进阶系列2: 如何合理地设计Redux的State

刘明 / 2670人阅读

摘要:设计一个好的并非易事,本文先从设计时最容易犯的两个错误开始介绍,然后引出如何合理地设计。错误以为设计的依据以为设计的依据,往往是一个对应一个子,的结构同返回的数据结构保持一致或接近一致。至此,的结构设计完成。

Redux是一个非常流行的状态管理解决方案,Redux应用执行过程中的任何一个时刻,都是一个状态的反映。可以说,State 驱动了Redux逻辑的运转。设计一个好的State并非易事,本文先从设计State时最容易犯的两个错误开始介绍,然后引出如何合理地设计State。

错误1:以API为设计State的依据

以API为设计State的依据,往往是一个API对应一个子State,State的结构同API返回的数据结构保持一致(或接近一致)。例如,一个博客应用,/posts接口返回博客列表,返回的数据结构如下:

[
  {
    "id": 1,
    "title": "Blog Title",
    "create_time": "2017-01-10T23:07:43.248Z",
    "author": {
      "id": 81,
      "name": "Mr Shelby"
    }
  }
  ...
]

我们还需要查看一篇博客的详情,假设通过接口/posts/{id}获取博客详情,通过接口/posts/{id}/comments获取博客的评论,返回的数据结构如下:

{
    "id": 1,
    "title": "Blog Title",
    "create_time": "2017-01-10T23:07:43.248Z",
    "author": {
      "id": 81,
      "name": "Mr Shelby"
    },
    "content": "Some really short blog content. "
}
[
  {
    "id": 41,
    "author": "Jack",
    "create_time": "2017-01-11T23:07:43.248Z",
    "content": "Good article!"
  }
  ...
]

上面三个接口的数据分别作为3个子State,构成应用全局的State:

{
  "posts": [
    {
      "id": 1,
      "title": "Blog Title",
      "create_time": "2017-01-10T23:07:43.248Z",
      "author": {
        "id": 81,
        "name": "Mr Shelby"
      }
    },
    ...
  ],
  "currentPost": {
    "id": 1,
    "title": "Blog Title",
    "create_time": "2017-01-10T23:07:43.248Z",
    "author": {
      "id": 81,
      "name": "Mr Shelby"
    },
    "content": "Some really short blog content. "
  },
  "currentComments": [
    {
      "id": 1,
      "author": "Jack",
      "create_time": "2017-01-11T23:07:43.248Z",
      "content": "Good article!"
    },
    ...
  ]
}

这个State中,posts和currentPost存在很多重复的信息,而且posts、currentComments是数组类型的结构,不便于查找,每次查找某条记录时,都需要遍历整个数组。这些问题本质上是因为API是基于服务端逻辑设计的,而不是基于应用的状态设计的。比如,虽然获取博客列表时,已经获取了每篇博客的标题、作者等基本信息,但对于获取博客详情的API来说,根据API的设计原则,这个API依然应该包含博客的这些基本信息,而不能只是返回博客的内容。再比如,posts、currentComments之所以返回数组结构,是考虑到数据的顺序、分页等因素。

错误2:以页面UI为设计State的依据

既然不能依据API设计State,很多人又会走到另外一个反面,基于页面UI设计State。页面UI需要什么样的数据和数据格式,State就设计成什么样。我们以todo应用为例,页面会有三种状态:显示所有的事项,显然所有的已办事项和显示所有的待办事项。以页面UI为设计State的依据,那么State将是这样的:

{
  "all": [
    {
      "id": 1,
      "text": "todo 1",
      "completed": false
    },
    {
      "id": 2,
      "text": "todo 2",
      "completed": true
    }
  ],
  "uncompleted": [
    {
      "id": 1,
      "text": "todo 1",
      "completed": false
    }
  ],
  "completed": [
    {
      "id": 2,
      "text": "todo 2",
      "completed": false
    }
  ]
}

这个State对于展示UI的组件来说,使用起来非常方便,当前应用处于哪种状态,就用对应状态的数组类型的数据渲染UI,不用做任何的中间数据转换。但这种State存在的问题也很容易被发现,一是这种State依然存在数据重复的问题;二是当新增或修改一条记录时,需要修改不止一个地方。例如,当新增一条记录时,all和uncompleted这两个数组都要添加这条新增记录。这种类型的State,既会造成存储的浪费,又会存在数据不一致的风险。

这两种设计State的方式实际上是两种极端的设计方式,实际项目中,完全按照这两种方式设计State的开发者并不多,但绝大部分人都会受到这两种设计方式的影响。请回忆一下,你是否有过把某个API返回的数据原封不动的作为State的一部分?又是否有过,为了组件渲染方便,专门为某个组件的UI定义一个State?

合理设计State

下面我们来看一下应该如何合理地设计State。最重要最核心的原则是像设计数据库一样设计State。把State看做一个数据库,State中的每一部分状态看做数据库中的一张表,状态中的每一个字段对应表的一个字段。设计一个数据库,应该遵循以下三个原则:

数据按照领域(Domain)分类,存储在不同的表中,不同的表中存储的列数据不能重复。

表中每一列的数据都依赖于这张表的主键。

表中除了主键以外的其他列,互相之间不能有直接依赖关系。

这三个原则,可以翻译出设计State时的原则:

把整个应用的状态按照领域(Domain)分成若干子State,子State之间不能保存重复的数据。

State以键值对的结构存储数据,以记录的key/ID作为记录的索引,记录中的其他字段都依赖于索引。

State中不能保存可以通过已有数据计算而来的数据,即State中的字段不互相依赖。

按照这三个原则,我们重新设计博客应用的State。按领域划分,State可以拆分为三个子State: posts、comments、authors,posts中的记录以博客的id为key值,包含title、create_time、author、comments,同样的方式可以设计出comments、authors的结构,最终State的结构如下:

{
  "posts": {
    "1": {
      "id": 1,
      "title": "Blog Title",
      "content": "Some really short blog content.",
      "created_at": "2016-01-11T23:07:43.248Z",
      "author": 81,
      "comments": [
        352
      ]
    },
    ...
  },
  "comments": {
    "352": {
      "id": 352,
      "content": "Good article!",
      "author": 41
    },
    ...
  },
  "authors": {
    "41": {
      "id": 41,
      "name": "Jack"
    },
    "81": {
      "id": 81,
      "name": "Mr Shelby"
    },
    ...
  }
}

现在这个State看起来是不是很像有三张表的数据库呢?但这个State还有不满足应用需求的地方:键值对的存储方式无法保证博客列表数据的顺序,但对于博客列表,有序性显然是需要的。解决这个问题,我们可以通过定义另外一个状态postIds,以数组格式存储博客的id:

{
  "posts": {
    "1": {
      "id": 1,
      "title": "Blog Title",
      "content": "Some really short blog content.",
      "created_at": "2016-01-11T23:07:43.248Z",
      "author": 81,
      "comments": [
        352
      ]
    },
    ...
  },
  "postIds": [1, ...],
  "comments": {
    "352": {
      "id": 352,
      "content": "Good article!",
      "author": 41
    },
    ...
  },
  "authors": {
    "41": {
      "id": 41,
      "name": "Jack"
    },
    "81": {
      "id": 81,
      "name": "Mr Shelby"
    },
    ...
  }
}

这样,当显示博客列表时,根据postIds获取列表顺序,然后根据博客id从posts中获取博客的信息。这个地方有些同学可能有疑惑,认为posts和postIds都保存了id数据,违反了不同State间不能有重复数据的原则。但其实这并不是重复数据,postIds保存的数据是博客列表的顺序,只不过“顺序”这个数据是通过博客id来体现的。这和一张表的主键同时可以用作另外一张表的外键,是同样的道理。同样需要注意的是,当新增加一条博客时,posts和postId这两个状态都要进行修改。这看似变得麻烦,不如直接使用一个数组类型的状态操作简单,但是当需要修改某一篇博客的数据时,这种结构就有了明显的优势,而且直接使用数组保存状态,会存在对象嵌套层级过深的问题,想象下访问评论的内容,需要通过类似posts[0].comments[0].content三层结构才能获取到,当业务越复杂,这个问题越突出。扁平化的State,才具有更好的灵活性和扩展性。

截至目前为止,我们的State都是根据后台API返回的领域数据进行设计的,但实际上,应用的State,不仅包含领域数据,还需要包含应用的UI逻辑数据,例如根据当前是否正在与服务器通信,处理页面的加载效果;当应用运行出错时,需要显示错误信息等。这时,State的结构如下:

{
  "isFetching": false,
  "error": "",
  "posts": {
    ...
  },
  "postIds": [1, ...],
  "comments": {
    ...
  },
  "authors": {
    ...
  }
}

随着应用业务逻辑的增加,State的第一层级的节点也会变得越来越多。这时候我们往往会考虑合并关联性较强的节点数据,然后通过拆分reducer的方式,让每一个子reducer处理一个节点的状态逻辑。这个例子中,我们可以把posts、postIds进行合并,同时状态名做了调整,把isFetching、error作为全局的UI逻辑状态合并:

{
  "app":{
    "isFetching": false,
      "error": "",
  },
  "posts":{
    "byId": {
      "1": {
        ...
      },
      ...
    },
    "allIds": [1, ...],
  } 
  "comments": {
    ...
  },
  "authors": {
    ...
  }
}

这样,我们就可以定义appReducer、postsReducer、commentsReducer、authorsReducer四个reducer分别处理4个子状态。至此,State的结构设计完成。

总结一下,设计Redux State的关键在于,像设计数据库一样设计State。把State看作应用在内存中的一个数据库,action、reducer等看作操作这个数据库的SQL语句。

欢迎关注我的公众号:老干部的大前端,领取21本大前端精选书籍!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/84943.html

相关文章

  • 前端进阶资源整理

    摘要:前端进阶进阶构建项目一配置最佳实践状态管理之痛点分析与改良开发中所谓状态浅析从时间旅行的乌托邦,看状态管理的设计误区使用更好地处理数据爱彼迎房源详情页中的性能优化从零开始,在中构建时间旅行式调试用轻松管理复杂状态如何把业务逻辑这个故事讲好和 前端进阶 webpack webpack进阶构建项目(一) Webpack 4 配置最佳实践 react Redux状态管理之痛点、分析与...

    BlackMass 评论0 收藏0
  • Redux 进阶

    摘要:系列文章入门进阶本文番外篇在之前的文章中,我们已经了解了到底是什么,用来处理什么样的问题,并创建了一个简单的。启动应用之后,就能在控制台中看到一下的输出。现在,如果你刷新界面就应该能看到控制台中已经输出了为和的。 系列文章: Redux 入门 Redux 进阶(本文) 番外篇: Vuex — The core of Vue application 在之前的文章中,我们已经了解了...

    zone 评论0 收藏0
  • 【React进阶系列】手写实现react-redux api

    简介:简单实现react-redux基础api react-redux api回顾 把store放在context里,所有子组件可以直接拿到store数据 使组件层级中的 connect() 方法都能够获得 Redux store 根组件应该嵌套在 中 ReactDOM.render( , rootEl ) ReactDOM.render( ...

    刘玉平 评论0 收藏0
  • Redux 入门

    摘要:系列文章入门本文进阶番外篇状态管理,第一次听到这个词要追溯到去年年底。只读的唯一改变的方法就是触发,是一个用于描述已发生事件的普通对象。没有特殊情况没有副作用,没有请求没有变量修改,只进行单纯执行计算。 系列文章: Redux 入门(本文) Redux 进阶 番外篇: Vuex — The core of Vue application 状态管理,第一次听到这个词要追溯到去年年...

    shusen 评论0 收藏0
  • Rematch: Redux 重新设计

    摘要:沿着管道有两组侦听器中间件和订阅。中间件是可以侦听传入的动作的函数,支持诸如,或侦听器之类的工具。将视为一个带有更新前更新后钩子的全局对象,以及能够以简单的方式合成新状态。应将两者视为一体,并且不再需要文件导出类型的字符串。 难道现在状态管理不是一个可以解决的问题吗?直观地说,开发人员似乎知道一个隐藏的事实:状态管理的使用似乎比需要的更困难。在本文中,我们将探讨一些你可能一直在问自己的...

    Taste 评论0 收藏0

发表评论

0条评论

刘明

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<