25

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

 4 years ago
source link: https://www.tuicool.com/articles/7nUR732
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

model-adapter

zEJF3uE.png!web

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

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

  • 嵌套数据: 哎呀~报错了; 哦~访问 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')
};

核心思路

建立一个新的模型, 通过适配器(Adapter) 映射 ( path get 机制)源数据(模型)上的属性

例如

  • 新模型的 a 属性映射源数据模型中的 a 属性, 即一对一的映射属性

    target.a = source.a

  • 新模型的 nb 属性映射源数据模型中的 b 属性

    target.nb = source.b

  • 新模型的 ccc 属性映射源数据模型中嵌套的 ccc 属性(通过 c.cc.ccc 属性的 path 路径)

    target.ccc = source.c.cc.ccc

新模型(target)            源数据模型(source)
{                        {
    a: 'a',        <─        a: 1,
   nb: 'b',        <─        b: '2',
  ccc: 'c.cc.ccc'  <─┐       c: {
                     │           cc: {
                     └─              ccc: 'ccc'
                                 }
                             }
}                        }

示例

嵌套数据: 打平数据结构, 映射 path 来访问

import ModelAdapter from 'model-adapter';

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

var model = new ModelAdapter({          // name, age 属性默认一对一映射
    countryName: 'extData.country.name' // 嵌套属性映射到源数据属性的 path 路径
}, ajaxData);

console.log(model.name);        // 'Sun'
console.log(model.age);         // 18
console.log(model.countryName); // 'China'

空数据: 设置默认值

import ModelAdapter from 'model-adapter';

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

var model = new ModelAdapter({
    name: { // null 的属性值
        defaultValue: 'Guest'
    },
    sex: { // undefined 的属性值
        defaultValue: 'man'
    }
}, ajaxData);

console.log(model.name); // 'Guest'
console.log(model.age);  // 18
console.log(model.sex);  // 'man'

格式化数据: 变形和还原

import ModelAdapter from 'model-adapter';

var ajaxData = {
    date: 1565001521464
};

var model = new ModelAdapter({
    date: {
        transformer: function(value, source) { // 变形器负责格式化数据
            return new Date(value).toISOString();
        },
        restorer: function(value, model) {     // 还原器负责还原回去
            return new Date(value).getTime();
        }
    }
}, ajaxData);

var restored = model.$restore();

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

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

import ModelAdapter from 'model-adapter';

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

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

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

验证数据: 验证器(仅输出日志提示)

import ModelAdapter from 'model-adapter';

var ajaxData = {
    age: '18'
};

var model = new ModelAdapter({
    age: {
        validator: 'number'
    }
}, ajaxData);

console.log(model.age); // '18'

先声明模型再适配数据

import ModelAdapter from 'model-adapter';

// 声明模型
var model = new ModelAdapter({
    countryName: 'extData.country.name'
});

var ajaxData = {
    name: 'Sun',
    age: 18,
    extData: {
        country: {
            name: 'China'
        }
    }
};
// 适配数据
model.$adapt(ajaxData);

console.log(model.name);        // 'Sun'
console.log(model.age);         // 18
console.log(model.countryName); // 'China'

声明模型类(推荐关闭 copy 机制)

import ModelAdapter from 'model-adapter';

class User extends ModelAdapter {
    constructor(source) {
        super({
            name: 'name',
            countryName: 'extData.country.name'
        }, source, false);
    }
}

var ajaxData = {
    name: 'Sun',
    age: 18,
    extData: {
        country: {
            name: 'China'
        }
    }
};

var user = new User(ajaxData);

console.log(user);             // <User>
console.log(user.name);        // 'Sun'
console.log(user.countryName); // 'China'

与其他框架集成

建议的接入方式

Node

例如

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

API 概览

  • 构造函数

    var model = new ModelAdapter(propertyAdapter, source, copy);
    • propertyAdapter : 属性适配器

      结构为

      {
          name1: <adapter>,
          name2: <adapter>,
          ...
      }
    • source : 源数据

    • copy : 是否自动一对一映射源数据上的属性

      注意: 开启和关闭 copy 参数的区别

      • 开启 copy : 适配数据时会自动将 source 上面的所有属性一对一映射一遍(为这些属性创建 propertyAdapter ), 再追加 propertyAdapter 参数显式声明的属性

        例如

        var model = new ModelAdapter({
            countryName: 'extData.country.name'
        }, {
            name: 'Sun',
            age: 18,
            extData: {
                country: {
                    name: 'China'
                }
            }
        });
        
        // copy 来的属性
        console.log(model.name);
        console.log(model.age);
        console.log(model.extData);
        // 显式声明的属性
        console.log(model.countryName);
      • 关闭 copy : 适配数据时只会有 propertyAdapter 显式声明的属性

        例如

        var model = new ModelAdapter({
            countryName: 'extData.country.name'
        }, {
            name: 'Sun',
            age: 18,
            extData: {
                country: {
                    name: 'China'
                }
            }
        }, false);
        
        // 只有显式声明的属性
        console.log(model.countryName);
  • 适配数据

    model.$adapt(source);
  • 还原数据

    var source = model.$restore();

参考

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

    场景

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

    总结

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK