痛点
写过vue项目的朋友一定知道鼎鼎大名的axios,在做vue项目时,都会对axios进行分装,将请求接口独立在一个文件夹中进行管理,方便后续的维护和开发,但是在nuxt中,nuxt有专门提供发起网络请求的方法,不需要使用额外的第三方库,也不建议使用其他第三方库,因为自身提供的库存在第三方库无法提供的功能,如服务端渲染的能力,但是自身提供的网络请求方法使用方式采用的hook的形式,对于习惯axios的封装的我们来说可能会存在不习惯,或者当我们要将一个Vue3项目进行迁移时需要将所有接口都转化成对应的方法才行,对此我对useFetch方法进行了一次封装。
封装
因为代码中有大量注释,就不一一解释了,直接放出代码
// constant.ts
export default {
CN: 'zh-CN',
EN: 'en-US'
}
// utils.ts
/**
* 串字符转小驼峰 ssr-get => ssrGet
* @param str 串字符
* @return 返回小驼峰字符
*/
export function transformStrSubToHump(str: string): string {
const re = /-(\w)/g
return str.replace(re, ($0, $1) => {
return $1.toUpperCase()
})
}
/**
*
* @param enumData 枚举
* @param needValue 是否只要枚举的值,默认true
*/
export function getEnumValueOrKeys(enumObject: any, needValue: boolean = true): number[] | string[] | any[] {
const result: number[] | string[] | any[] = []
for ( const key in enumObject ) {
if ( !isNaN(enumObject[key]) && needValue )
result.push(enumObject[key])
else result.push(key)
}
return result
}
// useRequest.ts
import type { UseFetchOptions } from 'nuxt/app'
import { useStorage } from '@vueuse/core'
import type { FetchResponse } from 'ofetch'
import { ElMessage } from 'element-plus'
import type { Ref } from 'vue'
import LANG_CONSTANT from '@/lang/i18n/constant'
import { getEnumValueOrKeys, transformStrSubToHump } from '~/utils/utils'
// 接口返回的数据类型
export interface DefaultResult<T = any> {
code: number
data: T
msg: string
}
// 接口返回的code存在以下几种情况时直接返回data
export enum ResultEnum {
SUCCESS = 1,
TOKEN_OVERDUE = 210311001, // 用户登录失败
}
// 返回请求结果的code需要进行错误处理的枚举
export enum ResultErrorEnum {
FAIL = 0,
SERVER_ERROR = -1,
}
export enum ServerStatusEnum {
NOT_FOUND = 404, // 服务器资源不存在
SERVER_ERROR = 500, // 服务器内部错误
ACCESS_DENIED = 403, // 没有权限访问该资源
LOGIN_EXPIRATION = 401, // 登录状态已过期
BAD_REQUEST = 400, // 客户端请求错误
}
type UrlType = string | Request | Ref<string | Request> | (() => string | Request)
type HttpOption<T> = UseFetchOptions<DefaultResult<T>>
interface RequestConfig<T = any> extends HttpOption<T> {
// 忽略拦截,不走拦截,拥有 responseData,且 code !== 0 的情况下,直接返回 responseData,
ignoreCatch?: boolean
// 忽略全局错误提示,走拦截,但是任何情况下都不会提示错误信息
ignoreGlobalErrorMessage?: boolean
notSSR?: boolean // 不进行SSR渲染
}
// 处理status不为200
function handlerServerError<T>(response: FetchResponse<DefaultResult<T>> & FetchResponse<ResponseType>, options: any) {
const { ignoreGlobalErrorMessage } = options
if ( ignoreGlobalErrorMessage )
return
const err = (text: string) => {
ElMessage.error({
message: response?._data?.msg ?? text,
})
}
if ( !response._data ) {
err('请求超时,服务器无响应!')
return
}
// 处理异常
const handleMap: { [key in ServerStatusEnum]: () => void } = {
[ServerStatusEnum.NOT_FOUND]: () => err('服务器资源不存在'),
[ServerStatusEnum.SERVER_ERROR]: () => err('服务器内部错误'),
[ServerStatusEnum.ACCESS_DENIED]: () => err('没有权限访问该资源'),
[ServerStatusEnum.LOGIN_EXPIRATION]: () => {
const router = useRouter()
console.log("401清除登录信息")
localStorage.removeItem('userInfo')
router.push('/login')
return err('登录状态已过期,需要重新登录')
},
[ServerStatusEnum.BAD_REQUEST]: () => err('客户端请求错误'),
}
handleMap[response.status as ServerStatusEnum] ? handleMap[response.status as ServerStatusEnum]() : err('未知错误!')
}
// 处理返回的code不为1的情况
function handlerResponseDataError<T>(data: DefaultResult<T>, options: any) {
const {
ignoreCatch,
ignoreGlobalErrorMessage,
} = options
const {
code,
msg,
} = data
if ( !ignoreCatch ) {
// 接口请求错误,统一处理
switch ( code ) {
default:
if ( !ignoreGlobalErrorMessage ) {
ElMessage.error({
message: msg ?? '服务响应失败,请稍后重试',
})
}
return Promise.reject(msg || '服务响应失败,请稍后重试')
}
}
}
async function request<T>(
url: string,
params: any,
options: RequestConfig<T>,
): Promise<DefaultResult<T> | T | undefined> {
try {
const headers = useRequestHeaders([ 'cookie' ])
const method = ((options?.method || 'GET') as string).toUpperCase()
const lang = useStorage('curLang', LANG_CONSTANT.CN)
const userInfo = useStorage<any>('userInfo', {})
const runtimeConfig = useRuntimeConfig()
const { baseUrl } = runtimeConfig.public
const baseURL = `${ baseUrl }`
// nuxt的useFetch在参数不变的情况下,数据是不会重新从后台接口去请求数据
// 确保每次请求会获取到,也可以使用时间戳解决
// await nextTick()
if ( options.notSSR )
await nextTick()
const {
data,
error,
} = await useFetch(
url,
{
// baseURL,
key: encodeURI(url + JSON.stringify(params)),
headers,
credentials: 'include',
params: method === 'GET' ? params : undefined,
body: method === 'POST' ? JSON.stringify(params) : undefined,
...options,
// 请求拦截
onRequest({
request,
options,
}) {
// 拦截时添加每个接口都需要传入的一些参数
options.params = {
...options.params,
lan: lang.value == 'zh-CN' ? 'cn' : 'en',
Authorization: userInfo.value?.api_key || '',
Mobile_key: userInfo.value?.mobile_key || '',
FromBrandId: userInfo.value?.brand_id || '',
timestamp: new Date().getTime()
}
},
onRequestError(context: any & { error: Error }): Promise<void> | void {
// console.log("请求错误:", Error)
},
onResponseError({
request,
response,
options,
}) {
handlerServerError(response, options)
return response ?? null
},
})
const responseData = data.value as DefaultResult<T>
if ( !error.value && !responseData ) {
console.log('ssr fetch 返回结果为空')
} else if ( error.value || !responseData ) {
return Promise.reject(error.value || '服务响应失败,请稍后重试')
} else {
const {
code,
data: result,
} = responseData
const resultSuccessEnum = getEnumValueOrKeys(ResultEnum)
// 接口请求成功,直接返回结果
if ( resultSuccessEnum.includes(code as any) ) {
console.log(`${ url }最终结果返回:`, responseData)
return responseData
}
const codeErrorValue = getEnumValueOrKeys(ResultErrorEnum)
if ( codeErrorValue.includes(code as any) ) {
const dataError = handlerResponseDataError(responseData, options)
if ( dataError )
return responseData
}
return responseData as DefaultResult<T>
}
} catch ( e ) {
console.error('请求异常:', url, params, options,e);
return Promise.reject(e);
}
}
const ssrPrefix = 'ssr-'
/**
* 自动导出请求 这里导出了 get post ssrGet ssrPost四个方法
* @returns {
* get
* post
* ssrGet
* ssrPost
* }
*/
interface Acc {
[key: string]: any;
}
type UseRequestReturnType = {
get: <T>(url: string, params?: any, option?: RequestConfig<T>) => Promise<T>
post: <T>(url: string, params?: any, option?: RequestConfig<T>) => Promise<T>
ssrGet: <T>(url: string, params?: any, option?: RequestConfig<T>) => Promise<T>
ssrPost: <T>(url: string, params?: any, option?: RequestConfig<T>) => Promise<T>
}
export const useRequest: UseRequestReturnType = [ 'get', 'post', `${ ssrPrefix }get`, `${ ssrPrefix }post` ]
.map((method: string) => [ method, <T>(url: string, params?: any, option?: RequestConfig<T>) => {
if ( !method.startsWith(ssrPrefix) ) {
return request<T>(url, params, {
method: method.replace(ssrPrefix, '') as any,
...option,
server: false,
notSSR: true,
})
}
return request<T>(url, params, { method: method.replace(ssrPrefix, '') as any, ...option })
} ])
.reduce((acc: Acc, [ method, fn ]) => {
acc[transformStrSubToHump(method as string)] = fn
return acc
}, {}) as UseRequestReturnType
使用方式
// URLConstant.ts
const prefix = "/api"
export default {
GET_BOOKS_LIST: `${prefix}/list`, // 获取列表数据
POST_ADD_BOOK: `${prefix}/book`, // 添加书本
}
// list.ts
import type { DefaultResult } from "~/composables/useRequest";
import URLConstant from "~/api/URLConstant";
// 获取书籍列表,采用ssr形式
export const getListSSRApi = () => {
return useRequest.ssrGet<DefaultResult<{
count: number
list: any[]
}>>(URLConstant.GET_RESUMES)
}
// 获取书籍列表,普通
export const getListApi = () => {
return useRequest.get<DefaultResult<{
count: number
list: any[]
}>>(URLConstant.GET_RESUMES)
}
export const postListApi = (params: {
title: string
price: number
}) => {
return useRequest.post<DefaultResult<{
count: number
list: any[]
}>>(URLConstant.POST_ADD_BOOK,params)
}
// book.vue
<template>
<div>
</div>
</template>
<script>
import {getListSSRApi} from "@/api/list"
const list = ref<any[]>([])
const count = ref(0)
getListData() // 请求数据
async function getListData() {
const res = await getListSSRApi()
if(res == 1) {
list.value = res.list
count.value = count
}
}
</script>