Nuxt3的UseFetch源码分析

前言

大家好,我是XMJ。最近一直在研究 Nuxt 相关的东西,为什么我要研究和写这些东西?我在搭建Nuxt相关的项目,使用的也是Nuxt3的版本,在搭建过程中遇到的很多问题去网上搜索总是找不到令人满意的解决办法,官网的东西也不是那么的全面,关于Nuxt3版本的技术文章也少的可怜,我一向觉得技术不能只是止步于能用就好,如果不对一项技术有足够的了解,在遇到问题的时候很难快速找到解决办法。今天就好好剖析一下usefetch这个官方推荐使用的用于数据请求的方法。

源码剖析

源码位置:github.com/nuxt/nuxt/b...

我把最主要的定义 useFetch的源码放在下面,通过注释的方式仔细说明:

js 复制代码
export function useFetch<
  ResT = void,//响应数据的类型,默认值为 void
  ErrorT = FetchError,//错误类型
  ReqT extends NitroFetchRequest = NitroFetchRequest,// 请求类型,ReqT 是 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,// 根据 ResT 类型来确定的类型。如果 ResT 为 void,那么 _ResT 为 FetchResult<ReqT, Method>,否则 _ResT 就是 ResT 本身。
  DataT = _ResT,//数据类型
  PickKeys extends KeysOf<DataT> = KeysOf<DataT>,从 DataT 中选择的键的类型,它是 DataT 的键的类型
  DefaultT = undefined,//默认值类型
> (
  request: Ref<ReqT> | ReqT | (() => ReqT),//-   `Ref<ReqT>`:这是一个响应式的引用对象,用于存储请求类型 `ReqT` 的值。`ReqT`:直接表示请求的类型,可以是一个包含请求相关信息的对象或其他数据结构。 `(() => ReqT)`:一个函数,当调用这个函数时,会返回一个 `ReqT` 类型的请求对象。这样可以根据需要动态地生成请求对象。
  arg1?: string | UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>,// 可选参数
  arg2?: string,// 可选参数
) 
{
  const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2]
  
// 这里使用了`computed`函数创建了一个计算属性`_request`。`computed`函数会根据其内部的函数来计算值,并在依赖的值发生变化时自动更新。这里的内部的函数是`() => toValue(request)`,它会将`request`的值通过`toValue`函数进行处理后返回。
  const _request = computed(() => toValue(request))

  const _key = opts.key || hash([autoKey, typeof _request.value === 'string' ? _request.value : '', ...generateOptionSegments(opts)])
  if (!_key || typeof _key !== 'string') {
    throw new TypeError('[nuxt] [useFetch] key must be a string: ' + _key)
  }
 // 这段代码整体上是为 `useFetch` 函数生成一个用于标识和缓存的键值 `_key` ,并进行一些有效性检查。目的是为了确保 `useFetch` 函数在进行数据获取操作时,能够有一个有效的键值用于缓存管理和标识请求,同时保证键值的类型符合预期,以避免后续可能出现的问题。
  if (!request) {
    throw new Error('[nuxt] [useFetch] request is missing.')
  }

  const key = _key === autoKey ? '$f' + _key : _key

  if (!opts.baseURL && typeof _request.value === 'string' && (_request.value[0] === '/' && _request.value[1] === '/')) {
    throw new Error('[nuxt] [useFetch] the request URL must not start with "//".')
  }
const {
    server,//指示是否在服务器端执行请求。如果设置为 `true`,则在服务器端执行请求;如果设置为 `false`,则在客户端执行请求。
    lazy,//指示是否延迟执行请求。如果设置为 `true`,则请求将在组件加载后才执行;如果设置为 `false`,则立即执行请求。
    default: defaultFn,//默认值函数,用于在请求失败或无数据时提供默认值。
    transform,//用于对响应数据进行转换的函数。您可以在这里处理响应数据,例如提取所需的字段或进行其他操作。
    pick,//数组类型,指定从响应数据中选择的键。只有这些键的数据将被保留
    watch,//这也是一个数组,包含要监视的选项。当这些选项的值发生变化时,将重新执行请求。
    immediate,//是否在组件加载时立即执行请求
    getCachedData,//一个函数,用于获取缓存的数据。如果请求已经被缓存,将使用此函数返回的数据。
    deep,//是否深度监听选项。如果设置为 `true`,则会深度监听选项的变化。
    dedupe,//指示是否去重请求。如果设置为 `true`,则相同的请求不会重复执行。
    ...fetchOptions
  } = opts
  
