我们之前出了太多在前端使用 TypeScript
的类来实现的面向对象编程范式的文章和示例,引发了不小的一些讨论:
今天我们不使用 class
,来看看如何写出一些优雅的代码。
目录结构分类
首先,我们需要先根据业务代码类型做好文件目录、文件的分类:
-
assets
用于存放一些公共的 样式文件 图片 、字体等资源文件。
-
common
用于存放一些公共基础的 数据模型 API 等
-
components
主要存放一些公共的组件文件。
-
service
存放业务代码中的一些 数据模型 API 方法库 等。
封装公共模型
首先,我们根据后端给的设计,会抽离一些公共的数据模型:
响应 response.ts
后端给了接口文档说明了返回的数据结构为 code
message
data
三个属性,其中 code
为 200
时为成功,其他为失败。
ts
export interface Response<T extends any = any> {
code: number
message: string
data: T
}
排序 sort.ts
在请求列表时候我们可以传入排序的模型,这里我们提前声明一下:
ts
export interface Sort {
field: string
direction: 'asc' | 'desc'
}
下面的分页中会用到。
分页 page.ts
首先抽离分页的基础模型,包含 页码
每页条数
等属性。
ts
interface Page{
pageNum: number
pageSize: number
}
分页响应 page.response.ts
然后接着封装响应部分的分页模型,响应分页模型直接是继承来自 Page
模型,其中增加了 总条数
列表
等属性。
ts
export interface PageResponse<T> extends Page{
total: number
list: T[]
}
列表请求 request.ts
接着我们将请求不列表的模型也进行声明,这里我们添加了一个 filter
过滤器属性,以及一个用于排序的 sort
属性:
ts
import type { Base } from "./base/base";
import type { Sort } from "./sort";
export interface Request<T extends Base> {
filter: T
sort: Sort
}
分页请求 page.request.ts
当然少不了分页请求的模型。分页请求模型继承来自不分页模型,只是添加了分页参数:
ts
import type { Base } from "../base/base"
import type { Request } from "../request"
import type { Page } from "./page"
export interface PageRequest<T extends Base> extends Request<T> {
page: Page
}
基础模型API base.ts
我们需要将后端固定的数据结构和一些基础API进行封装,例如:
- 公共属性 包含
ID
是否禁用
创建时间
更新时间
等 - 基础API 包含
详情
删除
列表
分页
新增
修改
禁用
启用
等
ts
export interface Base {
id: number
isDisabled: boolean
createTime: number
updateTime: number
}
export function BaseApi<T extends Base>(url: string) {
const baseUrl = `/${url}`
return {
async get(id: number): Promise<T> {
},
async del(id: number): Promise<void> {
},
async list(): Promise<T[]> {
},
async page(): Promise<PageResponse<T>> {
},
async add(item: T): Promise<number> {
},
async update(item: T): Promise<void> {
},
async disable(id: number): Promise<void> {
},
async enable(id: number): Promise<void> {
},
}
}
上面的具体代码我们还没有实现,等封装完基础服务层之后再补充。
封装基础服务层
有了上面的一些公共模型,我们可以开始封装一些基础的服务层了,例如 网络请求
数据持久化
等。
网络请求 http.ts
我们还是基于 axios
来封装网络请求,这里我们先封装一个 post
方法:
根据后端文档的声明:
- 所有HTTP状态码固定
200
,否则直接提示请求出现异常,请稍后再试
- 返回 JSON 中的
code
为200
时为成功,401
为未登录,其他为失败。
需要登录时,绝大部分场景直接跳转到 /login
页面,但可能小部分场景有其他业务。
ts
import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"
import type { Response } from "../response"
import { useRouter } from "vue-router"
import { ElMessageBox } from "element-plus"
export function Http<T = any>(url: string, header: Record<string, string> = {}, hooks: {
redirectToLogin?: () => void
errorHandler?: (response: AxiosResponse) => void,
beforeRequest?: (config: AxiosRequestConfig) => AxiosRequestConfig
} = {}) {
let config: AxiosRequestConfig = {}
config.headers = header
config.baseURL = import.meta.env.API_URL || '/api/'
const { redirectToLogin, errorHandler, beforeRequest } = hooks
config.headers.Authorization = localStorage.getItem('token') || ''
if (beforeRequest) {
config = beforeRequest(config)
}
const STATUS = {
SUCCESS: 200
}
const CODE = {
SUCCESS: 200,
UNAUTHORIZED: 401,
}
let response: Promise<AxiosResponse<Response<T>>>;
function showError(message: string, title = '请求异常') {
ElMessageBox.alert(message, title, {
confirmButtonText: '好的',
type: 'error'
})
}
async function respond<T = any>(response: Promise<AxiosResponse<Response<T>>>) {
return new Promise<T>(async (resolve, reject) => {
const res = await response
if (res.status !== STATUS.SUCCESS) {
if (errorHandler) {
errorHandler(res)
} else {
showError("请求出现异常,请稍后再试")
}
reject(res)
return
}
if (res.data.code === CODE.UNAUTHORIZED) {
if (redirectToLogin) {
redirectToLogin()
} else {
useRouter().replace("/login")
}
reject("登录信息已过期,请重新登录")
return
}
if (res.data.code !== CODE.SUCCESS) {
if (errorHandler) {
errorHandler(res)
} else {
showError(res.data.message)
}
reject(res.data)
return
}
resolve(res.data.data)
})
}
async function post(data?: any): Promise<T> {
response = axios.post(url, data, config)
return respond<T>(response)
}
async function get(): Promise<T> {
response = axios.get(url, config)
return respond<T>(response)
}
return {
post, get
}
}
好,在上面的封装中,我们实现了一个 get
和 post
请求,同时支持了 自动接管异常 自定义异常处理 等功能。
接下来我们就可以去完善 BaseApi
中的一些功能了。
ts
import type { PageRequest } from "../page/page.request"
import type { PageResponse } from "../page/page.response"
import type { Request } from "../request"
import { Http } from "../utils/http"
export interface Base {
id: number
isDisabled: boolean
createTime: number
updateTime: number
}
export function BaseApi<T extends Base>(url: string) {
const baseUrl = `${url}/`
return {
async get(id: number): Promise<T> {
return await Http<T>(baseUrl + 'getDetail').post({ id })
},
async del(id: number): Promise<void> {
await Http<void>(baseUrl + 'delete').post({ id })
},
async list(request: Partial<Request<T>> = {}): Promise<T[]> {
return await Http<T[]>(baseUrl + 'getList').post(request)
},
async page(request: Partial<PageRequest<T>> = {}): Promise<PageResponse<T>> {
return await Http<PageResponse<T>>(baseUrl + 'getPage').post(request)
},
async add(item: T): Promise<number> {
const saved = await Http<T>(baseUrl + 'add').post(item)
return saved.id
},
async update(item: T): Promise<void> {
await Http<void>(baseUrl + 'update').post(item)
},
async disable(id: number): Promise<void> {
await Http<void>(baseUrl + 'disable').post({ id })
},
async enable(id: number): Promise<void> {
await Http<void>(baseUrl + 'enable').post({ id })
},
}
}
好像舒服了。
业务测试
我们来写一个和用户相关的用户测试:
ts
import { BaseApi, type Base } from "../common/base/base";
import { Http } from "../common/utils/http";
export interface User extends Base {
email: string
nickname: string
password: string
}
export function UserApi() {
const baseUrl = 'user/'
const baseApi = BaseApi<User>(baseUrl)
return {
...baseApi,
async login(user: Partial<User>): Promise<string> {
return await Http<string>(baseUrl + 'login').post(user)
},
}
}
调用一下,舒服了:
ts
const { login} = UserApi()
const accessToken = await login({
email: "admin@hamm.cn",
password: "Aa123456"
})
总结
这次我们没有通过大伙好像都不太喜欢的 class
面向对象
装饰器
等方式实现了一小部分的代码,写着也挺好的。
编程范式没有好和不好,流行与古老,合理的场景选择合理的技术路线即可。
本文所有的源代码可以在Github获取:
Bye