Skip to content
On this page

useFetch

🔗vueuse-useFetch

效果

使用

1. 基本使用

ts
import { useFetch } from '@vueuse/core'
const { isFetching, error, data } = useFetch(url)

异步使用

ts
const { isFetching, error, data } = await useFetch(url)

手动触发

ts
const { execute } = useFetch(url, { immediate: false })
  execute()

请求拦截器

ts
const { data } = useFetch(url, {
  async beforeFetch({ url, options, cancel }) {
    const myToken = await getMyToken()

    if (!myToken)
      cancel()
    options.headers = {
      ...options.headers,
      Authorization: `Bearer ${myToken}`,
    }
    return {
      options,
    }
  },
})

响应拦截器

ts
const { data } = useFetch(url, {
  afterFetch(ctx) {
    if (ctx.data.title === 'HxH')
      ctx.data.title = 'Hunter x Hunter' // Modifies the response data

    return ctx
  },
})

事件

ts
const { data: a, onFetchResponse } = useFetch(url)
  onFetchResponse((response) => {
    response.json().then(res => {
      console.log(res)
    })
  })

思路

合并 defaultConfig 与用户配置和其他细项不再多说,重点是 同步获取数据,请求拦截,响应拦截,事件触发 这四个方面

使用 await 同步请求数据

🚀

await 是一个关键字, 它表示等待一个表达式, 返回一个 Promise 对象

🔥🔥🔥其实可以使用普通函数模拟

ts
function a(x): PromiseLike<any> {
  return {
    then: function (onfulfilled): Promise<any> {
      return Promise.resolve(onfulfilled(x));
    }
  }
}

async function b() {
  return await a(2)
}

let r = b();
r.then(res => {
  console.log(res)
})

能够使用 await 的条件是需要函数 返回一个以 thenkey值,value 为返回一个 Promise 的函数的对象
由于请求是异步方法,需要等到 请求完成之后 才可以触发 Promise 的 resolve / reject 方法,需要 waitUntilFinished 来在合适的时机触发

ts
// 配合使用
// return {
//   then(onFulfilled: any, onRejected: any) {
//     return waitUntilFinished().then(onFulfilled, onRejected);
//   },
// }

function until(r: any, v: boolean) {
  return new Promise<void>((resolve) => {
    watch(
      r,
      () => {
        if (v == r.value) {
          resolve();
        }
      },
      {
        immediate: true,
      }
    );
  });
}

function waitUntilFinished() {
  return new Promise<UseFetchReturn<T>>((resolve, reject) => {
    until(isFinished, true) 
      .then(() => resolve(shell)) 
      .catch((error: Error) => reject(error));
    });
  }

waitUntilFinished 名如其人 等待直到完成

核心是 until 函数, 使用 vue 提供的watch,只有当 unitl两个参数相等时,resolve 才会触发,resolve 触发之后,waitUntilFinished 才会返回 shell

请求拦截/响应拦截

在请求拦截是发生在请求响应之前,可以合并参数,在响应拦截中可以对数据进行处理

ts
if (config.beforeFetch) {
    Object.assign(context, await config.beforeFetch(context));
}

if (config.afterFetch) {
    // 把 json 之后的数据和 没有 json 后的数据 统一传入
   // 返回 数据解构 data
  ({ data: responseData } = await config.afterFetch({
      data: responseData,
      response: fetchResponse,
  }));
}

事件触发

使用了发布订阅创建一个工厂函数 createEventHook

ts
function createEventHook<T = any>(): EventHook<T> {
  const fns: Set<(param: T) => void> = new Set();

  const off = (fn: (param: T) => void) => {
    fns.delete(fn);
  };

  const on = (fn: (param: T) => void) => {
    fns.add(fn);
    // 切面
    const offFn = () => off(fn);
    return {
      off: offFn,
    };
  };

  const trigger = (param: T) => {
    return Promise.all(Array.from(fns).map((fn) => fn(param)));
  };

  return {
    on,
    off,
    trigger,
  };
}

所以使用 createEventHook 创建事件

ts
const responseEvent = createEventHook<Response>();
//  返回给外界
 const shell = {
  onFetchResponse: responseEvent.on,
 }
 // 当数据请求完成之后
 responseEvent.trigger(responseData);

源码

