Vmo前端数据模型设计

Vmo 是一个用于前端的数据模型。解决前端接口访问混乱,服务端数据请求方式不统一,数据返回结果不一致的微型框架。

Vmo 主要用于处理数据请求,数据模型管理。可配合当前主流前端框架进行数据模型管理 Vue,React,Angular。

能够有效处理以下问题:

  • 接口请求混乱,axios.get...随处可见。
  • 数据管理混乱,请求到的数据结果用完即丢、拿到的数据直接放进Store
  • 数据可靠性弱,不能保证请求数据是否稳定,字段是否多、是否少。
  • Action方法混乱,Action中及存在同步对Store的修改,又存在异步请求修改Store
  • 代码提示弱,请求到的数据无法使用TypeScript进行代码提示,只能定义 any 类型。
  • 无效字段增多,人员变动,字段含义信息逐步丢失,新业务定义新字段。
  • 项目迁移繁重,项目重构时,对字段不理解,重构过程功能点、数据丢失。

背景介绍

随着现有大前端的蓬勃发展,Vue、React 等框架不断流行,RN、Weex、Electron 等使用 JS 开发客户端应用的不断发展,Taro、mpVue、CML 等新型小程序框架的不断创新。JavaScript 将变得更加流行与多样,使用 JS 同构各端项目将不再是梦。

JS 的灵活在赋予大家方便的同时也同样存在着一些问题,同样实现一个数据获取到页面渲染的简单操作,可能就会有非常多的写法。正常的,在 Vue 中,可能会直接这样写:

const methods = {
  /**
   * 获得分类信息
   */
  async getBarData() {
    try {
      const { data } = await axios.get(url, params);

      return data;
    } catch (e) {
      console.error("something error", e);
    }
  }
};

这样的做法在功能上讲没什么问题,但在新增一些其他动作后,这样的做法就变得非常难以管理。

比如,需要在请求中加入一些关联请求,需要获取一个商品页的列表,查询参数包含,分页参数(当前页,查询数),分类 Id,搜索内容,排序方式,筛选项。

在执行该请求时,发现分类 Id 也需要另外一个接口去获取。于是代码成了:

const params = {
  sort: -1,
  search: "",
  filter: "",
  page: {
    start: 1,
    number: 10
  }
};
const methods = {
  /**
   * 获得商品列表
   */
  async getGoodsData() {
    try {
      const { data } = await axios.get(url.goodsType); // 获取所有分类Id
      const { id: typeId } = data;
      const res = await axios.get(url.goods, { ...params, typeId }); // 获取商品

      return res.data;
    } catch (e) {
      console.error("something error", e);
    }
  }
};

这样看上去貌似是完成了这个业务,但其实在业务不断变化的环境下,这样直接在组件中书写接口请求是非常脆弱的。

比如以下问题:

  • 返回结果中,有字段需要单独处理后才能使用。比如:后端可能返回的一个数组是,隔开
  • 返回结果中,有字段在某种情况下缺失
  • 接口地址发生变动
  • 随着业务变动,接口字段需要改动
  • 其他组件需要使用同样这份数据,但不能保证组件调用顺序
  • 部分接口数据需要前端缓存
  • 接口存储方式发生变化。比如:有网络走接口,没网络走 LocalStorage
  • 前端项目框架迁移,接口不变。Vue 转 React?Vue 转小程序?

为了让读者更容易理解我所说的痛点,我列举了几个反例场景来说明:

反例场景 1

const methods = {
  /**
   * 获取过滤项信息
   */
  async getFilterInfo() {
    try {
      const { data: filterInfo } = await axios.get(url.goodsType); // 获取所有分类Id
      // filterInfo.ids => "2,3,5234,342,412"
      filterInfo.ids = filterInfo.ids.map(id => id.split(","));

      return filterInfo;
    } catch (e) {
      console.error("something error", e);
    }
  }
};

在这个例子中,获取过滤项信息中返回的结果信息假设为:

{
  "ids": "2,3,5234,342,412",
  ...
}

在数据解析中,就需要处理为前端接受的数组,类似的解析还有非常多。

