Nuxt3 数据获取介绍
Nuxt 提供了两个组合函数和一个内置库,用于在浏览器或服务器环境中执行数据获取:useFetch
、 useAsyncData
和 $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 响应类型。
我们在实际使用时,大概率需要配置一些默认设置,比如基础路径,自定义请求头等,以及错误处理等拦截器的配置。但可惜的是官方文档对这部分并没有详细的介绍。只是简单的展示了一个使用拦截器的例子:
ts
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 源码定义后,我自行封装了一套组合函数,这里分享给大家。
ts
// /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 举例:
ts
// /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 的使用
ts
<script setup lang="ts">
import { getNewsList } from '~/api/news'
const { data } = await getNewsList()
</script>
<template>
<div>
{{ data }}
</div>
</template>
对于使用了响应式参数的情况,需要手动设置 key:
ts
<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
的封装也分享一下,感兴趣的朋友可以多关注下更新。