useFetch 和 useAsyncData 共享一组常见的选项和模式。
使用像 Nuxt 这样的框架可以在客户端和服务器环境中执行调用和呈现页面时,必须解决一些问题。这就是为什么 Nuxt 提供了组合函数来封装查询,而不是让开发者仅依赖于 $fetch 调用。
这个可组合函数提供了一个方便的封装,包装了 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 的封装
// /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 }) }, }让我们逐步分析这些代码片段:
最终,我们通过封装的 fetch 定义了一个 useHttp 可组合项,包含 get、post、put、delete 方法。
// /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 的封装也分享一下,感兴趣的朋友可以多关注下更新。