也许现在看这段代码无关痛痒,但若每次调用这个接口都需要这样处理,长期处理类似字段。甚至有很多开发者在一开始拿到这个字段都会暂时不去处理,到用到的地方再处理,每用一次处理一次。

那想想该是多么非常恶心的一件事情。

如果使用Vmo会在数据模型开始时,就使用load()来对数据做适配,拿到的数据能够稳定保证是我们所定义的那种类型。

反例场景 2

// component1
// 需要使用 Goods 数据

const mounted = async () => {
  const goods = await this.getGoodsData();
  this.$store.commit("saveGoods", goods); // 在store中存储

  this.goods = goods;
};

const methods = {
  /**
   * 获得商品列表
   */
  async getGoodsData() {
    try {
      const { data } = await axios.get(url.goodsType); // 获取所有分类Id
      const { id: typeId } = data;
      const res = await axios.get(url.goods, { ...params, typeId }); // 获取商品

      return res.data;
    } catch (e) {
      console.error("something error", e);
    }
  }
};
// component2
// 也需要使用 Goods 数据

const mounted = async () => {
  const goods = this.$store.state.goods;

  this.goods = goods;
};

在这个例子中,简单描述了两个组件代码(也许看上去很 low,但这种代码确实存在),他们都会需要使用到商品数据。按照正常流程组件组件的加载流程可能是

component1->component2

这样的顺序加载,那么上面这段是可以正常运行的。但假若业务要求,突然有一个component3要在两个组件之前加载,并且也需要使用商品数据,那么对于组件的改动是非常头疼的(因为实际业务中,可能你的数据加载要比这里复杂的多)。

反例场景 3

小明是一位前端开发人员,他与后端人员愉快的配合 3 个月完成了一款完整的 H5 SPA 应用。

业务发展的很快,又经过数十次迭代,他们的日活量很快达到了 5000,但存在 H5 的普遍痛点,用户留存率不高。

于是产品决定使用小程序重构当前项目,UI、后端接口不用改变。

小明排期却说要同样 3 个月,对此产品非常不理解,认为当初从无到有才用了 3 个月,现在简单迁移为什么也需要这么久。

小明认为,虽然接口、UI 不变。但小程序与 H5 之间存在语法差异,为了考虑后续 H5、小程序多端迭代保持统一,需要花时间在技术建设上,抽离出公共部分,以减轻后续维护成本。

产品非常不理解问开发,如果不抽离会怎么样,能快点吗?就简单的复制过来呢?于是小明为难之下,非常不满的说那可能 2 周。

Deal!就这么办。

2 周开发,1 周测试,成功上线!

第 4 周,随着需求迭代,后端修改了一个接口的返回内容,前后端联动上线后发现之前的 H5 页面出现大面积白屏。

事后定位发现,由于后端修改导致 H5 数据解析出现 JS 异常。项目组一致认为是由于前段人员考虑不够全面造成的本次事故,应该由小明承担责任。

5 个月后,小明离职...

反例场景 4

在业务场景中假设有一段接口返回的 Json 如下:

{
  "c": "0",
  "m": "",
  "d": {
    "bannerList": [
      {
        "bannerId": "...",
        "bannerImg": "...",
        "bannerUrl": "...",
        "backendColor": null
      }
    ],
    "itemList": [
      {
        "obsSkuId": "...",
        "obsItemId": "...",
        "categoryId": null,
        "itemName": "...",
        "mainPic": "...",
        "imgUrlList": null,
        "suggestedPriceInCent": null,
        "priceInCent": null,
        "obsBrandId": "...",
        "width": null,
        "height": null,
        "length": null,
        "bcsPattern": null,
        "commissionPercent": null,
        "buyLink": "...",
        "phoneBuyLink": false,
        "storeIdList": null,
        "storeNameList": null,
        "storeNumber": null,
        "cityIdList": null,
        "provinceIdList": null,
        "obsModelId": null,
        "desc": null,
        "shelfImmediately": null,
        "status": 1,
        "brandName": "...",
        "modelPreviewImg": null,
        "similarModelIdList": null,
        "similarModelImgList": null,
        "relatedModelId": null,
        "relatedModelImg": null,
        "brandAddress": null,
        "promotionActivityVO": null,
        "tagIds": null,
        "tagGroups": [],
        "favored": false
      }
    ],
    "newsList": [
      {
        "id": "...",
        "img": "...",
        "title": "...",
        "desc": "...",
        "date": null,
        "order": null
      }
    ],
    "activityList": [],
    "itemListOrder": 1,
    "activityOrder": 4,
    "lessonOrder": 3,
    "newsOrder": 1,
    "designerOrder": 2,
    "comboListOrder": 2
  }
}

