前端业务像 MVC模型一样 做数据管理

413 阅读6分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

Js 做数据初始化模型管理 起因

vue为例,当进行表单类数据编辑和新增的时候可能会采用下面写法

{
    data(){
        return{
            info:{
                name:"",
                age:0,
                ls:[{a:1,b:2}]
            }
        }
    }
    methods:{
        async init(){
            const res = awiter api()
            this.info = res
        }
    }
}

虽然业务越来越大甚至有些表单可以复用的时候,出现重复定义,数据和框架产生较强的管理,后期 往往重构或者更换框架的时候大部分东西不能抽离,最重要的是这些数据都散落在不同的vue文件中

Js 做数据初始化模型管理 思路

经常可以听到mvc 数据模型,前端是否也可以采用这种封层来管理业务形式,如果将接口看作成数据库,将后台返回的数据对象通过类形成model层,主要存放数据库中的表字段,将其他的逻辑和业务交给前端框架,这样可以将api数据层作为抽离,方便后期维护管理复用

设计所要具备的功能

  1. 需要可以对数据进行深copy
  2. 需要可以映射字段,也就是当字段类型是数组对象时候在初始化渲染要找到对应关系举个例子,{infols:[{name:"w",age:12},{name:"w",age:12}]},那infoLs中的对象需要知道自己和那个对象形成映射关系做对应的赋值操作
  3. 需要支持特殊操作标记,有些字段只是业务需要,但实际后台是不需要的
  4. 做字段转换,经常可能会遇到前后台字段定义名字不匹配,也就是后台命名为name前端可能命名为nema但是又不想对所有问题位置进行更改需要做。初始时候字段映射让namenema形成关联相对的当给后台数据时候需要namenema进行反转

需要掌握的知识

  1. 在明确了功能和所有做的,接下来实现需要用到class ,WeakMap,装饰器 这些概念

代码实现 -- 构想的工具方法实现

/**
    @class 进行装饰器 和序列化的工具类,可以基于此类进继承扩展
 */
export default class SerializeUtils {
    // 字段映射的装饰器的存储{ 当前构造函数:{当前构造函数字段:映射的构造函数 } }
    static jsonTypeWeakMap = new WeakMap()

    // 字段重新映射{ 当前构造函数:{当前构造函数字段:映射的构造函数:{isOutput :Boolean} } }
    static jsonFieldWeakMap = new WeakMap()

    /**
     * 描述 字段类型映射的装饰器
     * @date 2021-08-18
     * @param {Object} cls 定义类s
     * @returns {Function}
     *
     * @example
     * 当数据api格式{personLs:[{name:11},{name:11}]},将数组中的对象提取出成类
     * class A {
     *   @jsonType(映射类)
     *   personLs = []
     * }
     */
    static jsonType(cls) {
        return (target, name, descriptor) => {
            // 存储格式{ 当前构造函数:{当前构造函数字段:映射的构造函数 } }
            if (!SerializeUtils.jsonTypeWeakMap.has(target.constructor)) SerializeUtils.jsonTypeWeakMap.set(target.constructor, {})
            const obj = SerializeUtils.jsonTypeWeakMap.get(target.constructor)
            obj[name] = cls
            return descriptor
        }
    }

    /**
     * 描述  字段类型重命名的装饰器
     * @date 2021-08-19
     * @param {string} rename -- 新对象对应key
     * @param {Boolean} isOutput  输出字段是否重置翻译
     * @returns {any}
     */
    static jsonField(rename, isOutput = true) {
        return (target, name, descriptor) => {
            // 检差映射名字是否已经在当前对象存在
            if (Reflect.has(target, rename)) throw new Error('Field already exists')
            // 存储格式{ 当前构造函数:{当前构造函数字段:映射的构造函数:{isOutput:Boolean} } }
            if (!SerializeUtils.jsonFieldWeakMap.has(target.constructor)) SerializeUtils.jsonFieldWeakMap.set(target.constructor, {})
            const obj = SerializeUtils.jsonFieldWeakMap.get(target.constructor)
            obj[name] = { rename, isOutput }
            return descriptor
        }
    }

    /**
     * 描述
     * @date 2021-08-20
     * @param {Object} obj 转换对象
     * @param {String} oldKey 转换对象当前key
     * @returns {Object} {key:string,isOutput:Boolean}返回对应映射key
     */
    static oldKeyToNewKey(obj, oldKey) {
        let Cls, fieldsObj
        Cls = obj.constructor
        fieldsObj = SerializeUtils.jsonFieldWeakMap.get(Cls)
        return { newKey: fieldsObj?.[oldKey]?.rename || oldKey, isOutput: fieldsObj?.[oldKey]?.isOutput ?? true }
    }

