查看原文
其他

用代码聊聊我们跟目前主流前端编程不一样的地方

作者:Hamm

https://juejin.cn/post/7272372568623710264

写在前面

也许跟大部分的前端开发者不同,我们使用了 Vue3TypeScript, 但我们也许是又回到了 老古董 的编程方式中, 也许是习惯了 面向对象(OOP) ,又或者是跑了一圈, 相比现在的 JavaScriptPythonPHPGoC# 等,依然还是钟爱 Java, 我们不否认现在的主流在 函数式编程(FP) 上, 也越来越少的开发者喜欢在前端也这么抽象的使用 面向对象编程, 这篇文章只表达我们自己的喜好, 不强加任何观点。

很多人问为什么我们在Vue上这么搞,这里总结几个原因:招聘的成本重庆这个N线互联网城市的现状公司的决策 等等。

我们如何使用面向对象

我们在前端也引入了大量的面向对象的影子,包含了一些 数据交互实体相似API的封装 等:)

什么 Service / Entity 是不是像极了 Java 写后端时候的样子?

  • 数据实体的封装

数据实体作为前后端交互的主要数据对象,承载着前端和后端的数据交互、组件之间的数据交互等重要步骤。

如后端编程一样,我们将先按照指定的规范对数据结构进行约束,这里我们没有使用目前大家都喜欢的一些方式进行封装:如 interface, type 等,而是使用了 class 来进行封装。这里我们只谈谈这么封装的目的:

  • 固定字段规范的数据实体

所有的数据库交互实体,都会包含ID等字段,所以我们先定义个 BaseEntity 来进行数据约束:

class BaseEntity {
    id!: number
    createTime!: number
}

class UserEntity extends BaseEntity {
    // 无需再编写 基类中 声明的公共字段
    name!: string
}

如上的方式,我们可以少写很多公共的字段,而在一些公共的组件中需要传递数据时候, 我们可以限制类型为 BaseEntity 的子类即可,这样组件内就能取出传入数据的公共字段,比如可以直接取出 ID, 也可以自动的对创建时间进行友好的格式化显示等。

当然,这里也可以使用 interface 来定义,同样的实现继承来避免写相同的字段, 我们为什么依然使用class, 请继续阅读

  • 不固定规范的数据实体

难免碰到一些后端开发者不太喜欢使用 相同的公共字段,就像下面的一些数据交互方式:

{
  "user_id"123,
  "user_name""admin"
}
json复制代码{
  "role_id"122,
  "role_name""管理员"
}

如上习惯的数据交互方式,就不太适合使用 interface 继承来处理了, 但是配合装饰器,我们依然使用 class 的继承来实现这个需求:

我们还是声明了 BaseEntityUserEntity,但是我们加上了一些装饰器, 来配置一些关于数据转换方面的信息:)

class BaseEntity {
    id!: number
    createTime!: number
}

// 表示所有的用户字段 都需要用 user_ 开头
@Prefix("user_")
class UserEntity extends BaseEntity {
    name!: string
    idcard!: string
}

// 表示所有的角色字段 都需要用 role_ 开头
@Prefix("role_")
class UserEntity extends BaseEntity {
    name!: string
}

这样,我们就完成了一些配置,但可能这还不够,比如某一些字段 确实没有前缀,或者我们根本不想使用后端不规范的命名,比如后端给电话起了个简写的 pnum 当手机号?

@Prefix("user_")
class UserEntity extends BaseEntity {
    name!: string

    // 身份证号这个字段不需要前缀 就是 idcard
    @IgnorePrefix()
    idcard!: string
 
    @IgnorePrefix() 
    @Alias("pnum"//使用别名将后端的属性名称替代掉
    phone!: string
}

好的,于是我们开心的完成了关于字段的名称问题的一些配置, 但我们还需要一些处理的方法。于是我们声明一个 BaseModel 的类作为超类,让 BaseEntity 去继承它,这样所有的实体都拥有了这些转换方法,如果你是个不带 ID 的普通数据模型也可以直接继承 BaseModel

// 读取装饰器的一些配置,提供一些转换的方法
class BaseModel {
    // 具体的转换方法实现可以查看文末提供的开源项目代码
    toJson() {
        // 当前对象转为普通的JSON对象的方法
    }

