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); // 获取所有分类Idconst { 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); // 获取所有分类Idconst { 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 {@Fieldpublic key: string;@Fieldpublic name: string;@Fieldpublic 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;@Fieldpublic id: number;@Fieldpublic catId: number;@Fieldpublic aliasName: string;@Fieldpublic uid: number;@Fieldpublic userId: number;@Fieldpublic 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-thunk
、Redux-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@observablepublic key: string;@Field@observablepublic name: string;@Field@observablepublic 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
(请注明来意及掘金)
转载于:https://juejin.im/post/5c793a10e51d4506ce5b0918
Vmo前端数据模型设计相关推荐
- Cassandra数据模型设计最佳实践
2019独角兽企业重金招聘Python工程师标准>>> 本文是Cassandra数据模型设计第一篇(全两篇),该系列文章包含了eBay使用Cassandra数据模型设计的一些实践.其 ...
- 浅析SAAS数据模型设计(Oracle)
目前SAAS平台对于大家来说并不陌生,市场上真正属于SAAS应用的并不是特别多,还有很大一部分是ASP的模式在运行,不管对于公司还是技术部门都是很大的挑战.去年在做elearning项目的时候其实也就 ...
- 前端数据可视化可绘制地图等插件:Highcharts、Echarts和D3
前端数据可视化插件有很多,但我用过的只有Highcharts(https://www.hcharts.cn/).Echarts(http://echarts.baidu.com/)和D3(https: ...
- 前端 重构时需要注意的事项_前端数据层落地实践
源宝导读:天际移动平台经过重构改版,近期正式发布了1.0版本,我们在低代码开发方面做了进一步增强.本文主要围绕前端Model.前端业务逻辑(领域模型).数据层与视图层解耦(包装器模式)3个方面,给大家 ...
- Django+Vue开发生鲜电商平台之3.数据模型设计和资源导入
文章目录 一.项目初始化 二.数据模型设计 1.用户数据模型设计 2.商品数据模型设计 3.交易数据模型设计 4.用户操作数据模型设计 三.xadmin后台管理系统的配置 四.数据迁移和数据导入 1. ...
- php多条件筛选前台功能,JavaScript前端数据多条件筛选功能实现代码
有时候也会需要在前端进行数据筛选,增强交互体验.当数据可用的筛选条件较多时,把逻辑写死会给后期维护带来很*烦.下面是我自己写的一个简单的筛选器,筛选条件可以根据数据包含的字段动态设置.本文主要为大家详 ...
- A16.从零开始前后端react+flask - 将前端数据保存到数据库
上一节,我们讲了如何将前后端联系起来. A15.从零开始前后端react+flask - 将前后端联系起来 https://blog.csdn.net/GreatXiang888/article/de ...
- 激光SLAM 前端数据预处理--剔除坏点方法总结
激光SLAM 前端数据预处理--剔除坏点方法总结 距离处理 去除掉非常近的点 Code 测试 结果分析 去除掉非常近的点 Code 测试 去除NaN的点 Code 反射率处理 Code 测试 结果 去 ...
- web前端数据表格有合并项的一种简单实现方法
今天一个同学在QQ里问,如何实现前端数据表中有合并项的效果, 在QQ里打字说不清楚,于是写了一个简单的例子说明问题, 现在记录下来,以备后用: 先写一个基本的html页面: <!DOCTYPE ...
最新文章
- jpa删除数据后数据库无修改_java – JPA不删除数据库行
- topcoder srm 635 div1
- Linux之压缩与解压缩
- mybatis当遇到,用mysql关键字作为的字段的表,如何处理
- 搭建IIS并配置网站之旅
- python巡检脚本juniper_JUNIPER设备日常维护巡检命令
- 在mac中导入hadoop2.6.0源代码至eclipse
- 洛谷 题解 P2312 【解方程】
- IDA笔记-IDA Pro基本使用
- 超好用的自带火焰图的 Java 性能分析工具 Async-profiler 了解一下
- linux系统下如何使用U盘、光盘、软盘?如何挂载U盘,光盘镜像?
- scrapy-实现下一页请求, scrapy.Request
- 《软件工程导论》课后习题答案
- 前端开发桌面终极工具(FastStone Capture)推荐(转)
- 5大领先的商业智能解决方案,国产上榜!
- python----iter\next
- 启动一个SpringBoot的maven项目
- 植被覆盖指数计算教程(ENVI)
- 什么是透明数据加密(TDE)?
- a-upload 上传文件到阿里oss
热门文章
- mysql源码学习 vc项目解决方案文件_Mysql源码学习——源码目录结构
- 简易计算机单片机编程思路,到底以什么单片机入门?一些单片机简单的学习方法...
- 服务器空闲搭建什么网站,空闲的云服务器可以干什么
- godaddy php5.ini,Godaddy主机如何开启GZIP压缩 | Godaddy美国主机中文指南
- 和远程ip_漏洞Microsoft Windows TCP/IP 远程执行代码漏洞威胁通告
- java分布式锁终极解决方案之 redisson
- RocketMQ 高级功能介绍
- python strptime_Python法律实务应用——制作自己的LPR计算器(上)
- 自从学了这套框架,自动化+性能都解决了
- 运维企业专题(1)HTTP加速器——Varnish缓存机制前篇