省流:当使用 Nuxt 的 ssr 时,onMounted 中使用 useFetch 会出现不调用接口的情况,有两种方式解决这个问题。第一种,使用 $fetch 替代 useFetch;第二种,不在 onMounted 中获取数据,将 useFetch 放到 setup 下。
近日在使用 Nuxt 写一个企业官网,由于是新接触 Nuxt 和 ssr ,所以有些隐藏规则不懂,在看官方文档时也没有留意,比如在 onMounted 调用 useFetch。
老规矩,先说问题,再讨论原因,然后解决它。
问题复现:
开发模式下,启动服务后访问链接为 http://localhost:3000 , 导航栏上有个链接地址为 http://localhost:3000/goods,goods上有一个请求 loadData(使用了 useFetch) 是在 onMounted 中调用的,我遇到的问题是从 localhost:3000 的导航栏点进 goods 页面时,接口调用正常,而在 localhost:3000/goods 点击浏览器刷新时,接口却不调用了
问题分析:
观察控制台输出发现这样一段话,
[nuxt] [useFetch] Component is already mounted, please use $fetch instead. See nuxt.com/docs/gettin…
它想告诉我们的就是,在 onMounted 内请使用 $fetch 代替 useFetch,打开提示中的链接,能找到这样一段话
The useFetch composable is meant to be invoked in setup method or called directly at the top level of a function in lifecycle hooks, otherwise you should use $fetch method.
按照这个提示我们替换一下代码
// 原来代码
onMounted(() => {
loadGoods()
})
const loadGoods = async () => {
const data = await useFetch(url)
list.value = data.value
}
// 更改之后代码
onMounted(() => {
loadGoods()
})
const loadGoods = async () => {
const data = await $fetch(url)
list.value = data.value
}
运行一下,刷新后接口正常调用了,这样果然可以解决问题,那么既然 onMounted 内不让调用,我是不是可以把 loadGoods 放到外面调用呢,我尝试了一下,也是可以的。
// 原来代码
onMounted(() => {
loadGoods();
});
const loadGoods = async () => {
const data = await useFetch(url);
list.value = data.value;
};
// 更改之后代码
onMounted(() => {});
const loadGoods = async () => {
const data = await $fetch(url);
list.value = data.value;
};
await loadGoods();
那么到现在我们知道了,onMounted 不要和 useFetch 一起用,可以和 $fetch 一起用。
可是onMounted 和 useFetch 一起使用为什么刷新不调用接口呢?既然可能出现问题,Nuxt 为什么还要费力使用 $fetch 封装一个useFetch 呢?
带着这两个问题,我们来看看 Nuxt 关于 useFetch 的一点代码,已隐去无关代码
// nuxt/packages/nuxt/src/app/composables/fetch.ts
export function useFetch<
ResT = void,
ErrorT = FetchError,
ReqT extends NitroFetchRequest = NitroFetchRequest,
Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>,
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
arg1?: string | UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>,
arg2?: string,
) {
......
const asyncData = useAsyncData<_ResT, ErrorT, DataT, PickKeys, DefaultT>(key, () => {
......
}, _asyncDataOptions)
return asyncData
}
// nuxt/packages/nuxt/src/app/composables/asyncData.ts
export function useAsyncData<
ResT,
NuxtErrorDataT = unknown,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null> {
......
if (import.meta.client) {
// Setup hook callbacks once per instance
const instance = getCurrentInstance()
......
if (instance && !instance._nuxtOnBeforeMountCbs) {
instance._nuxtOnBeforeMountCbs = []
const cbs = instance._nuxtOnBeforeMountCbs
onBeforeMount(() => {
cbs.forEach((cb) => { cb() })
cbs.splice(0, cbs.length)
})
onUnmounted(() => cbs.splice(0, cbs.length))
}
if (fetchOnServer && nuxtApp.isHydrating && (asyncData.error.value || hasCachedData())) {
// 1. Hydration (server: true): no fetch
......
} else if (instance && ((nuxtApp.payload.serverRendered && nuxtApp.isHydrating) || options.lazy) && options.immediate) {
// 2. Initial load (server: false): fetch on mounted
// 3. Initial load or navigation (lazy: true): fetch on mounted
instance._nuxtOnBeforeMountCbs.push(initialFetch)
} else if (options.immediate) {
// 4. Navigation (lazy: false) - or plugin usage: await fetch
initialFetch()
}
......
}
......
}
}
用自然语言解释一下代码,useFetch执行会调用asyncData,这个方法里有一个关键的判断 nuxtApp.payload.serverRendered && nuxtApp.isHydrating,在刷新的时候 isHydrating 是true,进入代码instance._nuxtOnBeforeMountCbs.push(initialFetch),而这个方法是将initalFetch放入beforeMount回调。从链接点击isHydrating是false,代码会进入下一个分支 initialFetch()。这也就是产生我们前面提到的问题的原因。
至此,我们从代码角度看到了为什么我们不能在onMounted 中使用 useFetch 获取数据。
接下来我们看看为什么会有useFetch。
首先 useFetch 是对 useAsyncData 和 $fetch 的组合式api封装,其封装提供了几个功能,第一是针对url做的监听自动获取数据,第二是服务端渲染时提供了缓存,缓存内容可以从 useNuxtApp().payload中获取,缓存的规则是以调用提供的第一个string参数(不传key时根据url和option生成)作为key,经过 handler, transform 和 pick 处理过的返回内容做value存到payload中,这段代码同样在 useAsyncData.ts 中,在 asyncData.refresh 里可以找到。
export function useAsyncData<
ResT,
NuxtErrorDataT = unknown,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null> {
......
asyncData.refresh = asyncData.execute = (opts = {}) => {
......
const promise = new Promise<ResT>(
(resolve, reject) => {
try {
resolve(handler(nuxtApp))
} catch (err) {
reject(err)
}
})
.then(async (_result) => {
// If this request is cancelled, resolve to the latest request.
if ((promise as any).cancelled) { return nuxtApp._asyncDataPromises[key] }
let result = _result as unknown as DataT
if (options.transform) {
result = await options.transform(_result)
}
if (options.pick) {
result = pick(result as any, options.pick) as DataT
}
// 缓存数据到 nuxtApp 的 payload 中
nuxtApp.payload.data[key] = result
asyncData.data.value = result
asyncData.error.value = null
asyncData.status.value = 'success'
})
.catch((error: any) => {
......
})
.finally(() => {
......
})
nuxtApp._asyncDataPromises[key] = promise
return nuxtApp._asyncDataPromises[key]!
}
asyncData.clear = () => clearNuxtDataByKey(nuxtApp, key)
const initialFetch = () => asyncData.refresh({ _initial: true })
const fetchOnServer = options.server !== false && nuxtApp.payload.serverRendered
// Server side
......
// Allow directly awaiting on asyncData
const asyncDataPromise = Promise.resolve(nuxtApp._asyncDataPromises[key]).then(() => asyncData) as AsyncData<ResT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
Object.assign(asyncDataPromise, asyncData)
return asyncDataPromise as AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
}
最后,进行一下总结:
- 从使用上,onMounted 和 useFetch 不可以一起使用,可以将 useFetch 放到 setup 中,或者在 onMounted 使用 $fetch 替代 useFetch。
- 由于在 client 中 hydration 进行时 useFetch 会将请求放入到 _nuxtOnBeforeMountCbs 中,这使得 useFetch 会在 beforeMount 的回调中进行调用,这也是在刷新页面时不能 onMounted 中的 useFetch 不能调用接口的原因。
- useFetch 作为 Nuxt 中的 composable api,其优越性在于可以缓存请求服务端的数据,并可监听 url 变化来获取数据。