使用TypeScript封装一个更好用的Axios

·  阅读 2878
使用TypeScript封装一个更好用的Axios

封装axios算是老生常谈的话题了,基本上每个团队或者每个项目都会有自己的封装方式,也看到过一些封装。但是感觉还是多少有些不太好,或者不符合使用习惯。比如有的直接提供一个特定参数的请求函数,在内部再去调用axios,这种方式的使用就会因为参数而有局限性;大部分情况下是调用axios.create方法创建一个新实例,然后设置一下请求拦截处理,最后返回实例对象,一不小心还会把类型信息给丢掉了。

方案介绍

基于以上情况,并根据平时开发经历,逐步修改并完善了一套方案。完整代码见:

Github:github.com/chunjin666/…

Gitee:gitee.com/chunjine/en…

可以下载代码来查看实际使用效果,具体的代码结构和运行方式可以查看 README.md 文件中的说明。

特性

  • 封装后不改变 axios 本身提供的使用方式。
  • 基于 typescript ,增强了 axios 在使用过程中的类型的支持的完善度,并最大程度发挥 typescript 的类型推断能力,让使用者。
  • 新增了 getWrap postWrap putWrap deleteWrap patchWrap 等封装 API 的方法,使用非常简单,并保留了调用封装后函数后设置 config 的类型提示能力。
  • 新增了以下配置项:
    • handleError:是否统一处理错误,默认为 true。具体的错误处理方式需要根据自身业务需求完善。
    • showLoading:是否显示 loading 状态,默认为 true。具体的显示方式需要根据自身业务需求完善。
    • extractResponse:返回的数据是否进行提取处理,默认为 true。具体的提取方式需要根据后端接口格式修改。
  • 添加了常见错误情况的处理。

使用方式

封装API接口

下面使用使用基于 restful 规范的商品 CRUD 接口作为示例,展示了如何进行接口定义:调用 xxxWrap 扩展方法进行封装,定义好参数类型和返回值类型和请求url即可。

import type { PaginationParams, PaginationData } from '../request/types'
import request from '../request'

interface CreateGoodsParams {
  goodsName: string
  unitPrice: number
  count: number
}

interface EditGoodsParams extends CreateGoodsParams {
  id: string
  /** 是否上架 */
  shelf: boolean
}

interface GoodsDetailResponse extends CreateGoodsParams {
  id: string
  /** 是否上架 */
  shelf: boolean
}

interface GoodsListParams extends PaginationParams {
  /** 是否上架 */
  shelf: boolean
}

interface GoodsListResponseItem extends CreateGoodsParams {
  id: string
  /** 是否上架 */
  shelf: boolean
}

export const createGoods = request.postWrap<CreateGoodsParams, null>('/api/goods')

export const editGoods = request.putWrap<EditGoodsParams, null>('/api/goods')

export const deleteGoods = request.deleteWrap<{ id: string }, null>('/api/goods/{id}')

export const goodsDetail = request.getWrap<{ id: string }, GoodsDetailResponse>('/api/goods/{id}')

export const goodsList = request.getWrap<GoodsListParams, PaginationData<GoodsListResponseItem>>('/api/goods')
复制代码

调用封装好的API接口

下面展示了如何进行接口调用,接口符合规范的情况下,只需要处理调用成功的情况就行。

import { createGoods, deleteGoods, editGoods, goodsDetail, goodsList } from '../api/goods'

async function testCreate() {
  await createGoods({ goodsName: 'test', unitPrice: 2.33, count: 9999 })
  console.log('createGoods ok')
}

async function testEdit() {
  await editGoods({ id: '1', goodsName: 'test2', unitPrice: 2.33, count: 9999, shelf: true })
  console.log('editGoods ok')
}

async function testDelete() {
  await deleteGoods({ id: '1' })
  console.log('deleteGoods ok')
}

async function testDetail() {
  const detail = await goodsDetail({ id: '1' })
  console.log('goodsDetail ok', detail)
}

async function testList() {
  const res = await goodsList({ shelf: true })
  console.log('goodsList ok', res)
}

async function testClientError() {
  await clientError()
}

async function testServerError() {
  await serverError()
}
复制代码

更多详细用法介绍

一般情况下,后端接口的数据类型会遵循一个统一的结构,在最外层包含请求结果状态码、错误信息等。例如:

/**
 * 服务器端API统一数据格式
 *
 * TODO 根据实际情况修改
 */
export interface ServerResponseNormal<Data = any> {
  /** 状态码,0:正常 */
  code: number
  data: Data
  /** 错误信息 */
  msg: string
}
复制代码

默认情况下,我们会在返回拦截器里面对结果进行处理,只返回成功状态的data信息,同时,也支持设置 extractResponse: false 参数来返回服务器返回的完整结构信息。对于这两种不同返回值的情况,编辑器也能够自动推断出正确的类型信息。

我们先以 axios 原有的使用方法来展示:

import request from './request/index'

/**
 * 接口参数类型
 */
interface TestParams {
  b: number
}

/** 接口返回值类型 */
interface TestResponse {
  a: string
}

// 常规使用1:默认返回提取过的数据
request.get<TestResponse>('/api/test').then((res) => {
  console.log(res.a)
})
// 常规使用2:返回服务器端原始数据
request.get<TestResponse>('/api/test', { extractResponse: false }).then((res) => {
  console.log(res.data.a)
})
复制代码

此外,其他更多用法如下面的代码所示:

例如:新增的 handleError showLoading extractResponse 3个自定义参数的使用方法;封装API时设置更多默认参数;将参数对象中的参数值自动添加到 url 路径中;

import request from './request/index'

/**
 * 接口参数类型
 */
interface TestParams {
  b: number
}

/** 接口返回值类型 */
interface TestResponse {
  a: string
}

const params = { b: 1 }
// 封装1:默认返回提取过的数据
const getMethod1 = request.getWrap<TestParams, TestResponse>('/api/test')
// 使用
getMethod1(params).then((res) => {
  console.log(res.a)
})
// 封装2:封装为返回服务器端原始数据
const getMethod2 = request.getWrap<TestParams, TestResponse>('/api/test', { extractResponse: false })
// 使用
getMethod2(params).then((res) => {
  console.log(res.data.a)
})

// 封装3:封装为默认返回提取过的数据
const getMethod3 = request.getWrap<TestParams, TestResponse>('/api/test')
// 使用时设置返回服务器端原始数据
getMethod3(params, {
  extractResponse: true,
}).then((res) => {
  console.log(res.a)
})

// 封装4:封装 POST 请求
const postMethod4 = request.postWrap<TestParams, TestResponse>('/api/test')
// 使用
postMethod4(params).then((res) => {
  console.log(res.a)
})

// 封装5:封装 url参数
const getMethod5 = request.getWrap<TestParams & { id: string }, TestResponse>('/api/test/{id}')
// 使用
getMethod5({ id: '1', b: 1 }).then((res) => {
  console.log(res.a)
})

// 封装6:不统一处理错误,由调用者处理。以及其他参数设置
const getMethod6 = request.getWrap<TestParams, TestResponse>('/api/test', { handleError: false, showLoading: false })
// 使用
getMethod6(params).then((res) => {
  console.log(res.a)
}).catch((err: AxiosError) => {
  console.log(err)
})
复制代码

如何使用本方案

  1. 直接将 src/request/ 目录拷贝到项目中。
  2. 修改 src/request/types.ts 中的 ServerResponseNormal 接口为项目统一接口格式。
  3. 修改 ServerResponseNormal 类型修改导致的请求拦截器逻辑修改。
  4. src/request/index.ts src/request/handleError.ts 中根据自身需要完善 handleErrorshowLoading 的逻辑处理。
  5. 还可根据自身需求扩展其他功能。
  6. 测试代码并调用实际接口使用。

后记

基本的方案介绍如上所述。后续再找时间更新实现思路及涉及到的知识点。

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改