痛点

写过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>