Vmo 是一个用于前端的数据模型。解决前端接口访问混乱,服务端数据请求方式不统一,数据返回结果不一致的微型框架。javascript
Vmo 主要用于处理数据请求,数据模型管理。可配合当前主流前端框架进行数据模型管理 Vue,React,Angular。前端
可以有效处理如下问题:java
axios.get...
随处可见。Store
。Action
方法混乱,Action
中及存在同步对Store
的修改,又存在异步请求修改Store
。TypeScript
进行代码提示,只能定义 any
类型。随着现有大前端的蓬勃发展,Vue、React 等框架不断流行,RN、Weex、Electron 等使用 JS 开发客户端应用的不断发展,Taro、mpVue、CML 等新型小程序框架的不断创新。JavaScript 将变得更加流行与多样,使用 JS 同构各端项目将再也不是梦。ios
JS 的灵活在赋予你们方便的同时也一样存在着一些问题,一样实现一个数据获取到页面渲染的简单操做,可能就会有很是多的写法。正常的,在 Vue 中,可能会直接这样写:git
const methods = {
/** * 得到分类信息 */
async getBarData() {
try {
const { data } = await axios.get(url, params);
return data;
} catch (e) {
console.error("something error", e);
}
}
};
复制代码
这样的作法在功能上讲没什么问题,但在新增一些其余动做后,这样的作法就变得很是难以管理。github
好比,须要在请求中加入一些关联请求,须要获取一个商品页的列表,查询参数包含,分页参数(当前页,查询数),分类 Id,搜索内容,排序方式,筛选项。typescript
在执行该请求时,发现分类 Id 也须要另一个接口去获取。因而代码成了:json
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);
}
}
};
复制代码
这样看上去貌似是完成了这个业务,但其实在业务不断变化的环境下,这样直接在组件中书写接口请求是很是脆弱的。axios
好比如下问题:小程序
,
隔开为了让读者更容易理解我所说的痛点,我列举了几个反例场景来讲明:
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()
来对数据作适配,拿到的数据可以稳定保证是咱们所定义的那种类型。
// 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 个月完成了一款完整的 H5 SPA 应用。
业务发展的很快,又通过数十次迭代,他们的日活量很快达到了 5000,但存在 H5 的广泛痛点,用户留存率不高。
因而产品决定使用小程序重构当前项目,UI、后端接口不用改变。
小明排期却说要一样 3 个月,对此产品很是不理解,认为当初从无到有才用了 3 个月,如今简单迁移为何也须要这么久。
小明认为,虽然接口、UI 不变。但小程序与 H5 之间存在语法差别,为了考虑后续 H五、小程序多端迭代保持统一,须要花时间在技术建设上,抽离出公共部分,以减轻后续维护成本。
产品很是不理解问开发,若是不抽离会怎么样,能快点吗?就简单的复制过来呢?因而小明为难之下,很是不满的说那可能 2 周。
Deal!就这么办。
2 周开发,1 周测试,成功上线!
第 4 周,随着需求迭代,后端修改了一个接口的返回内容,先后端联动上线后发现以前的 H5 页面出现大面积白屏。
过后定位发现,因为后端修改致使 H5 数据解析出现 JS 异常。项目组一致认为是因为前段人员考虑不够全面形成的本次事故,应该由小明承担责任。
5 个月后,小明离职...
在业务场景中假设有一段接口返回的 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
在 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 完成一切到底有什么不一样呢?
请你们思考一个问题,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。做为最终的页面状态。
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
(请注明来意及掘金)