资讯专栏INFORMATION COLUMN

前端数据模型Model;适用于多人团队协作的开发模式

linkFly / 1922人阅读

摘要:前言本文讲述的数据模型并不是一个库,也不是需要的包,仅仅只是一种在多人团队协作开发的时候拟定的规则。至少目前为止,我们的开发团队再也没用过虽然一开始也没用,也不用担心后台数据的字段或结构发生变动,真正实现前后台并行开发的愉快模式。

前言

本文讲述的数据模型并不是一个库,也不是需要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 ((data).hasOwnProperty(e)) {
                    if( e == "status" ) {
                        this.status = EStatus[(data)[e]]
                    } else {
                        this[e] = (data)[e];
                    }
                }
            }
        }
    }
}

然后在调用的时候

/** 假设ProductModel类继承了BaseProductModel类 */
public productModel: ProductModel = new ProductModel();
/...more.../
this.productModel.setData({
    // 假设后台定义的创建时间字段是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;
}

//分页配置初始数据
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 {
    //表格配置
    tableConfig: TableConfig
    //表格头部配置
    tableHead: TableHead
    //表格数据流
    tableData$: BehaviorSubject

    //排序类型
    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 {

    constructor() {
        super();
    }

}

那么最后你只需要将BaseTableModel里的tableData$数据next成处理好的ProdcuModel数组就好了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98817.html

相关文章

  • 利用Dawn工程化工具实践MobX数据流管理方案

    摘要:新的项目目录设计如下放置静态文件业务组件入口文件数据模型定义数据定义工具函数其中数据流实践的核心概念就是数据模型和数据储存。最后再吃我一发安利是阿里云业务运营事业部前端团队开源的前端构建和工程化工具。 本文首发于阿里云前端dawn团队专栏。 项目在最初应用 MobX 时,对较为复杂的多人协作项目的数据流管理方案没有一个优雅的解决方案,通过对MobX官方文档中针对大型可维护项目最佳实践的...

    0x584a 评论0 收藏0
  • Backbone.js学习笔记(一)

    摘要:它通过数据模型进行键值绑定及事件处理,通过模型集合器提供一套丰富的用于枚举功能,通过视图来进行事件处理及与现有的通过接口进行交互。 本人兼职前端付费技术顾问,如需帮助请加本人微信hawx1993或QQ345823102,非诚勿扰 1.为初学前端而不知道怎么做项目的你指导 2.指导并扎实你的JavaScript基础 3.帮你准备面试并提供相关指导性意见 4.为你的前端之路提供极具建设性的...

    FrancisSoung 评论0 收藏0
  • 如何设计大型网站前端 JavaScript 框架

    摘要:前端单元测试,推荐淘宝开源的工具,简单易用,支持众多测试框架,也支持调试。这些也是设计前端框架时需要权衡的重要方面。最后,其实大型网站不一定要设计自己的前端框架,完全可以选用现有的框架。 有人在知乎上提问如何设计大型网站的前端 JavaScript 框架,有不少回答,其中得赞较多的两个回答如下: 相对大型的项目在前端 JS 方面有几个需要达成的目标: 1. 代码逻辑分层 ...

    Yuanf 评论0 收藏0
  • [ 前端框架 ] 前端 MV*框架意义

    摘要:从协作关系上讲,很多前端开发团队每个成员的职责不是很清晰,有了前端的框架,这个状况会大有改观。框架的理念是把前端按照职责分层,每一层都相对比较独立,有自己的价值,也有各自发挥的余地。 简介: MV框架又是为什么兴起的呢?它的出现,伴随着一些 Web 产品逐渐往应用方向发展,遇到了在 C/S 领域相同的问题:由于前端功能的增强、代码的膨胀,导致不得不做前端的架构这个事情了。经常有人质疑...

    fxp 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<