前端數據模型Model;適用於多人團隊協作的開發模式

前言

本文講述的數據模型並不是一個庫,也不是需要npm的包,僅僅只是一種在多人團隊協作開發的時候擬定的規則。至少目前爲止,我們的開發團隊再也沒用過mock(雖然一開始也沒用),也不用擔心後臺數據的字段或結構發生變動,真正實現前後臺並行開發的愉快模式。

本文技術棧有 Typescript、Rxjs、AngularX

定義Model

類比於java裏的類,我們的Model也是一個類,是TS的類,我們根據需求和設計圖或原型圖規劃好某一個具體的模塊的基類Model,並自行定義一些字段和枚舉類型,方法屬性等,並不需要強行和後臺的字段一致,要保證百分百純的前後端分離,舉個例子

比如開發某一個後臺管理項目,裏邊有產品(Product)模塊、用戶(User)模塊等

那麼我們會在model文件夾裏定義BaseProduct的基類

export class BaseProductModel {
    constructor() {}
    // 必有id 和 name
    public id: number = null;
    public name: string = '';
    /...more.../
}

基類的定義是必要的,可以節省很多不必要的代碼,並不需要寫一個頁面或組件就重新定義新的model,如果某一個組件裏面需要對這個產品的內容進行拓展的大可直接繼承,並不會影響其他有了這個基類的文件

我們推崇一切基類都必須繼承,不可直接構造

真實的項目中產品的字段和屬性肯定不止只有id和name,可能還包含版本、縮略圖地址、唯一標識、產品、對應規格的價格、狀態、創建時間等等;這些屬性完全可以放在基類裏,因爲所有產品都有這些屬性,說到類型和狀態的定義,請注意

絕對不能將可枚舉性質的屬性直接使用後臺或第三方返回的對應屬性

比如,產品模塊裏最基礎的狀態(status)屬性,假設後臺定義的對應狀態有

0: 禁用
1: 啓用
2: 隱藏
3: 不可購買

這四種,倘若我們在項目當中直接使用這些對應狀態的數字去判斷或進行邏輯處理,分不分的清另談,如果中途或以後狀態的數字變了,GG。可能大家覺得這樣的情況很少,但也不是沒有,一旦出現改起來BUG就一堆。

所以對於這種可枚舉性質的屬性我們會定義一個枚舉類(Enum)

export enum EStatus {
    BAN = 0,
    OPEN = 1,
    HIDE = 2,
    NOTBUY = 3
}

然後在model裏這樣

export class BaseProductModel {
    // ......
    public status: string = EStatus[1] // 默認啓用
}

美滋滋,而且在進行邏輯判斷的時候我們也不用去關心每個狀態對應的數字是什麼,我們只關心它是BAN還是OPEN,簡潔明瞭不含糊

而且我們還可以給model增加一個只讀屬性,用來返回這個狀態對應的中文提示(這種需求很常見)

public get conversionStatusHint() : string {
    const _ = { BAN: '禁用', OPEN: '啓用', HIDE: '隱藏', NOTBUY: '買不得呀' }
    return _[this.status] ? _[this.status] : ''
}

這樣就不用在每一個組件裏面寫一個方法來傳參數返回中文名稱了

到了這裏,我們的BaseProductModel已經算是定義好了,下面我們就需要給這個model定義一個方法

目的是把後臺返回的字段和數據結構轉化爲我們自己定義的字段和數據結構

轉化後臺數據

可能到了這裏很多人會覺得這是多此一舉,後臺都直接返回數據了還轉化什麼,返回什麼用什麼就得了。
但在大型的團隊開發項目當中,誰也不能保證一個字段也不修改,一個字段也不刪除或增加或缺失,牽一髮動全身。人生苦短。而且還有一種情況就是,可能這個項目是前端先進行,後臺還未介入,需要前端這邊先把整體的功能和樣式都先根據設計圖規劃開發。

export class BaseProductModel {
    // ......
    // 轉化後臺數據
    public setData( data: BaseProductModel ): void {
        if (data) {
            for (let e in this) {
                if ((<Object>data).hasOwnProperty(e)) {
                    if( e == 'status' ) {
                        this.status = EStatus[(<any>data)[e]]
                    } else {
                        this[e] = (<any>data)[e];
                    }
                }
            }
        }
    }
}

然後在調用的時候

/** 假設ProductModel類繼承了BaseProductModel類 */
public productModel: ProductModel = new ProductModel();
/...more.../
this.productModel.setData(<BaseProductModel>{
    // 假設後臺定義的創建時間字段是create_at,model裏定的創建時間是createTime
    createTime: data.create_at
});
// 即使數據結構不一致也可在這裏進行統一轉化

做好了轉化這一步,所有的數據變動和數據結構的變化都在這同一個地方修改即搞定,這個時候隨便後臺怎麼改,歡樂改,都不影響我們後續的邏輯處理和字段的變動。同理,在post數據給後臺的時候轉化就顯得容易多了,後臺需要什麼數據和字段再轉化一次不就得了。

以上的數據模型可以很好的降低前後臺掐架的概率,mock?不需要

下面是一個我們抽離出來的常用的表格數據模型基類

import { BehaviorSubject } from 'rxjs'

//分頁配置
export interface PaginationConfig {
    // 當前的頁碼
    pageIndex: number;
    // 總數
    total: number;
    // 當前選中的一頁顯示多少個的數量
    rows: number;
    // 可選擇的每頁顯示多少個數量
    rowsOptions?: Array<number>;
}

//分頁配置初始數據
export let PaginationInitConfig: PaginationConfig = {
    pageIndex: 1,
    total: 0,
    rows: 10,
    rowsOptions: [10, 20, 50]
}

