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
(請註明來意及掘金)