如何优雅的封装 Nuxt3 useFetch

如何优雅的封装 Nuxt3 useFetch

Tags
Vue.js
Nuxt.js
Published
April 16, 2024
Author

Nuxt3 数据获取介绍

Nuxt 提供了两个组合函数和一个内置库,用于在浏览器或服务器环境中执行数据获取:useFetchuseAsyncData 和 $fetch
简而言之:
  • useFetch 是在组件设置函数中处理数据获取的最简单方法。
  • $fetch 可以根据用户交互进行网络请求。
  • useAsyncData 结合 $fetch,提供了更精细的控制。
useFetch 和 useAsyncData 共享一组常见的选项和模式。

为什么需要使用特定的组合函数

使用像 Nuxt 这样的框架可以在客户端和服务器环境中执行调用和呈现页面时,必须解决一些问题。这就是为什么 Nuxt 提供了组合函数来封装查询,而不是让开发者仅依赖于 $fetch 调用。

网络请求重复

useFetch 和 useAsyncData 组合函数确保一旦在服务器上进行了 API 调用,数据将以有效的方式在负载中传递到客户端。
负载是通过 useNuxtApp().payload 访问的 JavaScript 对象。它在客户端上用于避免在浏览器中执行代码时重新获取相同的数据。
也就是说,如果你需要使用 SSR 在服务端获取数据时,就需要用到 Nuxt 提供的组合函数。
但一般情况下我们是不需要使用 useAsyncData 的,除非是 CMS 或第三方提供自己的查询层时。

useFetch 功能简述

