Nuxt之当useFetch遇上onMounted

3,734 阅读2分钟

省流:当使用 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…

Untitled.png

它想告诉我们的就是,在 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>)>
}

最后,进行一下总结:

  1. 从使用上,onMounted 和 useFetch 不可以一起使用,可以将 useFetch 放到 setup 中,或者在 onMounted 使用 $fetch 替代 useFetch。
  2. 由于在 client 中 hydration 进行时 useFetch 会将请求放入到  _nuxtOnBeforeMountCbs 中,这使得 useFetch 会在 beforeMount 的回调中进行调用,这也是在刷新页面时不能 onMounted 中的 useFetch 不能调用接口的原因。
  3. useFetch 作为 Nuxt 中的 composable api,其优越性在于可以缓存请求服务端的数据,并可监听 url 变化来获取数据。