用代码聊聊我们跟目前主流前端编程不一样的地方
作者:Hamm
https://juejin.cn/post/7272372568623710264
写在前面
也许跟大部分的前端开发者不同,我们使用了 Vue3
和 TypeScript
, 但我们也许是又回到了 老古董 的编程方式中, 也许是习惯了 面向对象(OOP) ,又或者是跑了一圈, 相比现在的 JavaScript、Python、PHP、Go、C# 等,依然还是钟爱 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
的继承来实现这个需求:
我们还是声明了 BaseEntity
和 UserEntity
,但是我们加上了一些装饰器, 来配置一些关于数据转换方面的信息:)
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<string, any>) {
// 将后端给过来的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。
没有必要在二者之间做出选择,成年人,为什么不能都要呢?
前端大学 公众号 祝 您:2023 年暴富!万事如意!
分享前端干货,点赞就是最大的支持,比心❤️