State设计,Redux 开发第一步

State是整个应用的数据,本质上是一个普通对象。
State决定了整个应用的组件如何渲染,渲染的结果是什么。可以说,State是应用的灵魂,组件是应用的肉体。

所以,在项目开发初期,设计一份健壮灵活的State尤其重要,对后续的开发有很大的帮助。
请注意,并不强制要求所有的数据都保存到State中,有些属于组件的数据是完全可以留给组件自身去维护的。

在设计State的过程中,对State进行拆分和改造是很有必要的,通常来说,我们可以从横向和纵向两个维度来对State进行拆分和改造。

如何横向拆分State?

通常,应用越庞大,State所包含的数据也就越多,数据结构也就越复杂。
为了便于管理这复杂的数据结构,我们通常会根据数据类别做一个横向的拆分。
就拿好奇心日报来说,先来个 不太好的示范,我们有首页列表、研究所列表:

{
    homeList: [{}, {}],
    paperList: [{}, {}]
}

接下来,试想一下:好奇心日报还有其他类型的列表,比如tag列表、category列表:

{
    homeList: [{}, {}],
    paperList: [{}, {}]
    // 其他的列表页数据
    tagList: [{}, {}],
    categoryList: [{}, {}]
}

所以我们的数据结构应该设计成这个样子吗?
这样的话,我们的State很快就会变得臃肿并且难以维护,并且下一步的Reducer设计会变得格外吃力。那我们尝试根据 数据类别 做一个横向拆分的设计:

{
    // 文章相关的数据结构
    articles: {
        // 首页列表页
        list: [{}, {}],
        // Tag列表页
        tagList: [{}, {}]
    },
    // 研究所相关的数据结构
    papers: {
        // 研究所列表页
        list: [{}, {}]
    }
}

是的,本质上就是将同一类别的数据归档:
✦ articles:state.articles,保存文章相关的数据,比如各种列表页
✦ papers:state.papers,保存研究所相关的数据,比如各种列表页

那么,这样设计的好处是什么呢?
✦ 分治法让State数据解耦,彼此间独立,我们可以对articles、papers分别管理。
✦ 数据按类别区分,同一类数据的处理逻辑(Reducer)可以放到一起,代码上更便于维护。

有人会问了,怎样才能对articles、papers分别管理呢?可以参考另外一篇博客 Reducer 最佳实践,Redux 开发最重要的部分

拆分State其实就是分治法,articles数据有一个专门的管理器,papers数据有一个专门的管理器,这样才能保证代码逻辑清晰,且彼此不关联。
数据分拆了,我们可以更细粒度的管理数据,这是横向的拆分,那纵向的问题怎么解决呢?所谓纵向,其实就是数据嵌套深的问题。

如何纵向改造State?

为什么要解决数据嵌套的问题呢?

举两个栗子你就清楚了。
✦ 场景一:好奇心日报有很多列表页,每个列表都存储了一份article/paper的详细字段,严重冗余,并且没有办法同步更新。

{
    // 文章相关的数据结构,显然id=3的文章数据冗余了一份
    articles: {
        // 首页列表页
        list: [{ id: 1, title: xxx }, { id: 3, title: xxx }],
        // Tag列表页
        tagList: [{ id: 2, title: xxx }, { id: 3, title: xxx }]
    }
}

✦ 场景二:好奇心日报的问卷的表结构复杂,想要增删改查都需要三次循环,简直是噩梦。

// 首先是paper表,包含paper的相关信息,以及一个question数组
// 其次是question表,包含question的相关信息,以及一个option数组
// 最后才是option表,包含option的相关信息。
papers: [{
   id: xxx,
    title: xxx,
    questions: [{
        id: xxx,
        title: xxx,
        sequence: xxx,
        options: [{
            id: xxx,
            title: xxx,
            sequence: xxx
        }]
    }] 
}]

