封装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
。具体的提取方式需要根据后端接口格式修改。
- handleError:是否统一处理错误,默认为
- 添加了常见错误情况的处理。
使用方式
封装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)
})
复制代码
如何使用本方案
- 直接将
src/request/
目录拷贝到项目中。 - 修改
src/request/types.ts
中的ServerResponseNormal
接口为项目统一接口格式。 - 修改
ServerResponseNormal
类型修改导致的请求拦截器逻辑修改。 - 在
src/request/index.ts
src/request/handleError.ts
中根据自身需要完善handleError
和showLoading
的逻辑处理。 - 还可根据自身需求扩展其他功能。
- 测试代码并调用实际接口使用。
后记
基本的方案介绍如上所述。后续再找时间更新实现思路及涉及到的知识点。