这个可组合函数提供了一个方便的封装,包装了useAsyncData$fetch。它根据 URL 和 fetch 选项自动生成一个键,根据服务器路由提供请求 URL 的类型提示,并推断 API 响应类型。
我们在实际使用时,大概率需要配置一些默认设置,比如基础路径,自定义请求头等,以及错误处理等拦截器的配置。但可惜的是官方文档对这部分并没有详细的介绍。只是简单的展示了一个使用拦截器的例子:
const { data, pending, error, refresh } = await useFetch('/api/auth/login', { onRequest({ request, options }) { // 设置请求头 options.headers = options.headers || {} options.headers.authorization = '...' }, onRequestError({ request, options, error }) { // 处理请求错误 }, onResponse({ request, response, options }) { // 处理响应数据 localStorage.setItem('token', response._data.token) }, onResponseError({ request, response, options }) { // 处理响应错误 } })

useFetch 的封装

在查阅 issue 和 Nuxt 源码定义后,我自行封装了一套组合函数,这里分享给大家。
// /composables/useHttp.ts import type { FetchError, FetchResponse, SearchParameters } from 'ofetch' import { hash } from 'ohash' import type { AsyncData, UseFetchOptions } from '#app' import type { KeysOf, PickFrom } from '#app/composables/asyncData' type UrlType = string | Request | Ref<string | Request> | (() => string | Request) type HttpOption<T> = UseFetchOptions<ResOptions<T>, T, KeysOf<T>, $TSFixed> interface ResOptions<T> { data: T code: number success: boolean detail?: string } function handleError<T>( _method: string | undefined, _response: FetchResponse<ResOptions<T>> & FetchResponse<ResponseType>, ) { // Handle the error } function checkRef(obj: Record<string, any>) { return Object.keys(obj).some(key => isRef(obj[key])) } function fetch<T>(url: UrlType, opts: HttpOption<T>) { // Check the `key` option const { key, params, watch } = opts if (!key && ((params && checkRef(params)) || (watch && checkRef(watch)))) console.error('\x1B[31m%s\x1B[0m %s', '[useHttp] [error]', 'The `key` option is required when `params` or `watch` has ref properties, please set a unique key for the current request.') const options = opts as UseFetchOptions<ResOptions<T>> options.lazy = options.lazy ?? true const { apiBaseUrl } = useRuntimeConfig().public return useFetch<ResOptions<T>>(url, { // Request interception onRequest({ options }) { // Set the base URL options.baseURL = apiBaseUrl // Set the request headers const { $i18n } = useNuxtApp() const locale = $i18n.locale.value options.headers = new Headers(options.headers) options.headers.set('Content-Language', locale) }, // Response interception onResponse(_context) { // Handle the response }, // Error interception onResponseError({ response, options: { method } }) { handleError<T>(method, response) }, // Set the cache key key: key ?? hash(['api-fetch', url, JSON.stringify(options)]), // Merge the options ...options, }) as AsyncData<PickFrom<T, KeysOf<T>>, FetchError<ResOptions<T>> | null> } export const useHttp = { get: <T>(url: UrlType, params?: SearchParameters, option?: HttpOption<T>) => { return fetch<T>(url, { method: 'get', params, ...option }) }, post: <T>(url: UrlType, body?: RequestInit['body'] | Record<string, any>, option?: HttpOption<T>) => { return fetch<T>(url, { method: 'post', body, ...option }) }, put: <T>(url: UrlType, body?: RequestInit['body'] | Record<string, any>, option?: HttpOption<T>) => { return fetch<T>(url, { method: 'put', body, ...option }) }, delete: <T>(url: UrlType, body?: RequestInit['body'] | Record<string, any>, option?: HttpOption<T>) => { return fetch<T>(url, { method: 'delete', body, ...option }) }, }
让我们逐步分析这些代码片段:
首先我们定义了一些 type 和 interface 来约束封装的请求方法参数,这里的类型定义是扒 useFetch 的源码写的(官方这部分使用很多 TypeScript 类型体操,看的让人头疼😬),这里重点提一下 ResOptions,这是我们业务上接口实际返回的数据格式,根据自身情况做调整即可。
接着定义了一个 handleError 的错误处理方法,这里可以做一些通用的错误处理,比如使用全局的消息通知展示错误消息,根据状态码渲染错误页面等。
checkRef 是用于判断对象中是否包含 ref 对象,在参数处理的第一步我们就会用到这个方法。
然后我们封装了一个 fetch 方法,接收两个参数,分别是 url 和 opts,并对类型做了限制,确保其符合 useFetch 方法的参数要求。
然后检查了 key、params 和 watch 几个参数,如果没有手动设置 key,但 params 或 watch 中有 ref 对象时,要进行错误提示。
key 是一个唯一的键,用于确保数据获取可以在请求之间正确去重。如果未提供,将根据使用useAsyncData的静态代码位置生成。
这是官方文档的介绍,由于请求可能在服务端或客户端去发起,如果 params 和 watch 使用了 ref 对象,并且没有设置唯一的 key,会导致客户端和服务端自动生成的 key 不一致,导致数据在客户端重复获取,这种情况下一定要手动设置唯一的 key。
接着我们将 lazy 选项默认值改为了 true,避免页面切换时的阻塞(但要处理数据 loading 显示效果)。
然后调用官方的 useFetch 方法并返回,在请求拦截器中设置了 baseURL 和 自定义请求头 Content-Language,把当前页面的语言传递过去。请求响应错误拦截器中则调用了之前定义的 handleError 方法处理错误。
然后通过 options 生成默认的 key,最后合并传递的 options。
最终,我们通过封装的 fetch 定义了一个 useHttp 可组合项,包含 get、post、put、delete 方法。

API 的封装

我们以一个新闻列表的 API 举例:
// /api/news.ts enum API { NEWS = '/news', } interface NewsDetailModel { content: string createAt: string id: number language: string summary: string title: string titleUrl: string updateAt: string url: string | null } export interface NewsListParams { _limit?: number _page?: number } interface PaginationMeta { count: number limit: number page: number } interface NewsListResponse { items: NewsDetailModel[] meta: PaginationMeta } export async function getNewsList(params?: NewsListParams, option?: HttpOption<NewsListResponse>) { return await useHttp.get<NewsListResponse>(API.NEWS, params, { ...option }) }

API 的使用

<script setup lang="ts"> import { getNewsList } from '~/api/news' const { data } = await getNewsList() </script> <template> <div> {{ data }} </div> </template>
对于使用了响应式参数的情况,需要手动设置 key:
<script setup lang="ts"> import { hash } from 'ohash' import { getNewsList } from '~/api/news' import type { NewsListParams } from '~/api/news' const page = ref(1) const { data, pending, error } = await getNewsList({ _limit: 10, _page: page as unknown as NewsListParams['_page'], }, { key: hash('news_list') }) </script> <template> <div> {{ data }} </div> </template>
到这就结束了,这套封装和接口定义,能够让我们不必重复写一些通用的配置,并且能根据返回数据的类型,智能的提示和限制参数,同时如果某些特殊情况下,需要修改一些默认配置,我们也能手动传递参数进行覆盖。
除了 SSR 使用的 useFetch,在客户端有时候需要根据用户交互进行网络请求,这时候需要用到官方提供的内置库 $fetch,有空我会把 $fetch 的封装也分享一下,感兴趣的朋友可以多关注下更新。