现在假如我们选中某个option,需要更新option的selected=true,怎么办呢?
我们需要三层循环,找到option,然后设置selected=true。而这仅仅是一个很简单的需求而已。类似的场景有很多,我们需要纵向的遍历数据结构,不仅性能差,代码冗余,维护起来也相当困难。

那么我们的想法就是数据扁平化,所谓的数据扁平化是什么意思呢?看以下代码就清楚了:

// 扁平化之后的数据
papers: [id1, id2]
papersHash: {
    id1: {
        id: xxx,
        title: xxx,
        questions: [id1, id2]
    }
}
questionsHash: {
    id1: {
        id: xxx,
        title: xxx,
        sequence: xxx,
        options: [id1, id2]
    }
}
optionsHash: {
    id1: {
        id: xxx,
        title: xxx,
        sequence: xxx
    }
}

从上面的结构可以看到,扁平化之后,数据被抽离到一个hash对象中,根据key-value 键值对可以轻松获取指定对象的值。而关联部分只存储id作为索引。
比如前面的场景,我们想要更新某个option,很简单,直接根据id更新optionsHash中的值即可。

那么,如何才能简单的扁平化数据呢?

推荐 normalizr.js 这个库,使用方法也很简单,定义好schema,声明各个schema之间的关系,然后就OK啦。
简单给个schema的例子,更多的内容,大家可以去看官方文档。

import { Schema, arrayOf, normalize } from 'normalizr';

const paperSchema = new Schema('papers');
const questionSchema = new Schema('questions');
const optionsSchema = new Schema('options');


// 声明paper.quesitons字段,是一个questionSchema的数组
paperSchema.define({
    questions: arrayOf(questionSchema)
});
// 声明question.options字段,是一个optionsSchema的数组
questionSchema.defind({
    options: arrayOf(optionsSchema)
});


// 比如我们获取到的研究所数据是papers
let normalizeData = normalize(papers, arrayOf(paperSchema));
// 最后得到的normalizeData结果就是
// normalizeData = {
//     result: [id1, id2, ...],
//     entities: {
//         papers: {
//             id1: {}
//         },
//         questions: {
//             id1: {}
//         },
//         options: {
//             id1: {}
//         }
//     }
// }

到此为止,State的横向拆分(分治法)和纵向拆分(扁平化)都已经完成。
Redux给我们的启示是面向数据编程。所以开发之前根据实际需求设计好State的数据结构,是一个项目的灵魂。也影响和决定了后续的开发。
当然,开发初期遇到State需要调整,请大胆的调整,而产品趋于成熟,对State的调整应该越谨慎越好。

总结说点啥?

请记住,开发之前先梳理需求,然后设计State的数据结构,这个重要性就好比数据库设计之于后台。它影响和决定了后续的开发。
State本身就是一个普通对象,为了能够进行更细粒度的管理和维护,我们考虑从横向/纵向两个维度来对State进行拆分和改造。

✦ 横向设计State:把State扔到一个超级reducer中是不明智的,我们应该根据数据类别对State进行拆分,为不同类别的数据提供专门的管理器。
✦ 纵向设计State:尽量将State进行扁平化处理,这样可以更灵活的对数据进行增删改查,并且避免冗余数据的问题。

此外,State的本质就是普通对象,之所以如此“处心积虑”的设计State,无非是为了让开发速度更快,逻辑更清晰,维护更方便。
如果你的应用本身就很简单,那就不要过度设计State,甚至可以不引入Redux。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 143,809评论 1 304
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,651评论 1 257
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 95,178评论 0 213
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 41,241评论 0 181
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 49,047评论 1 259
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,899评论 1 178
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,503评论 2 274
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,249评论 0 168
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 29,125评论 6 235
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,605评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,368评论 2 215
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,723评论 1 232
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,285评论 0 32
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,190评论 2 214
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,634评论 3 209
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,651评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,052评论 0 167
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,638评论 2 232
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,760评论 2 237

推荐阅读更多精彩内容