    /**
     * 描述 对象合并
     * @date 2021-08-18
     * @param {Object} target 合并的目标对象
     * @param {any} copy 被合并的对象
     * @param { Object } [deep=true] 是否深递归
     * @returns {any}
     *
     * @example
     * const a = {name:12,age:456}
     * const b = {name:1299,age:45633,zz:456}
     * SerializeUtils.objExtend(a,b)
     * a:{name:1299,age:45633}
     * b:{name:1299,age:45633,zz:456}
     */
    static objExtend(target, copy, deep = true) {
        let targetVal, copyVal, Cls, fieldsObj, rsArr, arrItem, shallowCopy, oldKey

        for (let key in target) {
            oldKey = key
            let { newKey, isOutput } = SerializeUtils.oldKeyToNewKey(target, key)
            targetVal = target[key]
            copyVal = copy[newKey]
            if (!Reflect.has(copy, newKey)) continue

            // 浅copy
            if (!deep) {
                if (typeof copyVal === 'object' && copyVal !== null) {
                    shallowCopy = Array.isArray(copyVal) ? [...copyVal] : { ...copyVal }
                } else {
                    shallowCopy = copyVal
                }
                target[key] = shallowCopy
                continue
            }

            // 深copy
            if (typeof copyVal === 'object' && copyVal !== null) {
                if (Array.isArray(copyVal)) {
                    rsArr = []
                    // 处理映射类字段 当使用jsonType映射的字段实例化自己的对应类
                    Cls = target.constructor
                    fieldsObj = SerializeUtils.jsonTypeWeakMap.get(Cls)
                    // fieldsObj && fieldsObj[key]
                    if (fieldsObj?.[key]) arrItem = new fieldsObj[key]()
                    copyVal.forEach((it, itKey) => {
                        if (!fieldsObj?.[key]) arrItem = targetVal[itKey]
                        typeof it !== 'object' || (it = SerializeUtils.objExtend(arrItem, it, deep))
                        rsArr.push(it)
                    })
                    target[key] = rsArr
                } else {
                    target[key] = SerializeUtils.objExtend(targetVal, copyVal, deep)
                }
            } else {
                target[key] = copyVal
            }
        }
        return target
    }

    /**
     * 描述 去除特殊标记字段
     * @date 2021-08-19
     * @param {Object} obj 去除对象
     * @param {string} startSymbol 特殊符号标记对象
     * @returns {any}
     *
     * @example
     * const a = {name:12,_age:456}
     * SerializeUtils.formatter(a,'_')
     * a:{name:1299}
     *
     * class A{
     *    @SerializeUtils.jsonField('newnew')
     *    obj1 = {ls:[1,3]}
     * }
     * const a = new A()
     * a:{newnew:[1,3]}
     */
    static formatter(obj, startSymbol) {
        if (obj === null) return null
        let clone = {}
        for (let key in obj) {
            if (key.startsWith(startSymbol)) continue
            let { newKey, isOutput } = SerializeUtils.oldKeyToNewKey(obj, key)
            if (!isOutput) newKey = key
            clone[newKey] = typeof obj[key] === 'object' ? SerializeUtils.formatter(obj[key], startSymbol) : obj[key]
        }
        if (Array.isArray(obj)) {
            clone.length = obj.length
            return Array.from(clone)
        }
        return clone
    }
}

创建父类让后续对象统一具备公共的方法

import SerializeUtils from './SerializeUtils'

export default class FormBase {
    init(data) {
        return SerializeUtils.objExtend(this, data)
    }

    shallowInit(data) {
        return SerializeUtils.objExtend(this, data, false)
    }

    formateObj() {
        return SerializeUtils.formatter(this, '_')
    }
}

使用

// 指定字段映射对应类

class Info {
    infoName = 'infoName'
}

class Person extends FormBase {
    name = 'json'
    @SerializeUtils.jsonType(Info)
    info = []
}

class A extends FormBase {
    name = '123'
    age = '456'
    ls = [1, 2, 3]
    ls1 = [{ name: 13 }]
    obj = new Person()

    @SerializeUtils.jsonType(Person)
    personLs = []
    obj1 = { ls: [1, 3] }
    _aaa = 'zzz'

    testDeepClone() {
        const { length } = this.personLs
        if (length) {
            this.personLs[0].name = 'clone'
        }
    }
}

// ----------测试数据------------
const api = {
    name: 'api',
    age: 'api',
    ls: [1],
    ls1: [{ name: 'api' }],
    obj: { name: 'api', age: 'api', info: [{ infoName: 'apiobj', name: 'api', age: 'api' }] },
    personLs: [
        { name: 'api', age: 'api', info: [{ infoName: 'api', name: 'api', age: 'api' }] },
        { name: 'api', age: 'api' },
    ],
}

const a = new A().init(api)
a.testDeepClone()

console.log('api', JSON.stringify(api, null, 4))

console.log('a', JSON.stringify(a, null, 4))

结合vue 来看

  • 文件结构目录

├── src
│   ├── data
│   │   └── ...
│   │       
│   └── view
│       └── ...

  • 使用 data 文件下 infoData.js, 我们根据接口给的数据格式定义了我们对应的info class,让它继承了父类FormBase,并且我们将属于这个类的一些行为处理的方法也定义其中类似下面的eat

class Info {
    infoName = 'infoName'
}

class Person extends FormBase {
    name = 'json'
    @SerializeUtils.jsonType(Info)
    info = []
    get showName(){ return 'aaa' }
    eat(){
    
    }
}
  • vue 使用代码 将来如果有复用或者app 移植的时候我们可以直接将data 文件移植出来,可以重复通过new 创建复用,并且通过类的定义和字段的映射让数据结构变得清晰。
import Info form '@/data/infoData'
{
    data(){
        return{
            info: new Info()
        }
    }
    methods:{
        async init(){
            const res = awiter api()
            // 会帮我们做对应的操作
            this.info.init(res)
        }
        
    }
}

总结

在项目逐渐越来越大,要追求的相对的是代码可维护性,欢迎大家一起讨论对类似的处理方式和解决思路

代码gitub地址

github.com/cyyspring/F…