nuxt请求封装踩坑(无良博文害我误入歧途)

0 阅读3分钟

无良博文害我误入歧途

一开始为了省事,不去读文档,就去社区看了很多的请求封装,因为一直是用vue开发,都有封装请求库的习惯,看了好多的封装nuxt请求的文章,基本上都是对useFetch的直接封装,又去官网看了一眼确实是有那么一丝道理,请求拦截,相应啥都有,就开始照搬照抄。

const { data, status, error, refresh, clear } = await useFetch('/api/auth/login', {
  onRequest({ request, options }) {
    // 设置请求头
    // 注意:这依赖于 ofetch >= 1.4.0,你可能需要刷新你的锁文件
    options.headers.set('Authorization', '...')
  },
  onRequestError({ request, options, error }) {
    // 处理请求错误
  },
  onResponse({ request, response, options }) {
    // 处理响应数据
    localStorage.setItem('token', response._data.token)
  },
  onResponseError({ request, response, options }) {
    // 处理响应错误
  }
})

因此得到一代请求的封装,但是其实全是坑基本上用不了,但是一直在开发博客的其他页面也就没太在意。

import { type UseFetchOptions, type AsyncData, useCookie, useFetch, useRuntimeConfig } from 'nuxt/app'
// import { ElMessage } from 'element-plus'

class Request {
  // 基础URL
  private baseURL: string

  constructor() {
    const runtimeConfig = useRuntimeConfig()
    this.baseURL = runtimeConfig.public.apiBase as string
  }

  awaitTo<T>(promise: Promise<T>) {
    return promise.then((data: T)=> [null, data]).catch((err: any)=> [err, null])
  }

  /**
   * 核心请求方法(包含完整拦截器)
   * @param url 请求地址
   * @param options 请求配置
   */
  async request<T = any>(url: string, options: UseFetchOptions<T> = {} as UseFetchOptions<T>) {
    // 1. 基础配置
    const fetchOptions: UseFetchOptions<T> = {
      baseURL: this.baseURL,
      timeout: 10000,
      method: options.method || 'GET',
      params: options.params,
      body: options.body,
      server: options.server ?? true,
      immediate: options.immediate ?? true,
      ...options,

      onRequest({ request, options }) {
        // 设置请求头
        // 注意:这依赖于 ofetch >= 1.4.0,你可能需要刷新你的锁文件
        // options.headers.set('Content-Type', 'application/json')
      },
      onRequestError({ request, options, error }) {
        // 处理请求错误
      },
      // 响应成功拦截器
      onResponse({ request, response, options }) {
        // console.log('response===========>', response)
        response._data = response._data.data
      },
      onResponseError({ request, response, options }) {
        // 处理响应错误
      }
    }
    // 2. 执行原生useFetch

    const res = this.awaitTo(useFetch<T>(url, fetchOptions as any))

    return res
  }

  // ========== 快捷请求方法 ==========
  async get<T = any>(url: string, params?: any, options: UseFetchOptions<T> = {} as UseFetchOptions<T>) {
    return this.request<T>(url, { ...options, method: 'GET', params })
  }

  async post<T = any>(url: string, data?: any, options: UseFetchOptions<T> = {} as UseFetchOptions<T>) {
    return this.request<T>(url, { ...options, method: 'POST', body: data })
  }

  async put<T = any>(url: string, data?: any, options: UseFetchOptions<T> = {} as UseFetchOptions<T>) {
    return this.request<T>(url, { ...options, method: 'PUT', body: data })
  }

  async delete<T = any>(url: string, params?: any, options: UseFetchOptions<T> = {} as UseFetchOptions<T>) {
    return this.request<T>(url, { ...options, method: 'DELETE', params })
  }
}

// 全局单例实例
export const request = new Request()

部署项目后发现请求根本用不了

后面部署后,每个请求都会重复一次,而且分页切换时候还会报错,才决定好好读读文档来解决请求问题。

$fetch根本不能作为直接获取数据的请求,可能会导致数据被获取两次:一次在服务器(用于渲染 HTML),另一次在客户端(当 HTML 被激活时)。这可能会导致激活问题、增加交互时间并引发不可预测的行为。useFetch 会确保请求在服务器上发生,并正确转发到浏览器。$fetch 没有这种机制,更适合仅从浏览器发起请求的场景。官网也给了例子,说useFetch 会确保请求在服务器上发生,并正确转发到浏览器。$fetch 没有这种机制,更适合仅从浏览器发起请求的场景。

<script setup lang="ts">
const { data } = await useFetch('/api/data')

async function handleFormSubmit() {
  const res = await $fetch('/api/submit', {
    method: 'POST',
    body: {
      // 我的表单数据
    }
  })
}
</script>

<template>
  <div v-if="data == undefined">
    无数据
  </div>
  <div v-else>
    <form @submit="handleFormSubmit">
      <!-- 表单输入标签 -->
    </form>
  </div>
</template>

正确的请求封装

其实官网上一开始就有了如何封装请求,只是他对数据获取有过多的陈述,导致我忽略这个他的封装指南,加之useFetch上有请求拦截等,我对封装请求的刻板印象,和那些无良博文,让我一度认为封装这个api是对的,知道自己亲身使用才发现不行,官网上明确的说明了,只能通过封装ofetch这个库,来进行统一的请求处理,但是页面的水合又不能使用这个请求,作为初始化请求,因此初始化请求还是要用到useFecth或者useAsyncData来做,只是将封装的ofecth作为请求参数传给这两个api即可。官网封装文档

  1. 封装ofetch统一处理请求,用于从客户端发起请求
     // plugins/api.ts 封装成一个插件
     export default defineNuxtPlugin((nuxtApp) => {
       const { session } = useUserSession()
     
       const api = $fetch.create({
         baseURL: 'https://api.nuxt.com',
         onRequest({ request, options, error }) {
           if (session.value?.token) {
             // 注意:这依赖于 ofetch >= 1.4.0 - 你可能需要刷新你的 lockfile
             options.headers.set('Authorization', `Bearer ${session.value?.token}`)
           }
         },
         async onResponseError({ response }) {
           if (response.status === 401) {
             await nuxtApp.runWithContext(() => navigateTo('/login'))
           }
         }
       })
     
       // 通过 useNuxtApp().$api 暴露
       return {
         provide: {
           api
         }
       }
     })
    
  2. 再通过上面这个插件封装useFecth,用于处理页面初始数据的获取
       import type { UseFetchOptions } from 'nuxt/app'
    
       export function useAPI<T>(
         url: string | (() => string),
         options?: UseFetchOptions<T>,
       ) {
         return useFetch(url, {
           ...options,
           $fetch: useNuxtApp().$api as typeof $fetch
         })
       }
    
  3. 页面请求的正确姿势
     import { ArticleApi, TagsApi } from '@/api'
     import type { IArticle, ITags } from '@/api'
     
     const { $api } = useNuxtApp()
     
     const pageQuery = ref({
       current: 1,
       size: 10,
       total: 0
     })
     
     const articleList = ref<IArticle[]>([])
     /**
      * 获取页面初始化的数据
      */
     const { data } = await useAPI<IPageResult<IArticle[]>>(ArticleApi.ArticlePage, {
       params: { current: pageQuery.value.current, size: pageQuery.value.size }
     })
    
     /**
      * 分页切换时,获取数据
      */
     async function getArticleList() {
       const { current, size } = pageQuery.value
       // 直接调用$api即可
       const res = await $api<IPageResult<IArticle[]>>(ArticleApi.ArticlePage, { params: { current: current, size }})
       manageArticleData(res)
     }