ts
import {
  computed,
  ComputedRef,
  isRef,
  ref,
  Ref,
  shallowRef,
  toRaw,
  watch,
} from "vue";
import { EventHookOn, createEventHook } from "utils/createEventHook";
import {
  BeforeFetchContext,
  DataType,
  fetchConfig,
  HttpMethod,
  UseFetchOptions,
} from "./types";

function containsProp(obj: object, ...props: string[]) {
  return props.some((k) => k in obj);
}

function isFetchOptions(obj: object): obj is UseFetchOptions {
  return (
    obj &&
    containsProp(
      obj,
      "immediate",
      "refetch",
      "initialData",
      "timeout",
      "beforeFetch",
      "afterFetch",
      "onFetchError",
      "fetch"
    )
  );
}

function headersToObject(headers: HeadersInit | undefined) {
  if (typeof Headers !== "undefined" && headers instanceof Headers)
    return Object.fromEntries([...headers.entries()]);
  return headers;
}

const resolveUnref = <T>(val: Ref<T> | T): T => {
  return unref(val);
};

export type MaybeComputedRef<T> = ComputedRef<T> | T | Ref<T>;

interface UseFetchReturn<T> {
  /**
   * Indicates if the fetch request has finished
   */
  isFinished: Ref<boolean>;

  /**
   * The statusCode of the HTTP fetch response
   */
  statusCode: Ref<number | null>;

  /**
   * The raw response of the fetch response
   */
  response: Ref<Response | null>;

  /**
   * Any fetch errors that may have occurred
   */
  error: Ref<any>;

  /**
   * The fetch response body on success, may either be JSON or text
   */
  data: Ref<T | null>;

  /**
   * Indicates if the request is currently being fetched.
   */
  isFetching: Ref<boolean>;

  /**
   * Abort the fetch request
   */
  abort: () => void;
  execute: (throwOnFailed?: boolean) => Promise<any>;
  onFetchResponse: EventHookOn<Response>;

  onFetchError: EventHookOn;
  onFetchFinally: EventHookOn;

  get(): UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>;
  post(
    payload?: MaybeComputedRef<unknown>,
    type?: string
  ): UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>;
  // type
  json<JSON = any>(): UseFetchReturn<JSON> & PromiseLike<UseFetchReturn<JSON>>;
}

const payloadMapping: Record<string, string> = {
  json: "application/json",
  text: "text/plain",
};

