前端数据范式化

前端数据范式化

前言

范式化(Normalization)是数据库设计中的一系列原理和技术,以减少数据库中数据冗余,增进数据的一致性。大部分学高校在本科数据库课程中就介绍了范式、反范式、一范式(1NF)、二范式(2NF)、三范式(3NF)、BC范式(BCNF)等概念。按照教材中的定义,范式是“符合某一种级别的关系模式的集合,表示一个关系内部各属性之间的联系的合理化程度”。我们用更直观的语言解释就是寻找对象之间的关系,通过某种方式将关系之间进行映射,减少数据之间的冗余,优化增删改查操作。


随着前端技术的发展,越来越多的业务逻辑逐渐从后端前移到前端,数据和逻辑越来越复杂。除此之外,前端技术的复杂还体现在要将业务逻辑与用户视图交互进行结合,就像俄罗斯国徽的双头鹰一样,一面望向交互和视图逻辑,一面望向业务领域逻辑逻辑。前端场景下需要的数据也越来越复杂和多样化,在我们日常开发中最明显对的体验就是数据字段变多、层次结构越来越深。

目前,越来越多的前端团队在数据层上进行探索与实践。比如根据不同业务场景下抽象中间层封装后端服务提供数据的BFF模型。还有通过GraphQL技术,基于图模式定义你的后端。然后客户端就可以请求所需要的数据集,并可以对数据进行灵活的聚合与映射。但是这两种模型都需要对后端系统进行一定程度的改造,有一定的改造成本。今天介绍一个Normalizr(paularmstrong/normalizr)的库,可以方便的在前端对数据进行范式化处理,压平数据层次,更方便灵活的处理和操作数据。

Normalizr介绍

先看一个官方的例子:

比如我们通过API接口获取了一个博客详情的数据,类似这样的

{
  "id": "123",
  "author": {
    "id": "1",
    "name": "Paul"
  },
  "title": "My awesome blog post",
  "comments": [
    {
      "id": "324",
      "commenter": {
        "id": "2",
        "name": "Nicole"
      }
    }
  ]
}

我们粗略的看到有三个原子对象author、comment和commenter复合形成了博文这个对象。其中一般情况下author和commenter都是user这个的表达。当我们需要获取深层次数据是要通过上层一层层的遍历,如果使用Normalizr。我们可以这样定义数据Schema

import { schema } from 'normalizr';  

const user = new schema.Entity('users');

const comment = new schema.Entity('comments', {
  commenter: user
});

const article = new schema.Entity('articles', {
  author: user,
  comments: [comment]
});

最后对数据进行范式化转换

const normalizedData = normalize(input, article);

得到结果结构如下面展示的这样,把杂糅在一起的对象分开,变的更加清晰。

{
  "entities": {
    "users": {
      "1": {
        "id": "1",
        "name": "Paul"
      },
      "2": {
        "id": "2",
        "name": "Nicole"
      }
    },
    "comments": {
      "324": {
        "id": "324",
        "commenter": "2"
      }
    },
    "articles": {
      "123": {
        "id": "123",
        "author": "1",
        "title": "My awesome blog post",
        "comments": ["324"]
      }
    }
  },
  "result": "123"
}

再比如,在一些场景下从后端返回的数据是深度嵌套有父子关系的结构,如下所示

{
  "id": 1,
  "name": "Andy Warhol",
  "parent": {
    "id": 7,
    "name": "Tom Dale",
    "parent": {
      "id": 4,
      "name": "Pete Hunt"
    }
  }
}

我么可以通过定义一个schema将深度嵌套的关系抹平,如果我们需要遍历的时候就不用手动写递归对树形结构进行递归遍历了

var user = schema.Entity('users')

user.define({
  parent: user
});

通过范式化之后的返回结果为

{
  "entities": {
    "users": {
      "1": {
        "id": 1,
        "name": "Andy Warhol",
        "parent": 7
      },
      "4": {
        "id": 4,
        "name": "Pete Hunt"
      },
      "7": {
        "id": 7,
        "name": "Tom Dale",
        "parent": 4
      }
    }
  },
  "result": 1
}

在这里你已经能感受到Normalizr的便利之处了吧,不仅如此扁平化的数据也可以优化代码。比如在React的的PropType定义的时候嵌套定义会使得结构变得难看。假如我们有这样的我们一组仓储配送数据。

