前端資料模型Model,適用於多人團隊協作的開發模式

_John發表於2018-10-29

前言

本文講述的資料模型並不是一個庫,也不是需要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陣列就好了。

相關文章