npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

model-adapter

v0.0.2

Published

model adapter

Downloads

8

Readme

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视图解耦,容易阻塞视图渲染,同时,在视图组件上存在太多的基础数据逻辑处理,没有有效复用。