Skip to content
master
Switch branches/tags
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
src
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

model-adapter

NPM version Build Status Coverage Status Known Vulnerabilities changelog license

npm-image

模型适配器: 后端数据与前端数据的桥梁

专注于解决前端那些老生常谈的问题(没碰到过算你赢), 如果你遇到过以下场景, 请试用一下

  • 嵌套数据: 哎呀~报错了; 哦~访问 xxx 为空了啊
  • 空数据: 咦~怎么没有头像; 哦~需要一个默认头像啊
  • 格式化数据: 诶~要显示年月日; 但返回的数据是时间戳啊

初衷

Vue 或者其他视图层框架中, 如果直接使用如下插值表达式, 当嵌套对象(通常是后端返回的数据)中的某一层级为空时就会报错 TypeError: Cannot read property 'xxx' of undefined, 造成整个组件都无法渲染.

{{a.aa.aaa}}

为了解决这种问题, 让前端的视图层能够容错增强代码的健壮性, 我们可能要写出如糖葫芦一般的防御性代码, 例如这样 {{a && a.aa && a.aa.aaa}}, 要是再多嵌套几层, 简直不忍直视啊.

舒服一些的处理方式是通过 object path get 之类的库事先处理好数据, 形成前端的视图层模型, 尽量避免嵌套数据, 再到视图层中使用, 例如

// 在视图中使用: {{aaa}}
var vm = {
    aaa: _.get('a.aa.aaa')
};

核心思路

建立一个新的模型, 通过设置默认值来补齐源数据(模型)上可能缺少的对象嵌套层次. 这样我们就能够以访问源数据一致的方式来访问新模型上的数据, 即可以理解为是对源数据的增强.

例如要访问源数据上的 a.aa.aaa, 如果源数据的 anull, 那么我们直接访问肯定是会报错的.

因此我们可以准备一份默认数据, 来补齐源数据上可能缺失的数据.

  • 当源数据上没有数据(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 时间戳
a.aa.aaa = 1566814067549 // 输入(input)
// getter 格式化
a.aa.aaa // 2019-08-26   // 输出(output)

保持输入和输出是有关联的因果关系

  • 输入 -> 输出: 因为有什么输入, 所以有什么输出, 类似函数式编程思维
  • 输入是原始值, 由输入值推导出输出, 输入是对外的唯一接口

示例

嵌套数据/空数据: 用默认值来补齐(重点是补齐嵌套对象)

import ModelAdapter from 'model-adapter';

// 这里示例由后端接口返回的数据
var ajaxData = {
    name: null,
    age: 18,
    extData: null
};

var model = new ModelAdapter(ajaxData, {
    name: 'Guest',
    extData: {
        country: {
            name: 'China'
        }
    }
});

console.log(model.name);                 // 'Guest'
console.log(model.age);                  // 18
console.log(model.extData.country.name); // 'China'

格式化数据: 变形

import ModelAdapter from 'model-adapter';

var ajaxData = {
    foo: {
        bar: {
            date: 1565001521464
        }
    }
};

var model = new ModelAdapter(ajaxData, null, {
    'foo.bar.date': {
        transformer: function(value, source) { // 变形器负责格式化数据
            return new Date(value).toISOString();
        }
    }
});

var restored = model.$restore();

console.log(model.foo.bar.date);    // '2019-08-05T10:38:41.464Z'
console.log(restored.foo.bar.date); // 1565001521464

数组: 在 transformer 中适配数组元素的模型

import ModelAdapter from 'model-adapter';

var ajaxData = {
    users: [{
        name: null,
        age: 18,
        extData: null
    }, {
        name: 'Shine',
        age: 19,
        extData: {
            country: {
                name: 'USA'
            }
        }
    }]
};

var model = new ModelAdapter(ajaxData, null, {
    users: {
        transformer: function(value) {
            return value.map(function(item) {
                return new ModelAdapter(item, {
                    name: 'Sun',
                    extData: {
                        country: {
                            name: 'China'
                        }
                    }
                });
            });
        }
    }
});

console.log(model.users[0].name);                 // 'Sun'
console.log(model.users[0].age);                  // 18
console.log(model.users[0].extData.country.name); // 'China'

console.log(model.users[1].name);                 // 'Shine'
console.log(model.users[1].age);                  // 19
console.log(model.users[1].extData.country.name); // 'USA'

先声明模型再设置源数据

import ModelAdapter from 'model-adapter';

// 声明模型(预先定义好 defaults 和 propertyAdapter)
var model = new ModelAdapter(null, {
    name: 'Guest',
    extData: {
        country: {
            name: 'China'
        }
    }
});

var ajaxData = {
    name: null,
    age: 18,
    extData: null
};
// 设置源数据
model.$setSource(ajaxData);

console.log(model.name);                 // 'Guest'
console.log(model.age);                  // 18
console.log(model.extData.country.name); // 'China'

声明模型类

import ModelAdapter from 'model-adapter';

// 声明模型类(预先定义好 defaults 和 propertyAdapter)
class User extends ModelAdapter {
    constructor(source) {
        super(source, {
            name: 'Guest',
            extData: {
                country: {
                    name: 'China'
                }
            }
        });
    }
}

var ajaxData = {
    name: null,
    age: 18,
    extData: null
};

// 使用模型类时, 只需要设置源数据
var user = new User(ajaxData);

console.log(user);                      // <User>
console.log(user.name);                 // 'Guest'
console.log(user.age);                  // 18
console.log(user.extData.country.name); // 'China'

与其他框架集成

建议的接入方式

  • 方式一: 在前端服务层中接入
  • 方式二: 在后端(Node)中间层中接入

例如

// service/user.js
export function getUser() {
    return axios('/user').then(function(response) {
        return new ModelAdapter(response.data, {
            name: 'Guest',
            extData: {
                country: {
                    name: 'China'
                }
            }
        });
    });
}

API 概览

  • 构造函数

    var model = new ModelAdapter(source, defaults, propertyAdapter);
    • source: 源数据

    • defaults: 源数据的默认值

    • propertyAdapter: 属性适配器

      结构为

      {
          propertyPath1: <adapter>,
          propertyPath2: <adapter>,
          ...
      }
      • 属性名为新模型的属性名, 用于指定要适配的属性的 path 路径
      • 属性值用于配置适配器, 支持的配置方式详见 API文档
  • 设置源数据

    model.$setSource(source);
  • 获取源数据(支持通过 propertyPath 参数安全地获取源数据)

    var source = model.$getSource(propertyPath);

    适用于你设置了 defaults, 但又需要判断原始值是否为"空"的情况

  • 新增/更新/删除属性适配器(当传入的适配器为 null 时, 删除该适配器)

    model.$setAdapter(propertyPath, adapter);
  • 还原数据(支持通过 propertyPath 参数安全地获取还原的数据)

    var restored = model.$restore(propertyPath);

    适用于你设置了 transformer, 但又需要根据原始值来进行判断的逻辑

参考

  • 「数据模型」是如何助力前端开发的

    场景

    • 在这种场景下,我们在开发中就不得不写一些防御性的代码,久而久之,项目中类似代码会越来越多,碰到层级深的,防御性代码就会写的越来越恶心。另外还有的就是,如果服务端在这中间某个字段删掉了,那就又得特殊处理了,否则会有一些未知的非空错误报错,这种编码方式会导致前端严重依赖服务端定义的数据结构,非常不利于后期维护。
    • 平时开发中,我们拿到了服务端返回的数据,有些不是标准格式的,是无法直接在视图上直接使用的,是需要额外格式化处理的,比如我司服务端返回的的价格字段单位统一是分,跟时间相关的字段统一是毫秒值,这个时候我们在组件的生命周期内,就不得不而外增加一些对数据处理的逻辑,还有就是这部分处理在很多组件都是公用的,我们就不得不频繁编写类似的代码,数据处理逻辑没有得到复用。
    • 在用户做了一些交互后,需要将一些数据存储到服务端,这个时候我们拿到的数据往往也是非标准的,就比如你要提交个表单,其中有个价格字段,你拿到价格单位可能是百位的,而服务端需要的单位必须是分位的,这个时候在提交数据之前,你又得对这部分数据进行处理,还有就是有些接口的参数是json字符串形式的,可能是多级嵌套的,你还要需要特意构造这样的参数数据格式,导致开发中编写了太多与业务无关的逻辑,随着项目逐渐扩大或者维护人员更迭,项目会越来越不好维护。

    总结

    • 前后端数据结构没有解耦,前端在应对不定的服务端数据结构前提下,需要编写过多的保护性代码,不利于维护的同时,代码健壮性也不高。
    • 基础数据逻辑处理没有和UI视图解耦,容易阻塞视图渲染,同时,在视图组件上存在太多的基础数据逻辑处理,没有有效复用。