闽公网安备 35020302035485号
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 的封装也分享一下,感兴趣的朋友可以多关注下更新。