ajax、axios、fetch的发展

  • ajax

在传统开发中,使用XMLHttpRequest对象创建的网络请求居多,这种异步技术称之为ajax。后来jQuery封装了它,但是这项技术有一个很大的问题,ajax中需要传回调函数,由于http请求是在异步队列中运行的,请求到的数据需要返回,就需要使用回调函数,但是这种方法就有很大的问题,那就是回调地狱,需要写大量的回调函数。直到后来Promise的出现解决回调地狱的问题,Promise就是为异步而生的.

  • axios

这个时候新的库就出现了——axiosaxios是基于Promiseajax的封装,里面通过了对应的配置和拦截器,分别是请求拦截器和响应拦截器。而node端的axios是对http库的封装

  • fetch fetch 是一种使用 promise 为构建块的现代异步网络请求方法.是当今进行异步网络请求的新标准.除了IE之外,在各大浏览器中的兼容性都还可以,在caniuse上查询fetch的浏览器兼容性,不支持的浏览器可以使用 fetch polyfill.其本质是一种标准,该标准定义了请求,响应和绑定的流程,还定义了Fetch的JavaScript API.而Fetch API 提供了 fetch() 方法.它被定义在BOM的window对象中,返回一个 Promise 对象,因此我们能够对返回的结果进行检索.

什么是useFetch

简单来说useFetch就是对fetch的封装。

为什么不使用axios?

在nuxt2中我们能看见nuxt-axios库,也就是axios对nuxt的封装,但是这种其实是有问题的,刚刚我们说到axios在浏览器端和node端是对ajax和http俩个库的封装,而nuxt是有一个两次渲染的这样在服务端和浏览器端的时候就是俩个不通的库,不利于优化,而fetch与其说是一个新的http库,不如说是一个新标准,在esm中已经将它列入一个标准化了

Nuxt3 数据获取介绍

Nuxt 提供了两个组合函数和一个内置库,用于在浏览器或服务器环境中执行数据获取:useFetchuseAsyncData$fetch

简而言之: useFetch 是在组件设置函数中处理数据获取的最简单方法。

这个可组合函数提供了一个方便的封装,包装了useAsyncData和$fetch。它根据 URL 和 fetch 选项自动生成一个键,根据服务器路由提供请求 URL 的类型提示,并推断 API 响应类型。 $fetch 可以根据用户交互进行网络请求。

useAsyncData 结合 $fetch,提供了更精细的控制。

useFetch 和 useAsyncData 共享一组常见的选项和模式,在后面的章节中我们将详细介绍。

为什么需要特定的组合函数?

使用像 Nuxt 这样的框架可以在客户端和服务器环境中执行调用和呈现页面时,必须解决一些问题。这就是为什么 Nuxt 提供了组合函数来封装查询,而不是让开发者仅依赖于 $fetch 调用。

useFetch封装最佳实践

TypeScript
import type {FetchError, FetchResponse, SearchParameters} from 'ofetch';
import {hash} from 'ohash';
import type {AsyncData, UseFetchOptions} from '#app';
import type {KeysOf} from '#app/composables/asyncData';

type UrlType = string | Request | Ref<string | Request> | (() => string | Request);

type HttpOption<T> = UseFetchOptions<ResOptions<T>, T, KeysOf<T>, any>;
interface ResOptions<T> {
    data: T;
    code: number;
    message: boolean;
    err?: string[];
}

function handleError<T>(
    _method: string | undefined,
    _response: FetchResponse<ResOptions<T>> & FetchResponse<any>,
) {
    // Implement error handling logic here
    console.error(`[useHttp] [error] ${_method}:`, _response);
}

function checkRef(obj: Record<string, any>) {
    return Object.keys(obj).some(key => isRef(obj[key]));
}

function fetch<T>(url: UrlType, opts: HttpOption<T>): AsyncData<ResOptions<T>, FetchError<ResOptions<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 { baseUrl } = useRuntimeConfig().public;

    return useFetch<ResOptions<T>>(url, {
        // Request interception
        onRequest({ options }) {
            options.baseURL = baseUrl;
            // Set the base URL
        },
        // 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({ method: options.method, params: options.params })]),
        // Merge the options
        ...options,
    }) as AsyncData<ResOptions<T>, FetchError<ResOptions<T>>>;
}

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 });
    },
};

这里我们使用到了ohash这个库,你也可以不使用自己定义,如果你和我一样的话你可以安装一下这个库

npm i ohash

即可

代码分析

TypeScript
type UrlType = string | Request | Ref<string | Request> | (() => string | Request);

type HttpOption<T> = UseFetchOptions<ResOptions<T>, T, KeysOf<T>, any>;

interface ResOptions<T> {
    data: T;
    code: number;
    message: boolean;
    err?: string[];
}
  • UrlType 定义了 fetch 函数中 URL 参数的类型,可以是字符串、Request 对象、引用(Ref)或返回上述类型的函数。
  • HttpOption<T> 是对 UseFetchOptions 的扩展,用于配置 HTTP 请求选项。
  • ResOptions<T> 是响应对象的结构,包含 data、code、message和可选的 err字段。 这些类型字段是直接复制官方的源码的,官方ts类型体操实在是太复杂了,就直接复制粘贴了,ResOptions这个类型工具接口返回的数据格式做调整即可,这里只是写了一个示例。
TypeScript
function handleError<T>(
  _method: string | undefined,
  _response: FetchResponse<ResOptions<T>> & FetchResponse<any>,
) {
  // Implement error handling logic here
  console.error(`[useHttp] [error] ${_method}:`, _response);
}
  • handleError 函数用于处理请求错误,当前仅在控制台输出错误信息。实际使用中,可以在这里添加更多的错误处理逻辑。
  • checkRef 是用于判断对象中是否包含 ref 对象
  • 然后检查 key 是否存在,如果 params 或 watch 包含引用属性但没有 key,则输出错误信息
  • key 是一个唯一的键,用于确保数据获取可以在请求之间正确去重。如果未提供,将根据使用useAsyncData的静态代码位置生成。
  • 接着我们将 lazy 选项默认值改为了 true,避免页面切换时的阻塞,这个要根据实际去改掉,这个是解决页面长时间白屏的问题。
  • 设置基础 URL 和请求头,其中包含当前语言环境。
  • 使用 useFetch 函数发送请求,处理请求和响应拦截,并生成缓存键。
  • 返回 AsyncData 对象,包含请求状态和数据。 最终,我们通过封装的 fetch 定义了一个 useHttp 可组合项,包含 get、post、put、delete 方法

api封装示例

TypeScript
interface List{
    id: number;
    name: string;
}
interface List{
    id: number;
    name: string;
}
export const getList = (params?: SearchParameters, option?: HttpOption<List>) => {
    return useHttp.get<List>('/list', params, option);
}

在代码中去使用

TypeScript
<script setup lang="ts">
const {data} = await getList()
</script>

<template>
  <div>
    <UButton label="Open"/>
    <div>
      data: {{ data}}
    </div>
  </div>
</template>