Skip to content

自定义插件

Vue3Request 提供了强大而灵活的插件系统,允许你根据业务需求开发自定义插件,扩展 useRequest 的功能。通过插件机制,你可以在请求的各个生命周期阶段注入自定义逻辑。

插件结构

一个 Vue3Request 插件本质上是一个函数,接收 requestInstanceoptions 参数,返回一个包含生命周期钩子的对象:

插件作为useRequest的第三个参数,需要传递一个数组,数组里的插件执行顺序采用洋葱模型(倒序执行)

你可以使用 Vue3Request 导出的 definePlugin 方法来定义插件,此方法具有丰富的 TS 类型提示

ts
const definePlugin: <D = any, P extends any[] = any, O = {}>(
  plugin: Plugin<D, P, O>
) => Plugin<D, P, O>;

泛型 Ddata 的类型,即 requestInstance.state.data;泛型 Pparams 的类型,即 requestInstance.state.paramsoptions.defaultParams;泛型 O 用来扩展 options 对象的属性

详细说明请参考 API 文档 - definePlugin

ts
import { useRequest, definePlugin } from "vue3-request";

interface IResult {
  code: number;
  msg: string;
  data: {
    name: string;
    age: number;
  };
}

const service = ({ id }: { id: number }): Promise<IResult> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        code: 200,
        msg: "success",
        data: {
          name: "zs",
          age: 24,
        },
      });
    }, 1000);
  });
};

interface IPlugin {
  level: string;
}

const customPlugin = definePlugin<IResult, [{ id: number }], IPlugin>(
(requestInstance, options) => {
    // 插件初始化逻辑

    return {
      onBefore: (params) => {
        // 请求前执行,对应 options.onBefore
      },
      onRequest: (service) => {
        // 请求时执行,可以修改 service
        // 必须返回一个新的 service 函数
        return service;
      },
      onSuccess: (params) => {
        // 请求成功时执行,对应 options.onSuccess
        // 如需访问响应数据,使用 requestInstance.state.data
        console.log('响应数据:', requestInstance.state.data);
      },
      onError: (params) => {
        // 请求失败时执行,对应 options.onError
        // 如需访问错误信息,使用 requestInstance.state.error
        console.log('错误信息:', requestInstance.state.error);
      },
      onFinally: (params) => {
        // 请求完成时执行(无论成功或失败),对应 options.onFinally
        // 可以同时访问 data 和 error
        const { data, error } = requestInstance.state;
        if (data) {
          console.log('请求成功');
        }
        if (error) {
          console.log('请求失败');
        }
      },
      onCancel: () => {
        // 请求取消时执行,对应 cancel() 方法
      },
    };
  }
);
const { data, loading } = useRequest(
  service,
  {
    level: "1", 
  },
  [customPlugin] 
);

实战示例

请求日志插件

TIP

打开控制台查看日志打印

更多实用插件示例

下面这些插件都只依赖 Vue3Request 的生命周期,不绑定 Axios、Fetch 或具体 UI 组件,适合直接放到项目的 src/plugins/request 目录中复用。

合并重复请求(Single Flight)

同一个接口可能被多个组件同时触发,例如页面初始化时,Header、Sidebar 和页面主体都请求当前用户。这个插件让相同 key 的请求共享同一个 Promise,避免短时间内重复访问后端。

ts
import { definePlugin } from "vue3-request";

const pendingRequests = new Map<string, Promise<unknown>>();

interface DedupeOptions<P extends unknown[]> {
  dedupeKey?: (params: P) => string;
}

export function createDedupePlugin<D, P extends unknown[]>() {
  return definePlugin<D, P, DedupeOptions<P>>(
    (requestInstance, options) => ({
      onRequest: (service) => async (...params) => {
        if (!options.dedupeKey) return service(...params);

        const key = options.dedupeKey(requestInstance.state.params);
        const pending = pendingRequests.get(key) as Promise<D> | undefined;
        if (pending) return pending;

        const current = service(...params);
        pendingRequests.set(key, current);

        try {
          return await current;
        } finally {
          if (pendingRequests.get(key) === current) {
            pendingRequests.delete(key);
          }
        }
      },
    })
  );
}
ts
type UserParams = [id: string];
const dedupePlugin = createDedupePlugin<User, UserParams>();

useRequest(getUser, {
  dedupeKey: ([id]) => `user:${id}`,
}, [dedupePlugin]);

key 中应包含接口身份和全部有效参数。这里只合并进行中的请求;请求结束后的数据复用仍交给内置缓存插件处理。

Token 失效后刷新并重放

