1. 写在前面
先聊聊我们为什么做了这么个项目吧。
在2019年夏天的某个夜晚,我还在拿 Vue2/JavaScript/Element UI 肝一个 B/S 架构的 Web B端后台管理项目。当时正在做的是一个 WMS系统 的出入库单据管理页面,出库单提交的时候,需要先查询其他 N 个接口的数据进行组合之后提交到后端。然而此时问题出现了:
- 某些接口返回的主键是
Long的ID,有些接口返回的是类似userId类的主键key - 某些接口布尔值返回了
1/0,有些又返回了true/false - 某些接口的
createTime返回的是毫秒时间戳,有的返回的是yyyy-MM-dd HH:mm:ss格式的字符串 - 某些接口的 空数组数据 返回的不是
[]而是null - 某些接口的字符串数据返回的不是
""而是null - 某些
PUT接口表示了新建一条数据,POST又用来做了查询 - 某些接口直接
{"code":500, "message":"未知异常"} - 某些接口。。。
- 某些。。。
在我情绪即将崩溃之际,凭借我三十年的工作经验(梗),我决定做点什么。于是有了这篇文章:分享一些我们正在使用的API接口开发规范 (当然,实际我们约定的规范这只是冰山一角)
然而约定规范后发现,后端改起来倒是挺快的(按照规范,我后来个人出了一套 Java 框架: AirPower4J),前端改起来可麻烦大了:
- 项目已经进行了 30% - 40% 了,直接改掉之前的也不太恰当了。
- 新的代码使用这套规范的话,基于
JavaScript的灵活,也不太好约束后来的代码了。 - 推翻重来的话,成本又太高了。
于是这个项目本着 瞎** 完成的原则进行了开发和交付,但团队成员都沉下心来思考了一下之前碰到的问题(团队成员水平参差不齐,而且团队是新成立的,也没有默契)。
最终我们决定 先写规范,再写代码,先做好技术选型后再开发业务代码。
2. 技术选型
在团队小伙伴激烈的讨论和评审之后,最终我们决定使用 TypeScript + Vue3 + Element Plus + Vite + Axios + Vue Router + Sass + ESLint + Prettier 等技术栈的方式来进行开发:
其中
TypeScript严格使用 数据建模 来规范代码,Vue3采用 Composition API 模式,ESLint采用 Airbnb 规范 等等。
于是乎有了这么一些清晰的概念(这里后端的小伙伴应该很熟悉了,特别是 Java SpringBoot JPA 的后端程序员):
因为起名的问题(抄了 Apple 难产产品的名字 AirPower),很多类都携带了前缀
Air
AirModel
一切数据结构(除配置类的JSON数据结构)之外,都继承来自 AirModel, 表示该类数据结构都至少包含 copy() toJson() recoverBy() 等很多常用的方法。
AirEntity(继承自 AirModel)
一切从数据库里出来的数据,都使用 Entity 声明为 实体,且继承来自 AirEntity,表示至少有 ID 创建时间 修改时间 这些前后端约定的公共属性
AirService
表示前端发送网络请求的 超类,其中完成了 Axios 的封装、Loading态管理、请求头管理、请求异常处理、响应数据处理、超时管理等等网络请求的通用功能。
AirEntityService(继承自 AirService)
表示是 实体数据 的网络请求服务,其中至少包含了 add() update() delete() get() list() page() 等网络请求功能。
AirEnum 枚举类
表示标准枚举类,其中至少包含 key label 等属性,且提供了 AirEnum 的助手类来完成一些枚举的其他功能,如通过 key 获取 label、创建枚举、枚举转数据等等常用的功能。
Decorator 装饰器
与 Java 注解类似,在前端的 TypeScript 里,我们也声明了大量的装饰器来完成一些通用的功能,实现一处配置处处生效。比如:
-
@Form表示该属性是表格字段,且可以配置输入类型默认值数据校验输入长度表单说明等常用的表单选项 -
@Table表示该属性是表格字段,且可以配置表格的宽度是否显示排序是否枚举渲染是否可复制是否是时间列等等表格选项 -
@Search表示该字段是搜索字段,会显示到表格上方的搜索区域里,且可配置搜索的一些表单样式等等 -
@Model@Field表示类或字段的一些翻译信息,比如UserEntity是用户,nickName是昵称等等 -
@Type表示强制转换类型,如后端的1/0转到前端的true/false,数组的null自动转为[]等,同时还提供了 类到JSON 和 JSON 到类的自定义转换等装饰器 -
@Alias表示类似fastjson的一些属性别名配置,可处理后端改名之后前端需要跟着修改的情况
还有很多很多很多很多。
看到这里,相信有一些 Java 开发者已经很熟悉了。
3. 部分实现
接下来我们将用代码来演示我们一些和后端非常类似的 前端、Vue3、TypeScript 的一些开发示例
3.1 实体类的声明
我们将通过继承的方式来声明用户实体,除了用户本身的属性之外,还从 BaseEntity 上继承了一些基础属性,如 ID 创建时间 修改时间 等等。
同时使用了大量的装饰器来对用户的一些行为进行了描述
@Model("用户")
export class UserEntity extends BaseEntity {
@Form({
email: true,
requiredString: true,
})
@Table()
@Search()
@Field('邮箱') email!: string
@Form({
password: true,
})
@Field('密码') password!: string
@Form({
requiredString: true,
minLength:3,
maxLength: 12
})
@Table()
@Search()
@Field('昵称') nickname!: string
@Form({
mobilePhone: true,
requiredString: true,
})
@Table({
phone: true,
})
@Search()
@Field('手机') phone!: string
@Field('角色')
@Table({
payloadArray: true,
payloadField: 'name',
})
@Type(RoleEntity, true) roleList!: RoleEntity[]
}
3.2 Service(API)的声明
通过类的声明,完成了后端API路径的配置,同时从服务基类中继承了所有的增删改查方法
export class UserService extends AbstractBaseService<UserEntity> {
baseUrl = 'user'
entityClass = UserEntity
async login(user: UserEntity): Promise<string> {
const result = await this.api('login').post(user)
return result as unknown as string
}
async register(user: UserEntity): Promise<void> {
await this.api('register').post(user)
}
async resetMyPassword(user: UserEntity): Promise<void> {
await this.api('resetMyPassword').post(user)
}
async getMyInfo(): Promise<UserEntity> {
const json = await this.api('getMyInfo').post()
return UserEntity.fromJson(json)
}
}
3.3 枚举的声明
声明了枚举之后,可以将这个枚举类标记到 @Form @Table @Search 等装饰器上,前端页面将会按照枚举进行下拉框的渲染等操作
import { AirEnum } from '@/airpower/base/AirEnum'
import { AirColor } from '@/airpower/enum/AirColor'
export class MaterialTypeEnum extends AirEnum {
static readonly PUBLIC = new MaterialTypeEnum(1, '公共物料', AirColor.SUCCESS)
static readonly PRIVATE = new MaterialTypeEnum(2, '私有物料', AirColor.NORMAL)
}
3.4 简单表格页面
通过我们内置的一些组件封装,实现了统一风格的标准工具栏、表格、分页等,且提供了方便的 useAirTable 的 Hooks,直接使用一些增删改查、禁用启用的基础功能。
<template>
<APanel>
<AToolBar
:loading="isLoading"
:entity="UserEntity"
:service="UserService"
@on-add="onAdd"
@on-search="onSearch"
/>
<ATable
v-loading="isLoading"
:data-list="response.list"
:entity="UserEntity"
:ctrl-width="150"
show-enable-and-disable
@on-edit="onEdit"
@on-delete="onDelete"
@on-enable="onEnable"
@on-disable="onDisable"
/>
<template #footerLeft>
<APage
:response="response"
@on-change="onPageChanged"
/>
</template>
</APanel>
</template>
<script lang="ts" setup>
import {
APage, APanel, ATable, AToolBar,
} from '@/airpower/component'
import { UserEntity } from '@/model/user/UserEntity'
import { UserService } from '@/model/user/UserService'
import { useAirTable } from '@/airpower/hook/useAirTable'
import { UserEditor } from './component'
import { AirDialog } from '@/airpower/helper/AirDialog'
import { UserSelector } from '@/view/console/user/component'
const {
isLoading, response,
onPageChanged, onDelete, onEdit, onAdd, onSearch, onEnable, onDisable,
} = useAirTable(UserEntity, UserService, {
editView: UserEditor,
})
</script>
3.5 编辑用户
通过封装的弹窗、表格字段等组件,通过 useAirEditor Hooks快速取出需要的一些方法和属性,快速实现用户的编辑功能。
<template>
<ADialog
:title="title"
:form-ref="formRef"
:loading="isLoading"
confirm-text="保存"
:fullable="false"
@on-confirm="onSubmit()"
@on-cancel="onCancel()"
>
<el-form
ref="formRef"
:model="formData"
label-width="120px"
:rules="rules"
@submit.prevent
>
<AFormField field="email" />
<AFormField field="nickname" />
</el-form>
</ADialog>
</template>
<script lang="ts" setup>
import {
AButton, ADialog, AFormField, AGroup,
} from '@/airpower/component'
import { airPropsParam } from '@/airpower/config/AirProps'
import { UserService } from '@/model/user/UserService'
import { UserEntity } from '@/model/user/UserEntity'
import { AirDialog } from '@/airpower/helper/AirDialog'
import { useAirEditor } from '@/airpower/hook/useAirEditor'
const props = defineProps(airPropsParam(new UserEntity()))
const {
isLoading, formData, formRef, title, rules,
onSubmit,
} = useAirEditor(props, UserEntity, UserService)
</script>
3.6 验证器相关
我们对 ElementPlus 的验证器进行了封装,使用起来更简单,更语义化(当然,@Form 上本身已经支持了很多验证):
const rules = {
nickname: [
AirValidator.show('不允许包含管理员等字符').ifContain('管', '理', '员'),
AirValidator.show('昵称只允许中文').ifNotChinese(),
AirValidator.show('昵称不能为空').ifEmpty(),
],
idcard: [
AirValidator.show('不是有效的二代身份证').ifNotChineseIdCard(),
],
account: [
AirValidator.show('账号只允许字母和数字').ifNot(AirInputType.NUMBER, AirInputType.CHAR),
],
}
3.7 更多的例子
太多了太多了。。。
4. 框架设计原则
AirPower4T 的灵感来自于 Java SpringBoot JPA 等后端开发思想,使用了大量的 类、枚举、接口、装饰器 等, 提供了很多基于 Element Plus 的常用后台管理组件,帮助开发者快速开发 Web 应用。
AirPower4T 的设计理念是 面向对象编程,将一切能抽象的功能、数据结构、服务封装为 类, 使用类的继承来实现一些代码的复用,减少重复代码,使代码更加清晰易读。 使用装饰器来实现一些 常用组件、数据转换规则、前端显示文案 等信息的配置,使组件的配置更集中、灵活、直观。
与目前主流的编码方式不太一致,但都是通过一些设计模式、架构思想、封装思维来完成一些重复代码的设计和封装、快速二次开发的接入等。
5. 开源仓库
我们目前是以核心包子目录的方式和宿主项目进行集成,如果你有兴趣,可以看看我们这套前端框架 AirPower4T 的开源仓库:
-
Github: github.com/HammCn/AirP…
-
Gitee: gitee.com/air-power/A…
具体如何使用,可以阅读开源仓库的 README
当然,我们也提供了与 AirPower4T 这套设计完美融合的后端框架 AirPower4J,如果你想做全栈,也可以参考我们后端的开源仓库:
-
Github: github.com/HammCn/AirP…
-
Gitee: gitee.com/air-power/A…
6. 最后
感谢你的阅读,如果能看到这里,也感谢你浓浓的兴趣。
今天的文章完毕。