Vmo 是一個用于前端的數(shù)據(jù)模型兢交。解決前端接口訪問混亂,服務端數(shù)據(jù)請求方式不統(tǒng)一锭沟,數(shù)據(jù)返回結(jié)果不一致的微型框架抽兆。
Vmo 主要用于處理數(shù)據(jù)請求,數(shù)據(jù)模型管理族淮”韬欤可配合當前主流前端框架進行數(shù)據(jù)模型管理 Vue,React,Angular凭涂。
能夠有效處理以下問題:
- 接口請求混亂,
axios.get...
隨處可見贴妻。 - 數(shù)據(jù)管理混亂切油,請求到的數(shù)據(jù)結(jié)果用完即丟、拿到的數(shù)據(jù)直接放進
Store
名惩。 - 數(shù)據(jù)可靠性弱澎胡,不能保證請求數(shù)據(jù)是否穩(wěn)定,字段是否多娩鹉、是否少攻谁。
-
Action
方法混亂,Action
中及存在同步對Store
的修改弯予,又存在異步請求修改Store
戚宦。 - 代碼提示弱,請求到的數(shù)據(jù)無法使用
TypeScript
進行代碼提示锈嫩,只能定義any
類型受楼。 - 無效字段增多,人員變動呼寸,字段含義信息逐步丟失艳汽,新業(yè)務定義新字段。
- 項目遷移繁重对雪,項目重構(gòu)時骚灸,對字段不理解,重構(gòu)過程功能點慌植、數(shù)據(jù)丟失甚牲。
背景介紹
隨著現(xiàn)有大前端的蓬勃發(fā)展,Vue蝶柿、React 等框架不斷流行丈钙,RN、Weex交汤、Electron 等使用 JS 開發(fā)客戶端應用的不斷發(fā)展雏赦,Taro、mpVue芙扎、CML 等新型小程序框架的不斷創(chuàng)新星岗。JavaScript 將變得更加流行與多樣,使用 JS 同構(gòu)各端項目將不再是夢戒洼。
JS 的靈活在賦予大家方便的同時也同樣存在著一些問題俏橘,同樣實現(xiàn)一個數(shù)據(jù)獲取到頁面渲染的簡單操作,可能就會有非常多的寫法圈浇。正常的寥掐,在 Vue 中靴寂,可能會直接這樣寫:
const methods = {
/**
* 獲得分類信息
*/
async getBarData() {
try {
const { data } = await axios.get(url, params);
return data;
} catch (e) {
console.error("something error", e);
}
}
};
這樣的做法在功能上講沒什么問題,但在新增一些其他動作后召耘,這樣的做法就變得非常難以管理百炬。
比如,需要在請求中加入一些關(guān)聯(lián)請求污它,需要獲取一個商品頁的列表剖踊,查詢參數(shù)包含,分頁參數(shù)(當前頁衫贬,查詢數(shù))德澈,分類 Id,搜索內(nèi)容祥山,排序方式,篩選項掉伏。
在執(zhí)行該請求時缝呕,發(fā)現(xiàn)分類 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);
}
}
};
這樣看上去貌似是完成了這個業(yè)務斧散,但其實在業(yè)務不斷變化的環(huán)境下供常,這樣直接在組件中書寫接口請求是非常脆弱的。
比如以下問題:
- 返回結(jié)果中鸡捐,有字段需要單獨處理后才能使用栈暇。比如:后端可能返回的一個數(shù)組是
,
隔開 - 返回結(jié)果中,有字段在某種情況下缺失
- 接口地址發(fā)生變動
- 隨著業(yè)務變動箍镜,接口字段需要改動
- 其他組件需要使用同樣這份數(shù)據(jù)源祈,但不能保證組件調(diào)用順序
- 部分接口數(shù)據(jù)需要前端緩存
- 接口存儲方式發(fā)生變化。比如:有網(wǎng)絡走接口色迂,沒網(wǎng)絡走 LocalStorage
- 前端項目框架遷移香缺,接口不變。Vue 轉(zhuǎn) React歇僧?Vue 轉(zhuǎn)小程序图张?
為了讓讀者更容易理解我所說的痛點,我列舉了幾個反例場景來說明:
反例場景 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);
}
}
};
在這個例子中诈悍,獲取過濾項信息中返回的結(jié)果信息假設為:
{
"ids": "2,3,5234,342,412",
...
}
在數(shù)據(jù)解析中祸轮,就需要處理為前端接受的數(shù)組,類似的解析還有非常多侥钳。
也許現(xiàn)在看這段代碼無關(guān)痛癢适袜,但若每次調(diào)用這個接口都需要這樣處理,長期處理類似字段舷夺。甚至有很多開發(fā)者在一開始拿到這個字段都會暫時不去處理痪蝇,到用到的地方再處理鄙陡,每用一次處理一次。
那想想該是多么非常惡心的一件事情躏啰。
如果使用Vmo
會在數(shù)據(jù)模型開始時趁矾,就使用load()
來對數(shù)據(jù)做適配,拿到的數(shù)據(jù)能夠穩(wěn)定保證是我們所定義的那種類型给僵。
反例場景 2
// component1
// 需要使用 Goods 數(shù)據(jù)
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 數(shù)據(jù)
const mounted = async () => {
const goods = this.$store.state.goods;
this.goods = goods;
};
在這個例子中毫捣,簡單描述了兩個組件代碼(也許看上去很 low,但這種代碼確實存在)帝际,他們都會需要使用到商品數(shù)據(jù)蔓同。按照正常流程組件組件的加載流程可能是
component1
->component2
這樣的順序加載,那么上面這段是可以正常運行的蹲诀。但假若業(yè)務要求斑粱,突然有一個component3
要在兩個組件之前加載,并且也需要使用商品數(shù)據(jù)脯爪,那么對于組件的改動是非常頭疼的(因為實際業(yè)務中则北,可能你的數(shù)據(jù)加載要比這里復雜的多)。
反例場景 3
小明是一位前端開發(fā)人員痕慢,他與后端人員愉快的配合 3 個月完成了一款完整的 H5 SPA 應用尚揣。
業(yè)務發(fā)展的很快,又經(jīng)過數(shù)十次迭代掖举,他們的日活量很快達到了 5000快骗,但存在 H5 的普遍痛點,用戶留存率不高塔次。
于是產(chǎn)品決定使用小程序重構(gòu)當前項目方篮,UI、后端接口不用改變励负。
小明排期卻說要同樣 3 個月恭取,對此產(chǎn)品非常不理解,認為當初從無到有才用了 3 個月熄守,現(xiàn)在簡單遷移為什么也需要這么久蜈垮。
小明認為,雖然接口裕照、UI 不變攒发。但小程序與 H5 之間存在語法差異,為了考慮后續(xù) H5晋南、小程序多端迭代保持統(tǒng)一惠猿,需要花時間在技術(shù)建設上,抽離出公共部分负间,以減輕后續(xù)維護成本偶妖。
產(chǎn)品非常不理解問開發(fā)姜凄,如果不抽離會怎么樣,能快點嗎趾访?就簡單的復制過來呢态秧?于是小明為難之下,非常不滿的說那可能 2 周扼鞋。
Deal申鱼!就這么辦。
2 周開發(fā)云头,1 周測試捐友,成功上線!
第 4 周溃槐,隨著需求迭代匣砖,后端修改了一個接口的返回內(nèi)容,前后端聯(lián)動上線后發(fā)現(xiàn)之前的 H5 頁面出現(xiàn)大面積白屏昏滴。
事后定位發(fā)現(xiàn)猴鲫,由于后端修改導致 H5 數(shù)據(jù)解析出現(xiàn) JS 異常。項目組一致認為是由于前段人員考慮不夠全面造成的本次事故影涉,應該由小明承擔責任变隔。
5 個月后规伐,小明離職...
反例場景 4
在業(yè)務場景中假設有一段接口返回的 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 等一些接口管理系統(tǒng)定義字段。
但隨著業(yè)務發(fā)展猖闪,版本快速迭代鲜棠,人員變動等因素影響,很有可能有一天
問前端人員培慌,前端人員說這個是后端傳過來就這樣豁陆,我不清楚。
問后端人員吵护,后端人員說這個是前端這么要的盒音,我不清楚。
這上面的字段公司上下沒有一個人能夠完全描述清楚其作用馅而。
這個時候如果該接口有業(yè)務變動祥诽,需要做字段調(diào)整,為了不產(chǎn)生未知的接口事故瓮恭,很可能就說提出不改變之前的接口內(nèi)容雄坪,新增一個接口字段實現(xiàn)功能的方案。
長此以往屯蹦,接口返回越來越多维哈,直到項目組花大力氣绳姨,重寫接口,前端重寫接口對接阔挠。
閃亮登場
基礎原型
先來看一段 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];
}
/**
* 將數(shù)據(jù)適配\轉(zhuǎn)換為模型字段
* @param data
*/
protected load(data: any): this {
data.filters = data.values;
return super.load(data);
}
}
const data = {
key: "styles",
name: "風格",
values: [
{ name: "現(xiàn)代簡約", value: "1" },
{ name: "中式現(xiàn)代", value: "3" },
{ name: "歐式豪華", value: "4" }
]
};
const filterModel = new FilterModel(data); // Vmo通過load方法對數(shù)據(jù)做適配
通過以上方式就成功的將一組 json 數(shù)據(jù)實例化為一個FilterModel
的數(shù)據(jù)模型飘庄。這將會為你帶來什么好處呢?
- 適配來源數(shù)據(jù)谒亦,處理需要改變的字段類型竭宰,如
string => array
- 可靠的字段定義,即使接口字段變動份招,數(shù)據(jù)模型字段也不會變
-
TypeScript
書寫提示切揭,一路回車不用說了,爽 - 計算屬性锁摔,如
firstFilter
- 一次定義廓旬,終生受益。不認識\未使用的字段 say GoodBye
- 如果項目需要遷移谐腰、后端同構(gòu)孕豹,拿來即用。
派生能力
在 Vmo 的設計中十气,數(shù)據(jù)模型只是基類励背,你同樣可以為數(shù)據(jù)模型賦予一些 "特殊能力" ,比如數(shù)據(jù)獲取砸西。
AxiosVmo 是基于 Vmo 派生的一個使用 axios 作為 Driver(驅(qū)動器) 實現(xiàn)數(shù)據(jù)獲取叶眉、存儲能力的簡單子類。
你同樣可以封裝自己的 Driver 芹枷,通過相同接口衅疙,實現(xiàn)多態(tài)方法,來做到在不同介質(zhì)上存儲和獲取數(shù)據(jù)鸳慈。比如 IndexDB,LocalStorage饱溢。
import { AxiosVmo } from "@vmojs/axios";
import { Field, mapValue } from "@vmojs/base";
import { USER_URL } from "../constants/Urls";
import FilterModel from "./FilterModel";
// 商品查詢參數(shù)
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;
}
/**
* 將請求的數(shù)據(jù)適配轉(zhuǎn)換為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 () => {
// 通過靜態(tài)方法創(chuàng)建 GoodsModel 集合
const goods = await GoodsModel.listWithDetail({ id: 1 });
})();
像上面這樣的一個GoodsModel
中,即定義了數(shù)據(jù)模型走芋,又定義了接口地址绩郎、請求方式與適配方法。 在返回結(jié)果中會創(chuàng)建出GoodsModel
的數(shù)據(jù)模型集合翁逞。
最終打印的結(jié)果:

Action 與 Store
與以往前端思維不同肋杖,我大費周章的折騰這么一套出來。到底與原來一些常用框架思維中的 action 完成一切到底有什么不同呢熄攘?
請大家思考一個問題兽愤,action 的定義到底是什么呢?
最初 Flux 設計中, action 的設計就是為了改變 Store 中的 state浅萧,來達到狀態(tài)可控逐沙、流向明確的目的。
Redux 中的 action 甚至都是不支持異步操作的洼畅,后來有一些變相的方式實現(xiàn)異步 action吩案,后來又有了Redux-thunk
、Redux-saga
這類異步中間件實現(xiàn)帝簇。
所以徘郭,最開始 action 的設計初衷是為了管理 Store 中狀態(tài),后來因為需要丧肴,開發(fā)者們賦予了 action 異步調(diào)用接口并改變 Store 狀態(tài)的能力残揉。
所以很多項目中,看到 action 經(jīng)常會類似這樣的方法芋浮,getUsers()
調(diào)用接口獲取用戶數(shù)據(jù)抱环,addUser()
添加用戶,removeUser()
刪除用戶纸巷。
那么哪個方法會有異步請求呢镇草?哪個方法是直接操作 Store 而不會發(fā)生接口請求呢?
Vmo
希望能夠提供一種設計思路瘤旨,將數(shù)據(jù)模型梯啤、異步獲取與頁面狀態(tài) 分開管理維護。
將數(shù)據(jù)獲取存哲、適配處理因宇、關(guān)聯(lián)處理等復雜的數(shù)據(jù)操作,交給Vmo
宏胯。
將Vmo
處理后的數(shù)據(jù)模型羽嫡,交給 Store本姥。作為最終的頁面狀態(tài)肩袍。
Mobx
Vmo
還可以配合Mobx
使用,完成數(shù)據(jù)模型與數(shù)據(jù)響應結(jié)合使用婚惫。
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[];
/**
* 將數(shù)據(jù)適配\轉(zhuǎn)換為模型字段
* @param data
*/
protected load(data: any): this {
data.filters = data.values;
return super.load(data);
}
}
總結(jié)
Vmo 強調(diào)的是一種設計
通過Vmo
希望能夠幫助前端人員建立起對數(shù)據(jù)的重視氛赐,對數(shù)據(jù)模型的認知。對數(shù)據(jù)的操作處理交給Model
先舷,恢復Store
對前端狀態(tài)的設計初衷艰管。
Vmo
是我的第一個個人開源項目,凝聚了我對目前大前端數(shù)據(jù)處理的思考沉淀蒋川,源碼實現(xiàn)并不復雜牲芋,主要是想提供一種設計思路。
GitHub 中有完整的 Example,感興趣的讀者可以移步至項目地址查看缸浦。
讓各位觀眾老爺見笑了夕冲,歡迎指點討論~
個人郵箱:wyy.xb@qq.com
個人微信:wangyinye
(請注明來意及掘金)