Vmo前端數據模型設計

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

好比如下問題:小程序

  • 返回結果中,有字段須要單獨處理後才能使用。好比:後端可能返回的一個數組是,隔開
  • 返回結果中,有字段在某種狀況下缺失
  • 接口地址發生變更
  • 隨着業務變更,接口字段須要改動
  • 其餘組件須要使用一樣這份數據,但不能保證組件調用順序
  • 部分接口數據須要前端緩存
  • 接口存儲方式發生變化。好比:有網絡走接口,沒網絡走 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 之間存在語法差別,爲了考慮後續 H五、小程序多端迭代保持統一,須要花時間在技術建設上,抽離出公共部分,以減輕後續維護成本。

產品很是不理解問開發,若是不抽離會怎麼樣,能快點嗎?就簡單的複製過來呢?因而小明爲難之下,很是不滿的說那可能 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 (請註明來意及掘金)

相關文章
相關標籤/搜索