//表格配置
export interface TableConfig extends PaginationConfig {
    // 是否顯示loading效果
    isLoading?: boolean;
    // 是否處於半選狀態
    isCheckIndeterminate?: boolean;
    // 是否全選狀態
    isCheckAll?: boolean;
    // 是否禁用選中
    isCheckDisable?: boolean;
    //沒有數據的提示
    noResult?: string;

}

//表頭
export interface TableHead {
    titles: string[];
    widths?: string[];
    //樣式類  src/styles/ 中有公用的表格樣式類
    classes?: string[];
    sorts?: (boolean | string)[];
}

//分頁參數
export interface PageParam {
    page: number;
    rows: number;
}

//排序類型
export type orderType = 'desc' | 'asc' | null | ''

//排序參數
export interface SortParam {
    orderBy?: string;
    order?: orderType
}

// 所有表格的基類
export class BaseTableModel<T> {
    //表格配置
    tableConfig: TableConfig
    //表格頭部配置
    tableHead: TableHead
    //表格數據流
    tableData$: BehaviorSubject<T[]>

    //排序類型
    orderType: orderType
    //當前排序的標示
    currentSortBy: string

    constructor(
        //選中的 key
        private checkKey: string = 'isChecked',
        //禁用的 key
        private disabledKey: string = 'isDisabled'
    ) {
        this.initData()
    }

    // 重置數據
    public initData(): void {
        this.tableHead = {
            titles: []
        }
        this.tableConfig = {
            pageIndex: 1,
            total: 0,
            rows: 10,
            rowsOptions: [10, 20, 50],
            isLoading: false,
            isCheckIndeterminate: false,
            isCheckAll: false,
            isCheckDisable: false,
            noResult: '暫無數據'
        }
        this.tableData$ = new BehaviorSubject([])
    }

    /**
     * 設置表格配置
     * @author GR-05
     * @param conf
     */
    setConfig(conf: TableConfig): void {
        this.tableConfig = Object.assign(this.tableConfig, conf)
    }

    /**
     * 設置表格頭部標題
     * @author GR-05
     * @param titles
     */
    setHeadTitles(titles: string[]): void {
        this.tableHead.titles = titles
    }

    /**
     * 設置表格頭部寬度
     * @author GR-05
     * @param widths
     */
    setHeadWidths(widths: string[]): void {
        this.tableHead.widths = widths
    }

    /**
     * 設置表格頭部樣式類
     * @author GR-05
     * @param classes
     */
    setHeadClasses(classes: string[]): void {
        this.tableHead.classes = classes
    }

    /**
     * 設置表格排序功能
     * @author GR-05
     * @param sorts
     */
    setHeadSorts(sorts: (boolean | string)[]): void {
        this.tableHead.sorts = sorts
    }

    /**
     * 設置當前排序類型
     * @param ot
     */
    setSortType(ot: orderType) {
        this.orderType = ot
    }

    /**
     * 設置當前排序標識
     * @param orderBy
     */
    setSortBy(orderBy: string) {
        this.currentSortBy = orderBy
    }

    /**
     * 設置當前被點擊的排序標示
     * @param i 排序數組索引
     */
    sortByClick(i: number) {
        if (this.tableHead.sorts && this.tableHead.sorts[i]) {
            if (!this.orderType) {
                this.orderType = 'desc'
            } else {
                this.orderType == 'desc' ? this.orderType = 'asc' : this.orderType = 'desc'
            }
            this.currentSortBy = this.tableHead.sorts[i] as string
        }
    }

    /**
     * 獲取當前的排序參數
     */
    getCurrentSort(): SortParam {
        return {
            order: this.orderType,
            orderBy: this.currentSortBy
        }
    }

    /**
     * 設置表格loading
     * @author GR-05
     * @param flag
     */
    setLoading(flag: boolean = true): void {
        this.tableConfig.isLoading = flag
    }

    /**
     * 設置當前表格數據總數
     * @author GR-05
     * @param total
     */
    setTotal(total: number): void {
        this.tableConfig.total = total
    }

    setPageAndRows(pageIndex: number, rows: number = 10) {
        this.tableConfig.pageIndex = pageIndex
        this.tableConfig.rows = rows
    }

    /**
     * 更新表格數據(新數據、單選、多選)
     * @author GR-05
     * @param dataList
     */
    setDataList(dataList: T[]): void {
        this.tableConfig.isCheckAll = false
        this.tableConfig.isCheckIndeterminate = dataList.filter(item => !item[this.disabledKey]).some(item => item[this.checkKey] == true)
        this.tableConfig.isCheckAll = dataList.filter(item => !item[this.disabledKey]).every(item => item[this.checkKey] == true)
        this.tableConfig.isCheckAll ? this.tableConfig.isCheckIndeterminate = false : {}
        this.tableData$.next(dataList);
        if (dataList.length == 0) {
            this.tableConfig.isCheckAll = false
        }
    }

    /**
     * 獲取已選的項
     * @author GR-05
     */
    getCheckItem(): T[] {
        return this.tableData$.value.filter(item => item[this.checkKey] == true && !item[this.disabledKey])
    }

}

我們爲什麼沒有抽離成組件而是數據模型這麼一個類上,主要是因爲,組件的樣式我們是不確定唯一性的,但數據和處理邏輯確是類似的,哪裏地方要用到,就在哪個組件裏new一個就好了;

其中BaseTableModel後面的T可以是所有你想在表格上渲染的任何一個model類,比如之前的ProductModel,頁面需求需要展示產品的表格列表,則

export class TableModel extends BaseTableModel<ProductModel> {

    constructor() {
        super();
    }

}

那麼最後你只需要將BaseTableModel裏的tableData$數據next成處理好的ProdcuModel數組就好了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章