//这里是将默认的配置和传入的选项做响应式处理
  const _fetchOptions = reactive({
    ...fetchDefaults,
    ...fetchOptions,
    cache: typeof opts.cache === 'boolean' ? undefined : opts.cache,
  })

  const _asyncDataOptions: AsyncDataOptions<_ResT, DataT, PickKeys, DefaultT> = {
    server,//指示是否在服务器端执行请求。如果设置为 `true`,则在服务器端执行请求;如果设置为 `false`,则在客户端执行请求。
    lazy,//指示是否延迟执行请求。如果设置为 `true`,则请求将在组件加载后才执行;如果设置为 `false`,则立即执行请求。
    default: defaultFn,// 默认值函数,用于在请求失败或无数据时提供默认值。
    transform,//用于对响应数据进行转换的函数。可以在这里处理响应数据,例如提取所需的字段或进行其他操作。
    pick,//指定从响应数据中选择的键。只有这些键的数据将被保留。
    immediate,//指示是否在组件加载时立即执行请求。
    getCachedData,//一个函数,用于获取缓存的数据。如果请求已经被缓存,将使用此函数返回的数据。
    deep,//是否深度监听选项。如果设置为 `true`,则会深度监听选项的变化。
    dedupe,//指示是否去重请求。如果设置为 `true`,则相同的请求不会重复执行。
    watch: watch === false ? [] : [_fetchOptions, _request, ...(watch || [])],
  } if (import.meta.dev && import.meta.client) {
    // @ts-expect-error private property
    _asyncDataOptions._functionName = opts._functionName || 'useFetch'
  }//一个数组,包含要监视的选项。当这些选项的值发生变化时,将重新执行请求。

   //声明了一个名为 `controller` 的变量,用于中止请求。
  let controller: AbortController
    //这里使用了useAsyncData这个api
  const asyncData = useAsyncData<_ResT, ErrorT, DataT, PickKeys, DefaultT>(key, () => {
    //创建一个 `AbortController` 实例,用于中止当前请求。
    controller?.abort?.()
    controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController
    //设置超时机制,以便在超时后中止请求。
    const timeoutLength = toValue(opts.timeout)
    let timeoutId: NodeJS.Timeout
    if (timeoutLength) {
      timeoutId = setTimeout(() => controller.abort(), timeoutLength)
      controller.signal.onabort = () => clearTimeout(timeoutId)
    }
    //根据用户提供的选项或全局 `$fetch` 函数,获取实际的 fetch 函数。
    let _$fetch = opts.$fetch || globalThis.$fetch

    // Use fetch with request context and headers for server direct API calls
    if (import.meta.server && !opts.$fetch) {
      const isLocalFetch = typeof _request.value === 'string' && _request.value[0] === '/' && (!toValue(opts.baseURL) || toValue(opts.baseURL)![0] === '/')
      if (isLocalFetch) {
        _$fetch = useRequestFetch()
      }
    }
    执行请求,并在请求完成后清除超时计时器。返回一个 `Promise<_ResT>`,其中包含获取的数据。
    return _$fetch(_request.value, { signal: controller.signal, ..._fetchOptions } as any).finally(() => { clearTimeout(timeoutId) }) as Promise<_ResT>
  }, _asyncDataOptions)
   //最终返回一个响应式的asyncData
  return asyncData
}

通过源码分析得出结论, usefetch 中使用了 useAsyncData 和 $fetch函数来进行封装。定义了一个 usefetch 函数,接收请求配置、选项传入,计算一个唯一的key值来缓存数据和防止重复请求,然后处理各种选项,例如服务器端执行、延迟执行、默认值、数据转换等,在异步数据获取的过程中,它中止之前的请求、设置超时机制,并根据用户提供的选项执行实际的请求,最后,返回一个响应式的数据对象。

使用useFetch

最简单的应用方法:

js 复制代码
    const { data: pictureData, pending, error } = useFetch(
    '/user/count',
    {
      immediate: true,//设置立即执行
      query: pictureQuery.value,//传入请求参数
      baseURL: 'https://xxx.examples.com',//请求baseUrl
      default: () => ({
        list: [],
        total: 0,
      }),//设置默认值,如果请求失败或没有数据,将返回一个空的 `content_list` 和总数为 0
      transform(
        res: ResResult<{
          total: number;
          list: Array<any>;
        }>,//
      ) {
        return {
          ...res.data,
        };
      },
    },
  );
    if (pending.value) {
      // 处理加载中情况,可以添加加载样式
    } else if (error.value) {
      // 处理错误
    } else {
      // 这里可以使用pictureData做逻辑
    }

pictureQuery 发生变化时,useFetch 会自动重新发起请求并更新 pictureData。这是因为 useFetch 是响应式的,它会监听 pictureQuery 的变化并触发相应的操作。因此,不需要手动处理数据更新,Nuxt 3 会自动帮你处理。

结论分析

如果仔细研读上面的代码分析,大概也就清楚了useFetch的封装是什么样的,但是肯定会疑惑为什么传入useFetch的参数opts在源码中列了那么多属性,为什么使用的时候只用了几。这是因为 useAsyncData 已经做了处理了,截取 useAsyncData 的一部分代码:

js 复制代码
 const getDefault = () => undefined
  const getDefaultCachedData = () => nuxtApp.isHydrating ? nuxtApp.payload.data[key] : nuxtApp.static.data[key]

  // Apply defaults
  options.server = options.server ?? true
  options.default = options.default ?? (getDefault as () => DefaultT)
  options.getCachedData = options.getCachedData ?? getDefaultCachedData

  options.lazy = options.lazy ?? false
  options.immediate = options.immediate ?? true
  options.deep = options.deep ?? asyncDataDefaults.deep
  options.dedupe = options.dedupe ?? 'cancel'

数据请求这边则由 $fetch负责了,所以官方提供的useFetch的功能还是比较完善了,直接使用也没有问题。如果需要配置自定义的请求头,或者参数扩展,加入Token等等操作,那就需要再对useFetch在做一层封装了,这个就要看各位项目中具体的需求了,如果还有什么不解的可以再去看一下 useAsyncData 和 $fetch的实现源码,这里就不再多做赘述了。

最后,如果大家看了有一定的收获,还烦请点点赞哈哈哈,写东西的动力也需要得到读者的响应,有任何不对的地方欢迎大佬前来指正,交流学习!!

相关推荐
卸任13 小时前
Vibe coding工具使用心得
前端·ai编程
brien913 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance14 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M14 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写14 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李14 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易14 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马14 小时前
前沿视界
前端
kyriewen14 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易14 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员