前端不在TypeScript使用类,如何写出清晰优雅的代码?

我们之前出了太多在前端使用 TypeScript 的类来实现的面向对象编程范式的文章和示例,引发了不小的一些讨论:

今天我们不使用 class,来看看如何写出一些优雅的代码。

目录结构分类

首先,我们需要先根据业务代码类型做好文件目录、文件的分类:

  • assets

    用于存放一些公共的 样式文件 图片字体等资源文件。

  • common

    用于存放一些公共基础的 数据模型 API

  • components

    主要存放一些公共的组件文件。

  • service

    存放业务代码中的一些 数据模型 API 方法库 等。

封装公共模型

首先,我们根据后端给的设计,会抽离一些公共的数据模型:

响应 response.ts

后端给了接口文档说明了返回的数据结构为 code message data 三个属性,其中 code200 时为成功,其他为失败。

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 中的 code200 时为成功,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
  }
}

好,在上面的封装中,我们实现了一个 getpost 请求,同时支持了 自动接管异常 自定义异常处理 等功能。

接下来我们就可以去完善 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获取:

github.com/HammCn/AirB...

Bye

相关推荐
打野赵怀真15 分钟前
render函数中return如果没有使用()会有什么问题?
前端·javascript
Riesenzahn17 分钟前
写一个左中右的满屏布局,左右固定220px,中间自适应并且要优先加载
前端·javascript
Riesenzahn17 分钟前
css在页面上画一个正方形,边长为页面宽度的一半
前端·javascript
tommyrunner19 分钟前
Cursor rule文件测试 一秒了解AI行为规则文件
前端·cursor
北京_宏哥24 分钟前
《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
java·前端·selenium
Nu1129 分钟前
weakMap 和 weakSet 原理
前端·面试
顾林海31 分钟前
深入理解 Dart 函数:从基础到高阶应用
android·前端·flutter
比特鹰35 分钟前
桌面端跨端框架调研
前端·javascript·前端框架
Ratten36 分钟前
【JavaScript】---- JS原生的深拷贝API structuredClone 使用详解与注意事项
前端·javascript