Skip to content

只给需要改的代码片段;复杂逻辑我都在上一行加了中文注释。以下改动基于你贴的这份 http 封装。

1) 请求可“集中取消”& 退出后不再飞行

位置:请求拦截器内(_axios.interceptors.request.use)头部插入

ts
// 复杂逻辑:为每个请求创建可集中取消的 AbortController,并桥接外部 signal
const ac = new AbortController();
if (req.signal) {
  const ext = req.signal as AbortSignal;
  if (ext.aborted) ac.abort();
  else ext.addEventListener?.("abort", () => ac.abort());
}
req.signal = ac.signal;
abortPool.add(ac);
// 复杂逻辑:把本次请求的 controller 暂存到 config 上,方便响应阶段释放
(req as any).__ac = ac;

位置:响应成功拦截器(res 分支)里,return res 之前插入

ts
// 复杂逻辑:请求完成后,从取消池中释放 controller
const ac = (res.config as any).__ac as AbortController | undefined;
if (ac) abortPool.delete(ac);

位置:响应失败拦截器(err 分支)里,return Promise.reject(err) 之前插入

ts
// 复杂逻辑:请求失败同样释放 controller,避免池子泄漏
const ac = (err?.config as any)?.__ac as AbortController | undefined;
if (ac) abortPool.delete(ac);

这样就和你现有的 cancelAll() 打通了:登出时执行 cancelAll(),所有在途请求立刻中止。


2) “未登录硬拦”鉴权请求(默认需要鉴权)

位置:请求拦截器内,给你现有的加 Token 逻辑改为如下**

ts
// 复杂逻辑:统一鉴权开关——默认需要鉴权,白名单或显式 { auth:false } 不需要
const needAuth =
  (req as any).auth !== false && !noAuthPaths.includes(req.url || "");

// 复杂逻辑:无 Token 且需要鉴权时,直接阻断请求,避免未登录拉数据
const at = storage.get(ACCESS_TOKEN);
const tt = storage.get(TOKEN_TYPE) || "Bearer";
if (needAuth && !at) {
  // 同时中止本次请求,确保不会真正发出
  ((req as any).__ac as AbortController | undefined)?.abort();
  return Promise.reject(new Error("UNAUTHENTICATED"));
}

// 复杂逻辑:有 Token 再注入 Authorization 头
if (needAuth && at) {
  req.headers = { ...req.headers, Authorization: `${tt} ${at}` };
}

使用方式:需要鉴权的 API 不用写任何额外配置(默认会拦),公开 API(或登录/注册)明确加上 { auth: false } 即可。


3) 统一 401 处理(去掉 useRouter 的非法使用)

位置:你的 codeResponseParser 内,将 401 分支替换为:

ts
// 复杂逻辑:统一未授权处理(不在解析器里用 useRouter)
if (code === 401) {
  storage.remove(ACCESS_TOKEN);
  storage.remove(TOKEN_TYPE);
  // 复杂逻辑:避免组合式 API 环境限制,使用硬跳转到登录页
  if (window.location.pathname !== LOGIN_ROUTE.path) {
    window.location.replace(LOGIN_ROUTE.path);
  }
  throw new Error("未授权,请重新登录");
}

位置:响应失败拦截器(err 分支)里,加入 HTTP 401 fallback

ts
// 复杂逻辑:后端直接返回 HTTP 401 时的兜底处理
if (err?.response?.status === 401) {
  storage.remove(ACCESS_TOKEN);
  storage.remove(TOKEN_TYPE);
  if (window.location.pathname !== LOGIN_ROUTE.path) {
    window.location.replace(LOGIN_ROUTE.path);
  }
}

说明:useRouter() 不能在解析器(组件外)调用,上面用 window.location.replace 做了上下文无关的重定向。


4) 实例方法:供登出调用(清鉴权头 & 集中取消)

位置:createAxiosHttp 末尾、return _axios as unknown as AxiosInstance 前插入

ts
// 复杂逻辑:向实例挂载工具方法,供外部优雅下线使用
(_axios as any).cancelAll = cancelAll;
(_axios as any).setAuthToken = (token?: string | null, type = "Bearer") => {
  if (token) {
    _axios.defaults.headers.common.Authorization = `${type} ${token}`;
    storage.set(ACCESS_TOKEN, token);
    storage.set(TOKEN_TYPE, type);
  } else {
    delete _axios.defaults.headers.common.Authorization;
    storage.remove(ACCESS_TOKEN);
    storage.remove(TOKEN_TYPE);
  }
};

这样在 logout() 里可以写:http.cancelAll?.(); http.setAuthToken?.(null)


5) (可选)请求方法支持 formdataauth 透传

位置:你重写的 (_axios as any).request = async function ...函数签名组装 cfg部分替换为:

ts
// 复杂逻辑:扩展 contentType,支持 formdata;透传 { auth?: boolean, signal?: AbortSignal }
(_axios as any).request = async function <T, P = RequestParams>(
  url: string,
  method: Method = "GET",
  params?: P,
  reqCfg?: AxiosRequestConfig & { auth?: boolean; signal?: AbortSignal },
  contentType: "json" | "urlencoded" | "formdata" = "json"
): Promise<T> {
  const cfg: AxiosRequestConfig & { auth?: boolean } = {
    url,
    method,
    ...reqCfg,
    headers: { ...reqCfg?.headers },
  };

  if (method === "GET" || method === "HEAD") {
    cfg.params = params;
    cfg.paramsSerializer = (d) =>
      qs.stringify(d, { indices: false, skipNulls: true });
  } else {
    // 复杂逻辑:根据 contentType 选择编码方式
    if (contentType === "formdata") cfg.data = toFormData(params as any);
    else if (contentType === "urlencoded")
      cfg.data = toUrlencoded(params ?? ({} as any));
    else cfg.data = params;

    if (contentType === "urlencoded")
      cfg.headers = {
        ...cfg.headers,
        "Content-Type": "application/x-www-form-urlencoded",
      };
    if (contentType === "json")
      cfg.headers = { ...cfg.headers, "Content-Type": "application/json" };
    // formdata 让浏览器自动带 boundary,不手动设 Content-Type
  }

  return nativeRequest(cfg) as Promise<T>;
};

有了 { auth?: boolean },你在公开接口(如 /api/pub/get_feednews)调用时可以:

ts
http.request("/api/pub/get_feednews", "GET", undefined, { auth: false });

6) (配合改动)登出时的两行调用(在你的 authStore.logout() 里加)

ts
// 复杂逻辑:登出时立即中止所有在途请求 + 清鉴权头
http.cancelAll?.();
http.setAuthToken?.(null);

7) (可选)类型补充,便于在项目里显式写 auth

新建:types/axios.d.ts

ts
// 复杂逻辑:为 AxiosRequestConfig 增加自定义字段 auth
import "axios";
declare module "axios" {
  interface AxiosRequestConfig {
    auth?: boolean;
  }
}

验证 Checklist

  • 退出登录瞬间 Network 面板无新的业务请求(只有静态资源预取不算)。
  • 任何标记 { auth: true | 默认 } 的请求在无 Token时不会发出。
  • 后端返回 HTTP 401业务码 401,都能统一清理并跳到登录页。
  • cancelAll() 能把“已发起未完成”的请求立即中断

以上改完,你截图里“跳转后还发请求”的现象就会消失;即使有极短时序触发,也会被 AbortController未登录硬拦兜住。

本站总访问