最近在使用 Nuxt3 提供的 useFetch 方法时,第一次请求可以正常收发,之后同一个请求就不发送了,所以看了一下 useFetch 的实现,发现是在特定场景下,命中了 nuxt 接口数据缓存。
正常情况:
刷新页面后,命中缓存:
先看下 useFetch 的 API:
function useFetch(
url: string | Request | Ref<string | Request> | () => string | Request,
options?: UseFetchOptions<DataT>
): Promise<AsyncData<DataT>>
type UseFetchOptions = {
key?: string
method?: string
query?: SearchParams
params?: SearchParams
body?: RequestInit['body'] | Record<string, any>
headers?: Record<string, string> | [key: string, value: string][] | Headers
baseURL?: string
server?: boolean
lazy?: boolean
immediate?: boolean
default?: () => DataT
transform?: (input: DataT) => DataT
pick?: string[]
watch?: WatchSource[]
}
type AsyncData<DataT> = {
data: Ref<DataT>
pending: Ref<boolean>
refresh: (opts?: { dedupe?: boolean }) => Promise<void>
execute: () => Promise<void>
error: Ref<Error | boolean>
}
useFetch
是对 useAsyncData
和 $fetch
的封装,默认情况下,如果 url 或者 options 是响应式的数据,会自动触发请求,更新请求结果。
这会带来一些方便,比如常见的列表搜索场景,只需将筛选条件变成响应式数据,作为 options 调用 useFetch 即可。
不过,有些场景下可能不适用,比如需要组合几个筛选条件再去查询,就可能在每个条件变化的时候多触发了几次查询请求。
useFetch 还可以通过 options 的 server
字段方便的实现 SSR。
接口数据缓存
问题出现:
商品加购请求 /cart/{id}
支持重复发送,测试过程中发现,第一次发送加购请求,收到响应之后,后续同一商品加购请求不会再次发出了。单步调试发现,命中了nuxt内部的接口数据缓存。
useFetch 显示调用以及监听到 url、options 变化,都是调用 refresh
方法,只是_initial
参数不同。
简化后的源代码:
asyncData.refresh = asyncData.execute = (opts = {}) => {
// Avoid fetching same key that is already fetched
if (opts._initial && hasCachedData()) {
return getCachedData()
}
asyncData.pending.value = true
const promise = new Promise<ResT>(
(resolve, reject) => {
try {
resolve(handler(nuxt))
} catch (err) {
reject(err)
}
})
.then((_result) => {
asyncData.data.value = result
asyncData.error.value = null
})
.catch((error: any) => {
asyncData.error.value = error
asyncData.data.value = unref(options.default?.() ?? null)
})
.finally(() => {
asyncData.pending.value = false
nuxt.payload.data[key] = asyncData.data.value
}
})
nuxt._asyncDataPromises[key] = promise
return nuxt._asyncDataPromises[key]
}
可以看到,接口请求之后,在 finally 中更新了 nuxt.payload.data
,
nuxt.payload.data[key] = asyncData.data.value
下一次请求,首先判断 nuxt.payload.data
有没有缓存,有缓存不再重复请求
if (opts._initial && hasCachedData()) {
return getCachedData()
}
const getCachedData = () => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]
命中缓存的条件
1. opts._initial === true
- client端主动触发的请求 _initial 的值都是 ture
- watch监听到依赖变化和
app:data:refresh
hook 触发的请求 _initial 为 false,不会命中缓存
2. nuxt.isHydrating === true
创建nuxt实例:
function createNuxtApp (options: CreateOptions) {
let hydratingCount = 0
const nuxtApp: NuxtApp = {
isHydrating: process.client,
deferHydration () {
if (!nuxtApp.isHydrating) { return () => {} }
hydratingCount++
let called = false
// deferHydration返回方法,后面统一叫做 cb
return () => {
if (called) { return }
called = true
hydratingCount--
if (hydratingCount === 0) {
nuxtApp.isHydrating = false
return nuxtApp.callHook('app:suspense:resolve')
}
}
}
}
return nuxtApp
}
client端应用初始化时,isHydrating 为 ture,调用 deferHydration 的返回方法 cb 才可以将 isHydrating 的值置为 false,有两个时机:
page:finish
hook 的回调方法- nuxt-root
Suspense
的 resolve 回调方法<Suspense @resolve="nuxtApp.deferHydration()"> <ErrorComponent v-if="error" :error="error" /> <IslandRenderer v-else-if="islandContext" :context="islandContext" /> <component :is="SingleRenderer" v-else-if="SingleRenderer" /> <AppComponent v-else /> </Suspense>
3. nuxt.payload.data[key]
通过 key 查询到接口数据缓存
isHydrating 为 true 时,请求就会通过 key 去查接口数据缓存
a unique key to ensure that data fetching can be properly de-duplicated across requests, if not provided, it will be generated based on the static code location where useAsyncData is used.
key如果不传的话,会默认生成一个:
function useFetch(request, arg1, arg2 ) {
const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2]
const _key = opts.key || hash([autoKey, unref(opts.baseURL), typeof request === 'string' ? request : '', unref(opts.params || opts.query)])
if (!_key || typeof _key !== 'string') {
throw new TypeError('[nuxt] [useFetch] key must be a string: ' + _key)
}
const key = _key === autoKey ? '$f' + _key : _key
// ...
}
arg2
是一个默认的 hash,也就是上面代码中的 autoKey,在 nuxt 的插件中被自动生成,实际就是通过 AST 找到 useFetch,然后根据一系列的规则,生成这样一个 hash,作为 useFetch 的第三个参数
具体的生成规则感兴趣的朋友可以自行翻阅
packages/vite/src/plugins/composable-keys.ts
查看。
_key
的优先级:options.key > 通过 autoKey,opts.baseURL,opts.params,opts.query 等计算出的 hash
正常流程
渲染 <AppComponent />
(即 nuxt-root ) -> 渲染 <NuxtPage />
-> 执行 layout supsense 的 resolve cb -> 执行 NuxtPage 回调 cb ( Suspense 的 reslove 事件)
由于 cb 是个闭包,第一次执行时 hydratingCount === 2,执行两次之后 hydratingCount === 0,所以会将 isHydrating 置为 false,client端同构完成。后续路由发生变化,重新渲染 NuxtPage,执行 deferHydration,由于 isHydrating 为 false, cb 是一个空方法,不会再更新 isHydrating, 就不会命中缓存;
即开启服务端渲染时,server 端将接口数据同步到 nuxt.payload.data 中,client 端初次渲染时,不再重复请求;后续 update 流程才去正常请求;
异常流程
- url 和 options 都不是 响应式数据,且请求都是用户手动触发的,满足 _initial === true
- 某一个页面作为首屏或者刷新时,在 setup 或者 watch immediate 中使用 useRouter().replace 改变了 query 参数,触发 NuxtPage 重新渲染,这时 hydratingCount 会累加;RouterView 匹配到同一个组件,Suspense resolve事件只执行一次,hydratingCount 没有减小到 0,nuxtApp.isHydrating 不会更新,还是 true
- options 中没有提供 key,由 nuxt 自动生成,由于 opts.baseURL,opts.params,opts.query 都没有提供,所以都是 undefined,这样每次计算出来的 _key 都是同一个;这样第一次
nuxt.payload.data[key]
为空,拿到响应赋值后,nuxt.payload.data[key]
就会被赋值
三个条件都满足,所有就命中了 nuxt 的接口数据缓存
清除接口数据缓存
-
如果接口的传参不变,但是需要实时请求,可以在useFetch option 传入
key
,请求响应使用后手动调用clearNuxtData(${mykey});
,将数据从nuxt.payload.data
中清除。 -
通过上一步,可以把当前请求的缓存清除,但是只是针对这一个请求,其他请求还是会有同样的问题,可以手动将缓存清空或者
isHydrating
设置为 false -
另一个方法是在
onMounted
钩子中改变路由,也可以避免缓存问题