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

写在前面

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

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

我们如何使用面向对象

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

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

数据实体的封装

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

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

固定字段规范的数据实体

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

typescript 复制代码
class BaseEntity {
    id!: number
    createTime!: number
}

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

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

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

不固定规范的数据实体

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

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

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

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

typescript 复制代码
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 当手机号?

typescript 复制代码
@Prefix("user_")
class UserEntity extends BaseEntity {
    name!: string

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

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

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

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

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

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

typescript 复制代码
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 中转换的时候判断是否标记这个驼峰装饰器, 来选择是否需要将字段名自动驼峰处理。

:) 是不是很有意思?

更变态的数据转换需求

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

  • 布尔、数字、字符串的转换
  • 如果没有值,需要给默认值
  • 如果是数组或者挂载的其他对象,如用户身上带了角色
  • 是枚举值,需要枚举字典等
  • 等等等等...

可以参考我们这篇关于数据转换的文章:基于装饰器-我是这么处理TypeScript项目数据转换的

相似API的封装

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

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

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

typescript 复制代码
abstract class AbstractBaseService {
    abstract apiUrl: string

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

    delete() {
    } // 删除

    // 等等等等
}

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

typescript 复制代码
class UserService extends AbstractBaseService {
    apiUrl = "user"
}

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

typescript 复制代码
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"
}

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

typescript 复制代码
const user = new UserEntity()
user.id = 1
new UserService().add(user) // 正常不报错

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

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

typescript 复制代码
// 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
    }
}

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

html 复制代码
<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。

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

That's all

我们最近在使用 vue3 TypeScript ElementPlus Java SpringBoot JPA 等技术栈实现一套基于面向对象的前后端统一开发全栈项目,欢迎关注:

前端:github.com/HammCn/AirP...

后端:github.com/HammCn/AirP...

相关推荐
腾讯TNTWeb前端团队1 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰5 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪5 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪5 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy6 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom6 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom6 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom6 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom6 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom7 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试