可以看到里面有非常多的字段,虽然一些公司会尝试使用类似 Yapi 等一些接口管理系统定义字段。

但随着业务发展,版本快速迭代,人员变动等因素影响,很有可能有一天

问前端人员,前端人员说这个是后端传过来就这样,我不清楚。

问后端人员,后端人员说这个是前端这么要的,我不清楚。

这上面的字段公司上下没有一个人能够完全描述清楚其作用。

这个时候如果该接口有业务变动,需要做字段调整,为了不产生未知的接口事故,很可能就说提出不改变之前的接口内容,新增一个接口字段实现功能的方案。

长此以往,接口返回越来越多,直到项目组花大力气,重写接口,前端重写接口对接。

闪亮登场

基础原型

先来看一段 Vmo 的代码:

import { Vmo, Field } from "@vmojs/base";

interface IFilterValue {
  name: string;
  value: string;
}
export default class FilterModel extends Vmo {
  @Field
  public key: string;
  @Field
  public name: string;
  @Field
  public filters: IFilterValue[];

  public get firstFilter(): IFilterValue {
    return this.filters[0];
  }

  /**
   * 将数据适配\转换为模型字段
   * @param data
   */
  protected load(data: any): this {
    data.filters = data.values;
    return super.load(data);
  }
}

const data = {
  key: "styles",
  name: "风格",
  values: [
    { name: "现代简约", value: "1" },
    { name: "中式现代", value: "3" },
    { name: "欧式豪华", value: "4" }
  ]
};

const filterModel = new FilterModel(data); // Vmo通过load方法对数据做适配

通过以上方式就成功的将一组 json 数据实例化为一个FilterModel的数据模型。这将会为你带来什么好处呢?

  • 适配来源数据,处理需要改变的字段类型,如string => array
  • 可靠的字段定义,即使接口字段变动,数据模型字段也不会变
  • TypeScript书写提示,一路回车不用说了,爽
  • 计算属性,如firstFilter
  • 一次定义,终生受益。不认识\未使用的字段 say GoodBye
  • 如果项目需要迁移、后端同构,拿来即用。

派生能力

在 Vmo 的设计中,数据模型只是基类,你同样可以为数据模型赋予一些 "特殊能力" ,比如数据获取

AxiosVmo 是基于 Vmo 派生的一个使用 axios 作为 Driver(驱动器) 实现数据获取、存储能力的简单子类。

你同样可以封装自己的 Driver ,通过相同接口,实现多态方法,来做到在不同介质上存储和获取数据。比如 IndexDB,LocalStorage。

import { AxiosVmo } from "@vmojs/axios";
import { Field, mapValue } from "@vmojs/base";
import { USER_URL } from "../constants/Urls";
import FilterModel from "./FilterModel";

// 商品查询参数
interface IGoodsQuery {
  id: number;
  search?: string;
  filter?: any;
}

interface IGoodsCollection {
  goods: GoodsModel[];
  goodsRows: number;
  filters: FilterModel[];
}

export default class GoodsModel extends AxiosVmo {
  protected static requestUrl: string = USER_URL;

  @Field
  public id: number;
  @Field
  public catId: number;
  @Field
  public aliasName: string;
  @Field
  public uid: number;
  @Field
  public userId: number;
  @Field
  public size: { x: number; y: number };

  /**
   * 返回GoodsModel 集合
   * @param query
   */
  public static async list(query: IGoodsQuery): Promise<GoodsModel[]> {
    const { items } = await this.fetch(query);
    return items.map(item => new GoodsModel(item));
  }

  /**
   * 返回GoodsModel 集合 及附属信息
   * @param query
   */
  public static async listWithDetail(
    query: IGoodsQuery
  ): Promise<IGoodsCollection> {
    const { items, allRows, aggr } = await this.fetch(query);
    const goods = items.map(item => new GoodsModel(item));
    const filters = aggr.map(item => new FilterModel(item));
    return { goods, goodsRows: allRows, filters };
  }

