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

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

本文技術棧有 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)屬性,假設後臺定義的對應狀態有java

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

這四種,假若咱們在項目當中直接使用這些對應狀態的數字去判斷或進行邏輯處理,分不分的清另談,若是中途或之後狀態的數字變了,GG。可能你們以爲這樣的狀況不多,但也不是沒有,一旦出現改起來BUG就一堆。
因此對於這種可枚舉性質的屬性咱們會定義一個枚舉類(Enum)npm

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?不須要
下面是一個咱們抽離出來的經常使用的表格數據模型基類post

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,頁面需求須要展現產品的表格列表,則this

export class TableModel extends BaseTableModel<ProductModel> {

    constructor() {
        super();
    }

}

那麼最後你只須要將BaseTableModel裏的tableData$數據next成處理好的ProdcuModel數組就行了。
轉載於猿2048:☞《前端數據模型Model,適用於多人團隊協做的開發模式》

相關文章
相關標籤/搜索