    fromJson(json: Record<stringany>) {
        // 将后端给过来的JSON转为我们需要的类对象
    }
}

class BaseEntity extends BaseModel {
    // 不再重复写了
}

那么接下来我们就可以完成一些数据转换,然后实现不管后端的字段名如何,都能轻松的应对:

const json = {} // 从后端拿回来的JSON
const user = new UserEntity().fromJson(json) // 当然,还可以直接提供一些静态方法:
// 如  const user = UserEntity.fromJson(json) 、 const userList = UserEntity.fromJsonArray(jsonArray)
console.log(user.id) // 直接取我们自己声明的 id 而不是跟着后端走的 user_id

怎么样,是不是很开心,我管你怎么改,我字段名可以不被你牵着鼻子走, 即使后端接口把 user_id 改成了 userid ,我也不需要在我的代码中一个个的搜索跟着改:我只需要将 UserEntity 配置的装饰器改为 @Prefix("user"),如果对方需要改成 userId, 我还可以再写个装饰器, @Hump(),然后在 BaseModel 中转换的时候判断是否标记这个驼峰装饰器, 来选择是否需要将字段名自动驼峰处理。

:) 是不是很有意思?

  • 更变态的数据转换需求

如上所说,我们可以自动来处理一些字段名称的处理,我们也能来做一些字段属性类型的处理:

  • 布尔、数字、字符串的转换

  • 如果没有值,需要给默认值

  • 如果是数组或者挂载的其他对象,如用户身上带了角色

  • 是枚举值,需要枚举字典等

  • 等等等等...

  • 相似API的封装

在日常开发中,我们通常会遇到相同结构和请求方式的接口,有相同的接口命名方式,相同的参数和返回值等:)

一般来说,接口的请求地址可能不太一样,我们可以声明一个抽象类,要求子类中自行传入这个地址:

于是我们尝试使用一个 AbstractBaseService 类来进行一些基于面向对象继承的处理:)

abstract class AbstractBaseService {
    abstract apiUrl: string

    add() {
        request(this.apiUrl + "/add")
    }

    delete() {
    } // 删除

    // 等等等等
}

那么我们其他的子类就可以直接继承这个 Service 同时实现一下 apiUrl 这个属性 (Java: 直接抽象属性???)

class UserService extends AbstractBaseService {
    apiUrl = "user"
}

UserService 就拥有了所有父类中的增删改查方法,是不是很爽?当然,这里再加上泛型,把数据类型也约束上:

abstract class AbstractBaseService<E extends BaseEntity> {
    abstract apiUrl: string

    add(entity: E) {
        request(this.apiUrl + "/add", entity.toJson())
    }

    delete(entity: E) {
        request(this.apiUrl + "/delete", entity.toJson())
    }
}

// 子类传入对应的泛型约束
class UserService extends AbstractBaseService<UserEntity> {
    apiUrl = "user"
}

那么,这里的封装不仅实现了父类方法的复用,连接口请求把类型都卡死了:

const user = new UserEntity()
user.id = 1
new UserService().add(user) // 正常不报错

const role = new RoleEntity()
role.id = 1
new UserService().add(role) // 滚犊子 类型不匹配

这样就完成了公共部分的封装,而且还加上了一些类型约束,如果再把这些通用的操作以及动态绑定的数据统一抽到一个 hook 中, 那岂不是美滋滋?像这样:)

// ClassConstructor是我们封装的包装类
export function useAdd<E extends BaseEntity>(ServiceClass: ClassConstructor<E>, EntityClass: ClassConstructor<E>) {
    const formData = ref(new EntityClass())
    const service = ref(new ServiceClass())
    const isLoading = ref(false)

    const onAdd = () => {
        isLoading.value = true
        try{
            service.add(formData.value);
        }catch (e){
            alert("添加失败")
        }finally {
            isLoading.value = false
        }
    }

    return {
        formData, onAdd
    }
}

调用的视图可就更简单了:)

<template>
    <form>
        <input type="text" v-model="formData.name"/>
        <button @click="onAdd"></button>
    </form>
</template>
<script setup lang="ts">
    const {formData,onAdd} = useAdd(UserService,UserEntity)
</script>

这么写起来,是不是爽了很多呢?

本文总结

本文代码可能没有经过验证,都是写文章的时候顺带在 markdown 中直接手撸的,如有错误请评论区指出。

这里又回到了之前文章说的话题上了,我们也不仅仅是只使用了面向对象,我们也使用了函数式的一些hook。

没有必要在二者之间做出选择,成年人,为什么不能都要呢?

推荐阅读  点击标题可跳转

1、前端流程图插件对比选型

2、基于 vue3+threejs 实现可视化大屏

3、二十张图片彻底讲明白 Webpack 设计理念,以看懂为目的

继续滑动看下一个
大前端技术之路
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存