  public static async fetch(query: IGoodsQuery): Promise<any> {
    const result = await this.driver.get(this.requestUrl, query);
    return result;
  }

  /**
   * 将请求的数据适配转换为Model
   * @param data
   */
  protected load(data: any): this {
    data.catId = data.cat_id;
    data.aliasName = data.aliasname;
    data.userId = data.user_id;

    return super.load(data);
  }
}

(async () => {
  // 通过静态方法创建 GoodsModel 集合
  const goods = await GoodsModel.listWithDetail({ id: 1 });
})();

像上面这样的一个GoodsModel中,即定义了数据模型,又定义了接口地址、请求方式与适配方法。 在返回结果中会创建出GoodsModel的数据模型集合。

最终打印的结果:

Action 与 Store

与以往前端思维不同,我大费周章的折腾这么一套出来。到底与原来一些常用框架思维中的 action 完成一切到底有什么不同呢?

请大家思考一个问题,action 的定义到底是什么呢?

最初 Flux 设计中, action 的设计就是为了改变 Store 中的 state,来达到状态可控、流向明确的目的。

Redux 中的 action 甚至都是不支持异步操作的,后来有一些变相的方式实现异步 action,后来又有了Redux-thunkRedux-saga这类异步中间件实现。

所以,最开始 action 的设计初衷是为了管理 Store 中状态,后来因为需要,开发者们赋予了 action 异步调用接口并改变 Store 状态的能力。

所以很多项目中,看到 action 经常会类似这样的方法,getUsers()调用接口获取用户数据,addUser()添加用户,removeUser()删除用户。

那么哪个方法会有异步请求呢?哪个方法是直接操作 Store 而不会发生接口请求呢?

Vmo 希望能够提供一种设计思路,将数据模型、异步获取与页面状态 分开管理维护。

将数据获取、适配处理、关联处理等复杂的数据操作,交给Vmo

Vmo处理后的数据模型,交给 Store。作为最终的页面状态。

Mobx

Vmo还可以配合Mobx使用,完成数据模型与数据响应结合使用。

import { Vmo, Field } from "@vmojs/base";
import { observable } from "mobx";

interface IFilterValue {
  name: string;
  value: string;
}
export default class FilterModel extends Vmo {
  @Field
  @observable
  public key: string;
  @Field
  @observable
  public name: string;
  @Field
  @observable
  public filters: IFilterValue[];

  /**
   * 将数据适配\转换为模型字段
   * @param data
   */
  protected load(data: any): this {
    data.filters = data.values;
    return super.load(data);
  }
}

总结

Vmo 强调的是一种设计

通过Vmo希望能够帮助前端人员建立起对数据的重视,对数据模型的认知。对数据的操作处理交给Model,恢复Store对前端状态的设计初衷。

Vmo 是我的第一个个人开源项目,凝聚了我对目前大前端数据处理的思考沉淀,源码实现并不复杂,主要是想提供一种设计思路。

GitHub 中有完整的 Example,感兴趣的读者可以移步至项目地址查看。

项目地址

让各位观众老爷见笑了,欢迎指点讨论~

个人邮箱:wyy.xb@qq.com

个人微信:wangyinye (请注明来意及掘金)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,869评论 6 13
  • ## 框架和库的区别?> 框架(framework):一套完整的软件设计架构和**解决方案**。> > 库(lib...
    Rui_bdad阅读 2,890评论 1 4
  • 本文首发于:CSDN「前端开发者说」公众号。CSDN「前端开发者说」公众号(ID:bigfrontend),专注前...
    RachelQG阅读 4,808评论 2 20
  • 一:什么是闭包?闭包的用处? (1)闭包就是能够读取其他函数内部变量的函数。在本质上,闭包就 是将函数内部和函数外...
    xuguibin阅读 9,523评论 1 52
  • 下班的时候,电梯间里,人不多,不像平时人多拥挤,我低着头看着手机。 耳边传来两个姑娘的对话。 其中一个说,我今天去...
    顾紫阅读 3,386评论 0 4