[{
    "id": "CABA",
    "area": "Ciudad Autónoma de Buenos Aires",
    "stores": [{
        "name": "ABASTO",
        "sortName": "AB"
    }, {
        "name": "MICRO-CENTRO",
        "sortName": "MC"
    }, {
        "name": "CABALLITO",
        "sortName": "CB"
    }, {
        "name": "BELGRANO",
        "sortName": "BE"
    }]
}, {
    "id": "BSAS",
    "area": "Provincia de Buenos Aires",
    "stores": [{
        "name": "RAMOS MEJIA",
        "sortName": "RM"
    }, {
        "name": "LANUS",
        "sortName": "LN"
    }, {
        "name": "LA PLATA",
        "sortName": "LP"
    }, {
        "name": "VICENTE LOPEZ",
        "sortName": "VL"
    }]
}, {
    "id": "Interior",
    "area": "Interior del País",
    "stores": [{
        "name": "ROSARIO",
        "sortName": "RO"
    }, {
        "name": "CORDOBA",
        "sortName": "CO"
    }]
}]

常规情况我们定义的propTypes会是这个样子的

SomeComponent.propTypes = {
  areas: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.string,
    name: PropTypes.string,
    stores: PropTypes.arrayOf(PropTypes.shape({
      name: PropTypes.string,
      sortName: PropTypes.string
    }))
  }))
}

可以定义这样的schema结构

const store = new schema.Entity('store', undefined, { idAttribute: 'sortName' });
const area = new schema.Entity('area', {
  stores: [store]
});

最终范式化之后的数据结构中没有两层数组的嵌套。

{
  "entities": {
    "store": {
      "AB": {
        "name": "ABASTO",
        "abbreviatedName": "AB"
      },
      "MC": {
        "name": "MICRO-CENTRO",
        "abbreviatedName": "MC"
      },
      "CB": {
        "name": "CABALLITO",
        "abbreviatedName": "CB"
      },
      "BE": {
        "name": "BELGRANO",
        "abbreviatedName": "BE"
      },
      "RM": {
        "name": "RAMOS MEJIA",
        "abbreviatedName": "RM"
      },
      "LN": {
        "name": "LANUS",
        "abbreviatedName": "LN"
      },
      "LP": {
        "name": "LA PLATA",
        "abbreviatedName": "LP"
      },
      "VL": {
        "name": "VICENTE LOPEZ",
        "abbreviatedName": "VL"
      },
      "RO": {
        "name": "ROSARIO",
        "abbreviatedName": "RO"
      },
      "CO": {
        "name": "CORDOBA",
        "abbreviatedName": "CO"
      }
    },
    "area": {
      "CABA": {
        "id": "CABA",
        "area": "Ciudad Autónoma de Buenos Aires",
        "stores": ["AB", "MC", "CB", "BE"]
      },
      "BSAS": {
        "id": "BSAS",
        "area": "Provincia de Buenos Aires",
        "stores": ["RM", "LN", "LP", "VL"]
      },
      "Interior": {
        "id": "Interior",
        "area": "Interior del País",
        "stores": ["RO", "CO"]
      }
    }
  },
  "result": ["CABA", "BSAS", "Interior"]
}

这样我们的PropTypes就可以写成

SomeComponent.propTypes = {
  areaNames: PropTypes.arrayOf(PropTypes.string),
  areas: PropTypes.objectOf(PropTypes.shape({
    id: PropTypes.string,
    name: PropTypes.string,
    stores: PropTypes.arrayOf(PropTypes.string)
  })),
  stores: PropTypes.objectOf(PropTypes.shape({
    name: PropTypes.string,
    abbreviatedName: PropTypes.string
  }))
}

这样PropTypes的定义从3层变为了1层,shape定义还能进行一定程度上的复用。

这里只是抛砖引玉,范式化的数据结构在和Immutable.js在一起也能碰撞出奇妙的化学反应,可以降低复杂的不可变数据更新时候的操作成本。并且也可以为Redux等状态管理工具提供结构化的数据源。

实际上在和GraphQL技术深度组合的Apollo框架中也融合了Normalizaion的概念,只不过把需要手动进行范式化的操作进行自动化了。

总结

使用Normalizr对API返回的数据进行处理,可以将数据层次进行压平。在React技术使用场景中可以简化Prop-type的校验。

但是前端数据范式化操作并不是必须的也并不是银弹,要根据实际业务场景进行选择。首先,在简单场景下定义Schema也许投入并不小于产出。其次,如果后端接口有良好的领域设计、数据库设计和合理的API接口设计,前端数据操作也许并不是瓶颈。最后,在有条件的情况下使用GraphQL等数据聚合工具可能是一种更为灵活高效选择。


参考资料

React PropTypes validation when using normalizr

编辑于 2018-05-07 09:42