model-adapter
模型适配器: 后端数据与前端数据的桥梁
专注于解决前端那些老生常谈的问题(没碰到过算你赢), 如果你遇到过以下场景, 请试用一下
- 嵌套数据: 哎呀~报错了; 哦~访问 xxx 为空了啊
- 空数据: 咦~怎么没有头像; 哦~需要一个默认头像啊
- 格式化数据: 诶~要显示年月日; 但返回的数据是时间戳啊
初衷
在 Vue
或者其他视图层框架中, 如果直接使用如下插值表达式, 当嵌套对象(通常是后端返回的数据)中的某一层级为空时就会报错 TypeError: Cannot read property 'xxx' of undefined
, 造成整个组件都无法渲染.
aaaaaa
为了解决这种问题, 让前端的视图层能够容错增强代码的健壮性, 我们可能要写出如糖葫芦一般的防御性代码, 例如这样 {{a && a.aa && a.aa.aaa}}
, 要是再多嵌套几层, 简直不忍直视啊.
舒服一些的处理方式是通过 object path get
之类的库事先处理好数据, 形成前端的视图层模型, 尽量避免嵌套数据, 再到视图层中使用, 例如
// 在视图中使用: {{aaa}}var vm = aaa: _;
核心思路
建立一个新的模型, 通过设置默认值来补齐源数据(模型)上可能缺少的对象嵌套层次. 这样我们就能够以访问源数据一致的方式来访问新模型上的数据, 即可以理解为是对源数据的增强.
例如要访问源数据上的 a.aa.aaa
, 如果源数据的 a
为 null
, 那么我们直接访问肯定是会报错的.
因此我们可以准备一份默认数据, 来补齐源数据上可能缺失的数据.
- 当源数据上没有数据(
undefined
或者null
)时, 模型返回默认数据上的数据 - 当源数据上有数据时, 模型返回源数据上的数据
新模型(target) 源数据(source) 默认值(default)
{ { {
a: { <─ a: null, <─ a: {
aa: { aa: {
aaa: 'default-aaa' aaa: 'default-aaa'
} }
}, },
b: 'source-b' <─ b: 'source-b' b: 'default-b',
c: 'default-c' <─ <─ c: 'default-c'
} } }
另外一种映射属性的实现思路可以参考v0.0.1版本
针对格式化数据的需求, 采取的思路为将属性改写为 setter/getter
, 以输入和输出的概念来适配新模型上的属性
setter
做为输入(input), 以源数据上的值为标准来接收数据- 例如源数据返回的字段值为时间戳, 那么我们设置属性值时, 始终设置为时间戳:
a.aa.aaa = 1566814067549
- 例如源数据返回的字段值为时间戳, 那么我们设置属性值时, 始终设置为时间戳:
getter
做为输出(output), 将源数据做转换后返回我们需要的格式- 例如将时间戳格式化为日期字符串
a.aa.aaa // 2019-08-26
- 例如将时间戳格式化为日期字符串
// setter 时间戳aaaaaa = 1566814067549 // 输入(input)// getter 格式化aaaaaa // 2019-08-26 // 输出(output)
保持输入和输出是有关联的因果关系
输入 -> 输出
: 因为有什么输入, 所以有什么输出, 类似函数式编程思维- 输入是原始值, 由输入值推导出输出, 输入是对外的唯一接口
示例
嵌套数据/空数据: 用默认值来补齐(重点是补齐嵌套对象)
; // 这里示例由后端接口返回的数据var ajaxData = name: null age: 18 extData: null; var model = ajaxData name: 'Guest' extData: country: name: 'China' ; console; // 'Guest'console; // 18console; // 'China'
格式化数据: 变形
; var ajaxData = foo: bar: date: 1565001521464 ; var model = ajaxData null 'foo.bar.date': { // 变形器负责格式化数据 return value; } ; var restored = model; console; // '2019-08-05T10:38:41.464Z'console; // 1565001521464
transformer
中适配数组元素的模型
数组: 在 ; var ajaxData = users: name: null age: 18 extData: null name: 'Shine' age: 19 extData: country: name: 'USA' ; var model = ajaxData null users: { return value; } ; console; // 'Sun'console; // 18console; // 'China' console; // 'Shine'console; // 19console; // 'USA'
先声明模型再设置源数据
; // 声明模型(预先定义好 defaults 和 propertyAdapter)var model = null name: 'Guest' extData: country: name: 'China' ; var ajaxData = name: null age: 18 extData: null;// 设置源数据model; console; // 'Guest'console; // 18console; // 'China'
声明模型类
; // 声明模型类(预先定义好 defaults 和 propertyAdapter) { supersource name: 'Guest' extData: country: name: 'China' ; } var ajaxData = name: null age: 18 extData: null; // 使用模型类时, 只需要设置源数据var user = ajaxData; console; // <User>console; // 'Guest'console; // 18console; // 'China'
与其他框架集成
建议的接入方式
- 方式一: 在前端服务层中接入
- 方式二: 在后端(
Node
)中间层中接入
例如
// service/user.js { return ;}
API 概览
-
构造函数
var model = source defaults propertyAdapter;-
source
: 源数据 -
defaults
: 源数据的默认值 -
propertyAdapter
: 属性适配器结构为
propertyPath1: <adapter>propertyPath2: <adapter>...- 属性名为新模型的属性名, 用于指定要适配的属性的 path 路径
- 属性值用于配置适配器, 支持的配置方式详见 API文档
-
-
设置源数据
model; -
获取源数据(支持通过
propertyPath
参数安全地获取源数据)var source = model;适用于你设置了
defaults
, 但又需要判断原始值是否为"空"的情况 -
新增/更新/删除属性适配器(当传入的适配器为
null
时, 删除该适配器)model; -
还原数据(支持通过
propertyPath
参数安全地获取还原的数据)var restored = model;适用于你设置了
transformer
, 但又需要根据原始值来进行判断的逻辑
参考
-
场景
- 在这种场景下,我们在开发中就不得不写一些防御性的代码,久而久之,项目中类似代码会越来越多,碰到层级深的,防御性代码就会写的越来越恶心。另外还有的就是,如果服务端在这中间某个字段删掉了,那就又得特殊处理了,否则会有一些未知的非空错误报错,这种编码方式会导致前端严重依赖服务端定义的数据结构,非常不利于后期维护。
- 平时开发中,我们拿到了服务端返回的数据,有些不是标准格式的,是无法直接在视图上直接使用的,是需要额外格式化处理的,比如我司服务端返回的的价格字段单位统一是分,跟时间相关的字段统一是毫秒值,这个时候我们在组件的生命周期内,就不得不而外增加一些对数据处理的逻辑,还有就是这部分处理在很多组件都是公用的,我们就不得不频繁编写类似的代码,数据处理逻辑没有得到复用。
- 在用户做了一些交互后,需要将一些数据存储到服务端,这个时候我们拿到的数据往往也是非标准的,就比如你要提交个表单,其中有个价格字段,你拿到价格单位可能是百位的,而服务端需要的单位必须是分位的,这个时候在提交数据之前,你又得对这部分数据进行处理,还有就是有些接口的参数是json字符串形式的,可能是多级嵌套的,你还要需要特意构造这样的参数数据格式,导致开发中编写了太多与业务无关的逻辑,随着项目逐渐扩大或者维护人员更迭,项目会越来越不好维护。
总结
- 前后端数据结构没有解耦,前端在应对不定的服务端数据结构前提下,需要编写过多的保护性代码,不利于维护的同时,代码健壮性也不高。
- 基础数据逻辑处理没有和UI视图解耦,容易阻塞视图渲染,同时,在视图组件上存在太多的基础数据逻辑处理,没有有效复用。