多个请求同时收到 401 时,通常只应刷新一次 Token,其余请求等待刷新完成后再重放。模块级的 refreshingToken 用于避免并发刷新。

ts
import { definePlugin } from "vue3-request";

let refreshingToken: Promise<void> | null = null;

interface AuthRefreshOptions {
  isUnauthorized?: (error: unknown) => boolean;
  refreshToken?: () => Promise<void>;
}

export function createAuthRefreshPlugin<D, P extends unknown[]>() {
  return definePlugin<D, P, AuthRefreshOptions>((_request, options) => ({
    onRequest: (service) => async (...params) => {
      try {
        return await service(...params);
      } catch (error) {
        if (
          !options.refreshToken ||
          !options.isUnauthorized?.(error)
        ) {
          throw error;
        }

        refreshingToken ??= options.refreshToken().finally(() => {
          refreshingToken = null;
        });

        await refreshingToken;
        return service(...params);
      }
    },
  }));
}
ts
const authRefreshPlugin = createAuthRefreshPlugin<Order[], []>();

useRequest(getOrders, {
  isUnauthorized: (error) =>
    axios.isAxiosError(error) && error.response?.status === 401,
  refreshToken: authStore.refreshToken,
}, [authRefreshPlugin]);

重放只执行一次,不会出现无限刷新循环。实际项目中,刷新失败后还应由 authStore 统一清理登录状态并跳转登录页。

限制全局并发数

批量加载图片详情、报表分片或大量下拉选项时,无限制并发容易挤占浏览器连接、触发网关限流。下面的插件让同一插件实例下的请求排队执行。

ts
import { definePlugin } from "vue3-request";

export function createConcurrencyPlugin<
  D,
  P extends unknown[]
>(maxConcurrent: number) {
  if (!Number.isInteger(maxConcurrent) || maxConcurrent <= 0) {
    throw new RangeError("maxConcurrent must be a positive integer");
  }

  let activeCount = 0;
  const queue: Array<() => void> = [];

  const acquire = () => new Promise<void>((resolve) => {
    if (activeCount < maxConcurrent) {
      activeCount += 1;
      resolve();
      return;
    }
    queue.push(() => {
      activeCount += 1;
      resolve();
    });
  });

  const release = () => {
    activeCount -= 1;
    queue.shift()?.();
  };

  return definePlugin<D, P>((_request, _options) => ({
    onRequest: (service) => async (...params) => {
      await acquire();
      try {
        return await service(...params);
      } finally {
        release();
      }
    },
  }));
}
ts
// 在模块顶层创建一次,所有使用它的实例共享最多 4 个并发名额
const reportConcurrencyPlugin =
  createConcurrencyPlugin<ReportChunk, [chunkId: string]>(4);

useRequest(loadReportChunk, {}, [reportConcurrencyPlugin]);

若在每个组件内部重新创建插件,每个组件会拥有独立队列,也就无法实现跨组件限流。

失败时返回降级数据

推荐列表、运营配置等非核心接口失败时,与其让整个页面进入错误态,通常更适合返回本地默认值。该插件只处理明确允许降级的错误。

ts
import { definePlugin } from "vue3-request";

interface FallbackOptions<D, P extends unknown[]> {
  shouldFallback?: (error: unknown) => boolean;
  fallback?: (error: unknown, params: P) => D | Promise<D>;
}

export function createFallbackPlugin<D, P extends unknown[]>() {
  return definePlugin<D, P, FallbackOptions<D, P>>(
    (requestInstance, options) => ({
      onRequest: (service) => async (...params) => {
        try {
          return await service(...params);
        } catch (error) {
          if (
            !options.fallback ||
            !options.shouldFallback?.(error)
          ) {
            throw error;
          }

          return options.fallback(error, requestInstance.state.params);
        }
      },
    })
  );
}
ts
const fallbackPlugin = createFallbackPlugin<Banner[], []>();

useRequest(getBanners, {
  shouldFallback: (error) => !navigator.onLine || isTimeoutError(error),
  fallback: () => localStorageBanners,
}, [fallbackPlugin]);

降级成功后,请求会进入 onSuccess 而不是 onError。因此它适合“默认数据也算有效结果”的非核心接口,不应该用于订单提交、支付等写操作。

这些插件可以组合使用。由于 onRequest 按洋葱模型执行,插件数组中靠前的插件位于外层,可以观察到内层插件处理后的最终结果或错误;生命周期型插件则按照插件数组顺序依次执行。

通过自定义插件,你可以将 useRequest 扩展到任何业务场景,实现高度定制化的异步数据管理解决方案。

贡献者 ☘️

Flame-00

Flame-00

Creator