export function useFetch<T>(
  url: string,
  args?: Partial<UseFetchOptions>
): UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>> {
  let config: UseFetchOptions & fetchConfig = {
    data: {},
    method: "GET",
    type: "text",
    payload: undefined as unknown,
    immediate: true,
    refetch: false,
  };

  // 与用户 传入的类型进行合并
  if (args && isFetchOptions(args)) {
    config = { ...config, ...args };
  }

  /**
   * @description 是否在 loading 状态
   * @param {boolean} isLoading
   */
  const loading = (isLoading: boolean) => {
    isFetching.value = isLoading;
    isFinished.value = !isLoading;
  };

  let controller: AbortController | undefined;

  /**
   * @description abort 取消请求,在 fetch 的 signal属性 注入controller.signal
   */
  const abort = () => {
    controller?.abort();
    controller = new AbortController();
    controller.signal.onabort = () => (aborted.value = true);
    config = {
      ...config,
      signal: controller.signal,
    };
  };

  /** @type {EventHook<Response>} responseEvent,事件触发 */
  const responseEvent = createEventHook<Response>();
  const errorEvent = createEventHook<any>();
  const finallyEvent = createEventHook<any>();

  const aborted = ref(false);
  const statusCode = ref<number | null>(null);
  const response = shallowRef<Response | null>(null);
  const error = shallowRef<any>(null);

  const data = shallowRef(config.data || {});

  const isFinished = ref(false);
  const isFetching = ref(false);

  let execute = async (throwOnFailed = false) => {
    loading(true);
    error.value = null;
    statusCode.value = null;
    aborted.value = false;
    data.value = null;

    if (config.payload) {
      if (config.payloadType) {
        (config.headers as any)["Content-Type"] =
          payloadMapping[config.payloadType] ?? config.payloadType;
      }

      const payload = resolveUnref(config.payload);

      // 如果 payloadType 时 json 就先 JSON.stringify
      config.body =
        config.payloadType === "json"
          ? JSON.stringify(payload)
          : (payload as BodyInit);
    }

    let isCanceled = false;

    const context: BeforeFetchContext = {
      url: resolveUnref(url),
      options: config,
      cancel: () => {
        isCanceled = true;
      },
    };

    // 把用户的与当前的 context 合并
    if (config.beforeFetch) {
      Object.assign(context, await config.beforeFetch(context));
    }

    // 如果调用了 cancel 函数,就停止
    if (isCanceled || !fetch) {
      loading(false);
      return Promise.resolve(null);
    }

    /** @type {json / text} 格式化后的真实数据 */
    let responseData: any = null;

    return new Promise<Response | null>((resolve, reject) => {
      fetch(context.url, {
        ...context.options,
        headers: {
          ...headersToObject(config.headers),
          ...headersToObject(context.options?.headers),
        },
      })
        .then(async (fetchResponse) => {
          // 保留初始数据
          response.value = fetchResponse;
          statusCode.value = fetchResponse.status;

          responseData = await fetchResponse[config.type]();

          if (!fetchResponse.ok) {
            data.value = config.data || null;
            throw new Error(fetchResponse.statusText);
          }

          if (config.afterFetch) {
            // 把 json 之后的数据和 没有 json 后的数据 统一传入
            // 返回 数据解构  data
            ({ data: responseData } = await config.afterFetch({
              data: responseData,
              response: fetchResponse,
            }));
          }
          data.value = responseData;

          responseEvent.trigger(responseData);
          return resolve(fetchResponse);
        })
        .catch(async (fetchError) => {
          let errorData = fetchError.message || fetchError.name;

          if (config.onFetchError)
            ({ error: errorData } = await config.onFetchError({
              data: responseData,
              error: fetchError,
              response: response.value,
            }));
          error.value = errorData;

          errorEvent.trigger(fetchError);

          if (throwOnFailed) return reject(fetchError);

          return resolve(null);
        })
        .finally(() => {
          loading(false);

          finallyEvent.trigger(null);
        });
    });
  };

  const refetch = ref(config.refetch);

  watch([refetch, ref(url)], ([refetch]) => refetch && execute(), {
    deep: true,
  });

  if (config.immediate) {
    setTimeout(execute, 0);
  }

  const shell: UseFetchReturn<T> = {
    isFinished,
    statusCode,
    response,
    error,
    data,
    isFetching,
    abort,
    execute,
    // 会返回一个 off 事件
    onFetchResponse: responseEvent.on,
    onFetchError: errorEvent.on,
    onFetchFinally: finallyEvent.on,
    // method
    get: setMethod("GET"),
    post: setMethod("POST"),

    // type
    json: setType("json"),
  };

  function setMethod(method: HttpMethod) {
    return (payload?: unknown, payloadType?: string) => {
      if (!isFetching.value) {
        config.method = method;
        config.payload = payload;
        config.payloadType = payloadType;

        // watch for payload changes

        watch(
          () => config.payload,
          () => execute()
        );

        const rawPayload = resolveUnref(config.payload);
        // Set the payload to json type only if it's not provided and a literal object is provided and the object is not `formData`
        // The only case we can deduce the content type and `fetch` can't
        if (
          !payloadType &&
          rawPayload &&
          Object.getPrototypeOf(rawPayload) === Object.prototype &&
          !(rawPayload instanceof FormData)
        )
          config.payloadType = "json";
      }
      return {
        ...shell,
        then(onFulfilled: any) {
          return new Promise(onFulfilled);
        },
      } as any;
    };
  }

  function until(r: any, v: any) {
    return new Promise<void>((resolve) => {
      watch(
        r,
        () => {
          if (v == r.value) {
            resolve();
          }
        },
        {
          immediate: true,
        }
      );
    });
  }

  function waitUntilFinished() {
    return new Promise<UseFetchReturn<T>>((resolve, reject) => {
      until(isFinished, true)
        .then(() => resolve(shell))
        .catch((error: Error) => reject(error));
    });
  }

  function setType(type: DataType) {
    return () => {
      if (!isFetching.value) {
        config.type = type;

        watch(
          () => config.type,
          () => execute()
        );

        return {
          ...shell,
          then(onFulfilled: any, onRejected: any) {
            return waitUntilFinished().then(onFulfilled, onRejected);
          },
        } as any;
      }
      return {
        ...shell,
        then(onFulfilled: any, onRejected: any) {
          return waitUntilFinished().then(onFulfilled, onRejected);
        },
      } as any;
    };
  }
  return {
    ...shell,
    then(onFulfilled, onRejected) {
      return waitUntilFinished().then(onFulfilled, onRejected);
    },
  };
}