Redux是一个很是流行的状态管理解决方案,Redux应用执行过程当中的任何一个时刻,都是一个状态的反映。能够说,State 驱动了Redux逻辑的运转。设计一个好的State并不是易事,本文先从设计State时最容易犯的两个错误开始介绍,而后引出如何合理地设计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:bash
{
"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之因此返回数组结构,是考虑到数据的顺序、分页等因素。服务器
既然不能依据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,既会形成存储的浪费,又会存在数据不一致的风险。app
这两种设计State的方式其实是两种极端的设计方式,实际项目中,彻底按照这两种方式设计State的开发者并很少,但绝大部分人都会受到这两种设计方式的影响。请回忆一下,你是否有过把某个API返回的数据原封不动的做为State的一部分?又是否有过,为了组件渲染方便,专门为某个组件的UI定义一个State?post
下面咱们来看一下应该如何合理地设计State。最重要最核心的原则是像设计数据库同样设计State。把State看作一个数据库,State中的每一部分状态看作数据库中的一张表,状态中的每个字段对应表的一个字段。设计一个数据库,应该遵循如下三个原则:spa
这三个原则,能够翻译出设计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本大前端精选书籍!