26.7—— request,路由请求
2026/8/15 13:38:03 网站建设 项目流程

请求request流程

6号,7号,8号,9号,10号

拦截器思路:

  1. 基础数据
    • 一个“待处理请求”的记录表(Map)pendingRequests,key是请求唯一标识,value是取消函数
    • 唯一标识函数对参数键排序再序列化,JSON.stringify(data,Object.keys(data).sort())
  2. 每次请求进来先检查记录表有没有“相同标识”的请求
    • 如果有,说明上一次请求还没完成,直接调用对应的取消函数把它取消掉
    • 给当前的请求注入 CancelToken,并登记在 pendingRequests 里。CancelToken 只能使用一次,如果这个请求被取消并重新发起,旧的token 已经失效,需要重新生成。
    • 请求完成后(无论成功或失败),在响应拦截器里把它从记录表里删除

问题:当用户切换页面时,怎么把页面上所有还在飞的请求一次性全取消掉?
在哪里取消?axios还是组件?可以利用记录表pendingRequests吗?

给页面一个唯一的标志,并存放到 pendingRequests 的value里。
取消所有请求思路:

  1. 给请求打标签:发送请求时,用组件的唯一表示(比如路由路径)请求分组
    • 在路由守卫里,把当前页面的标志主动注入到所有请求中axios.defaults.pageId = to.path
  2. 在组件挂载时记录:把“当前页面标志pageId”存起来,或提供一个方法,能把请求注册到当前页面下。
    • axios.defaults.pageId获取pageId
    • 给当前的请求注入 CancelToken,和pageId,并登记在 pendingRequests 里
  3. 在组件卸载时取消:离开页面时,遍历记录表pendingRequests,把属于这个页面的所有请求取消并清理。
    • 在路由守卫的 from 离开时自动取消

问题:如何取消指定的请求
思路:给请求再添加一个requestType标签,这样既能按页面取消,也能精准取消

  1. pendingRequests 的 value 结构
{cancel:Function,pageId:string,requestType?:string// 可选标签}
  1. 新增按页面+类型取消函数
    • 在遍历记录表pendingRequests判断value.pageId === pageId(参数) && value.requestType === requestType(参数),如果是的话就取消请求和删除记录表里的请求
  2. 在组件里使用取消指定的请求
// 导出请求axios.get('/api/export',{requestType:'export'});// 列表刷新请求axios.get('/api/list',{requestType:'list-refresh'});// 用户重新点击导出,只取消上一次导出,不影响列表刷新cancelRequestsByType(currentPageId,'export');

问题:当我们主动取消了请求,axios会抛出一个 Cancel 错误。在拦截器里,我们要怎么区分“正常的取消”(不需要报错提示)和“真正的接口错误”(需要弹窗提示)?

思路:

  1. 先用axios.isCancel(error)判断错误对象是否为取消请求生产的Cancel类型。
  2. 进一步检查该请求是否在我们维护的 pendingRequests Map 中,即是否为我们主动取消的。我们主动取消的请求前一定还在表中(因为是我们调用cancel()后才抛出的错误),而其他来源的取消(如浏览器中断、第三方库取消)则不会在表中
  3. 双重判断:isCancel 为 true 且 pendingRequests.has(requestKey) 为 true → 我们主动取消,静默处理(不弹提示)。否则,可能是其他取消或真实错误,按真实错误处理。

补充:对于真实错误返回Promise.reject(error),让上层捕捉;对于主动取消,返回Promise.resolve()吞掉错误。

问题:智能重试机制
场景:用户正在后台填一个复杂的表单,点保存时,恰好网络抖动了一下,接口返回 500 或者超时。如果直接弹个“保存失败”的提示,用户心态炸裂,前面填的东西也可能丢了。

我们想要的效果是:

  • 遇到网络错误或 5xx 错误,自动静默重试 2 次,不弹错误提示
  • 遇到 4xx 错误(参数错误、无权限),不重试,直接提示
  • 重试间隔逐次递增(1秒、2秒、4秒)
  • 重试次数用完后才弹出“保存失败,请稍后重试”

设计:

  • 重试逻辑:放在axios.interceptors.response.use的第二个参数(错误回调)
  • 重试判断:网络错误/超时/5xx -> 重试;4xx(参数错、无权限)->不重试,直接抛
  • 配置方式:全局默认+单个请求覆盖
  • CancelToken: 每次重试都要创建新的,旧的已经失效了

思路:

  1. 基础数据
    • 全局默认配置含:重试次数基础延迟(毫秒),重试条件(网络错误/超时/服务端错误 返回 true,其余返回 false)
    • 重置配置:合并默认的配置和config的重置配置参数
    • 初始化重置次数:(config.__retryCount || 0)
  2. 不满足重试条件,直接抛return Promise.reject(error)
  3. 重试次数用完:重置次数大于等于重置配置的次数,直接抛return Promise.reject(error)
  4. 计算延迟
  5. 等待后重新发送请求await new Promise(resolve => setTimeout(resolve, delay));
    • 因为前面有 await,所以当前拦截器函数会暂停 delay 毫秒,等 Promise 完成后再继续执行后面的代码(创建新 CancelToken、增加计数、重发请求)。
    • 本质:它是一个异步的“等待”,而不是阻塞整个 JavaScript 线程。
  6. 等待结束后,检查是否页面已切换
    • 比较config.__pageIdaxios.defaults.pageId
    • 如果不相等,页面已切换,终止重试,清理登记表
    • 返回一个特殊错误,不让他触发业务层的错误return Promise.reject({ __canceledByNavigation: true, message: '页面已切换,重试终止', });
  7. 创建新的 CancelToken,更新 pendingRequests
    • 要先清理旧登记
  8. 记录重试次数(config.__retryCount + 1
  9. 重新发送请求return axios(config)

问题:Token 无感刷新
什么是“无感”?
用户正在填一个复杂表单,点提交。此时 token 恰好过期(通常只有2小时有效期),后端返回 401。

  • 有感:弹出一个登录框,“请重新登录”。用户心态炸裂,填的东西全丢了。
  • 无感:前端自动用 refresh_token 去换一个新的 access_token,然后把刚才那个失败的请求重新发一遍。整个过程用户完全没有感知。

核心思路:

  1. 请求拦截器(主动检查):发请求前,先检查access_token是否只剩5分钟。如果是,且没在刷新中,就先刷新再发
  2. 响应拦截器(被动兜底):收到401时,触发刷新,重试原请求
  3. 刷新锁(核心机制):用一个 Promise 队列,保证即使10个请求同时401,也只有一个刷新请求飞出去,其他都排队
  4. 退出登录:刷新失败(refresh_token 也过期)时,清除token,跳转登录页

token思路:

  1. 基础数据
  • Token 相关工具函数
    1. 从 store 获取 accessToken函数(可以改为localStorage存储,更解耦)
    2. 从 store 获取 refreshToken函数(可以改为localStorage存储,更解耦)
    3. 保存 Token 函数到 store ,参数为accessToken, refreshToken(可以改为localStorage存储,更解耦)
    4. 清除权限函数clearAuth(),清除accessToken, refreshToken,跳转到登录页
    5. 检查 token 是否快过期函数(提前 5 分钟)isTokenExpiringSoon()
    6. 是否正在刷新refreshing
    7. 排队等待的请求refreshSubscribers = []
    8. token 刷新完成后执行的函数onRefreshed(newToken),遍历refreshSubscribers ,用新的token重试所有请求,重置refreshSubscribers = []
    9. token 刷新失败函数onRefreshFailed(error),遍历refreshSubscribers ,返回报错,重置refreshSubscribers = []
  1. 执行刷新refreshTokens()
    • 如果没有刷新 token,refreshToken,就清除权限,返回Promise.reject(new Error('无 refresh token'))
    • 调刷新接口,注意:用 axios 原生方法,不走我们自己封装的 service,避免死循环
    • 获取新的accessToken, newRefreshToken,并保存,返回accessToken
    • 失败就clearAuth(),throw 错误
  2. 修改请求拦截器(在原有基础上追加放在最后面,新增:Token 主动刷新)
    • 基础数据:tokenAttached布尔值,标记是否已经在排队/刷新逻辑里挂过 token
    • 如果 token 快过期isTokenExpiringSoon(),且当前请求不是刷新请求本身
      • 如果没有在刷新,则触发步骤2刷新。获取新的getAccessToken,设置 config.headers.Authorization =Bearer ${newToken},刷新逻辑里挂过 tokentokenAttached = true,执行函数onRefreshed(新的getAccessToken)。失败就调用刷新失败函数onRefreshFailed(error),finally执行refreshing = false;
      • 否则正在刷新中,当前请求排队等待。await new Promise((resolve, reject) => {refreshSubscribers.push((token, error) => {...}},成功时token也要挂上,刷新逻辑里挂过 tokentokenAttached = trueresolve(token);失败时reject(error);。失败时这样写,有个bug,当clearAuth() 里触发了 router.push(‘/login’),页面开始跳转。在跳转完成前,那些排队请求的报错可能会短暂触发页面上的错误提示。
    • 正常 token:直接挂上config.headers.Authorization =Bearer ${token};
    • 返回 config
  3. 修改响应拦截器(在原有 onRejected 最前面插入,新增:被动 401 刷新)
    • 基础数据const config = error.config;config.__isRetryRequest:标记,避免循环
    • 如果 error.response?.status === 401,且当前请求不是刷新请求本身config.url !== '/api/auth/refresh'
      • 如果config.__isRetryRequest为 true,刷新成功后重试仍 401 ,为避免死循环,直接清除认证并跳转,并返回错误
      • 第一次遇到 401!refreshing
        • 抢到刷新锁,refreshing = true;
        • 执行刷新refreshTokens()
        • 成功:
        • 唤醒所有排队请求onRefreshed(newToken);
        • 重试当前请求设置config.headers.Authorization =Bearer ${newToken}`` ;
        • 标记,避免循环config.__isRetryRequest = true;
        • 注意:重试前不用手动清理 pending,因为 service(config) 会再次进入请求拦截器登记
        • 返回service(config);
        • 失败:
        • 刷新失败,通知所有排队请求 rejectonRefreshFailed(refreshError);
        • 清除认证clearAuth();
        • 删除队列里的值pendingRequests.delete(key);
        • 返回报错return Promise.reject(refreshError);
      • 已有刷新正在进行,排队等待
        • 返回 Promise,refreshSubscribers.push({}),push内容如下
        • resolve 方法:参数(token),设置请求头,config.__isRetryRequest设置为true,resolve(service(config));
        • reject方法:参数(err),从请求队列里删除,reject(err);

问题:如果确实需要两次请求或多次相同请求发出怎么办?
场景:

  1. 并发文件上传:同时上传两个相同的文件(不同目录、不同用途)
  2. 并发创建订单:业务允许用户同时创建两笔内容相同的订单
  3. 并发导出:同时导出两个相同参数的报表(可能格式不同、用途不同)

方案 1:请求级别豁免(推荐)
在请求 config 上加一个自定义参数allowDuplicate,拦截器看到这个标记就跳过重复取消逻辑。

请求拦截器修改
如果请求标记了 allowDuplicate,跳过重复取消逻辑

  • 在取消重复请求那里添加判断条件,!config.allowDuplicate

方案 2:请求类型级别豁免
如果某一类请求(比如所有上传)都需要允许重复,可以在配置里预设:

  • 添加白名单;
  • 在取消重复请求那里添加判断条件,和非白名单的接口

方案 3:用 requestType 来区分“同类但不同目的”的请求
如果请求参数完全一样,但业务含义不同,可以给 requestType 加后缀来区分:

  • 因为 requestType 没有被纳入 getRequestKey 的生成逻辑(目前只用了 method + url + params + data),所以如果你希望 requestType 也参与去重判断,需要把它加入 getRequestKey

流程

登录接口 (store:存token和个人信息数据)
⬇️
请求axios
|—请求拦截器,失败了就Promise.reject(error),成功了如下:
|—|—判断token,没有就设置
|—|—判断config.signal,没有就设置
|—|—判断是否过期,如果过期了就返回
|—|—加入请求队列
|—响应拦截器,成功就移除请求队列,返回响应,失败如下
|—|—如果过期
|—|—|—中断请求,创建新的AbortController,返回Promise.reject(error)
|—|—如果请求状态401,且返回的信息为“token无效”
|—|—|—过期状态设置为 true
|—|—|—中断请求,创建新的AbortController,给出页面提示
|—|—|—返回 清理数据store
|—|—|—|—路由重定向,路由重定向.then过期状态重置置为 false
⬇️
路由
|—全局前置守卫 router.beforeEach
|—|—设置页面标题
|—|—1. 白名单放行
|—|—2. 获取token
|—|—3. 未登录跳转登录页
|—|—4. 获取全局配置(若未加载),500回退500的页面,其他错误视为未登录,回退登录
|—|—5. 若目标是 /login,已登录重定向到首页
|—|—6. 判断路由是否已加载
|—|—|—已加载:检查目标是否匹配(有匹配说明已授权,没有就返回404页面)
|—|—|—未加载:请求动态路由(从store)并添加 router.addRoute(routes[i]), 重定向到当前路径,触发重新匹配(替换历史记录)。报错500回退500的页面,他错误可取消导航或跳转登录
|—|—|—|—store:请求动态路由 getAccessRoutes,先 getPermissions 获取权限,然后同时获取远程路由和动态路由
|—全局后置钩子 router.afterEach
|—|—取消所有请求
|—重置路由,将 router 变量重新赋值,并返回新实例供外部更新。用于退出等场景

动态路由与权限

21号

前端权限体系 │ ├── 一、动态路由生成 │ ├── 核心问题:不同角色看到不同菜单,无权限页面即使输URL也无法访问 │ ├── 数据来源 │ │ ├── 后端返回菜单列表(扁平或树形) │ │ │ ├── 字段:id, parentId, name, path, permission, icon, type, hidden │ │ │ └── 格式:扁平结构(靠parentId关联)或 树形结构(直接嵌套) │ │ └── 登录成功后获取,存入 store / sessionStorage │ │ │ ├── 扁平数据 → 路由树 │ │ ├── 算法:两次遍历 + Map │ │ │ ├── 第一轮:所有节点放入 Map<id, route> │ │ │ └── 第二轮:根据 parentId 挂到父节点的 children │ │ ├── 组件映射:path → component 的配置表 │ │ │ ├── 独立配置文件 routeConfig.js │ │ │ └── 新增页面只需改这一处 │ │ └── 生成 Vue Router 可用格式(path, name, meta, component, children) │ │ │ ├── 添加时机 │ │ ├── 路由守卫 beforeEach 中 │ │ ├── 标记位 dynamicRoutesAdded 防重复 │ │ └── 添加后 next({ ...to, replace: true }) 重定向,确保新路由生效 │ │ │ ├── 刷新恢复 │ │ ├── 动态路由存于内存,刷新后丢失 │ │ ├── 解决方案:重新请求菜单接口 │ │ └── 用 dynamicRoutesAdded 判断是否需要重新获取 │ │ │ └── 完整流程 │ ├── 用户访问 → 路由守卫 │ ├── 未登录 → 跳转登录 │ ├── 已登录 且 dynamicRoutesAdded === false │ │ ├── 显示 loading(避免白屏) │ │ ├── 请求菜单 → 生成路由 → addRoute │ │ └── 标记完成 → 关闭 loading → 放行 │ └── 已登录 且 dynamicRoutesAdded === true │ ├── 检查 to.meta.permission → 调用 hasPermission │ ├── 无权限 → 跳转 /404 │ └── 有权限 → 放行 │ ├── 二、按钮级权限 │ ├── 核心问题:同一页面内,不同角色看到不同按钮 │ ├── 权限数据 │ │ ├── 后端返回权限对象 { "PERM_KEY": true/false } │ │ ├── 存入 Vuex store │ │ └── 异步加载完成前需要 loading 保护 │ │ │ ├── 判断函数 hasPermission(permissionName) │ │ ├── 从 store 读取权限对象 │ │ ├── 返回布尔值 │ │ └── 权限未加载时,返回 true 避免闪烁 │ │ │ ├── 实现方案对比 │ │ ├── 方案A:自定义指令 v-permission │ │ │ ├── 优点:使用简洁,适合大量按钮 │ │ │ ├── 缺点:直接操作 DOM,与 Vue 响应式存在冲突风险 │ │ │ └── 注意:必须移除 DOM 而非隐藏(display:none 可被控制台恢复) │ │ │ │ │ └── 方案B:封装组件 <Permission> │ │ ├── 优点:v-if 控制渲染,安全可靠 │ │ ├── 缺点:需要引入组件 │ │ └── 适用:需要“禁用+提示”的场景 │ │ │ ├── 多权限联合判断 │ │ ├── 指令支持字符串或数组 │ │ └── 用 Array.every() 判断所有权限都为 true 才显示 │ │ │ └── 用户体验细节 │ ├── 无权限按钮:默认隐藏(安全) │ └── 特殊场景:禁用 + 悬浮提示,让用户知道功能存在但无权使用 │ └── 三、综合边界处理 ├── 用户手动输入 URL → 路由守卫判断权限 → 无权限跳 404 ├── 用户修改本地权限数据 → 无意义,后端接口仍有权限校验 ├── 权限接口慢 → loading 遮罩 + 权限未加载时按钮默认显示 └── 菜单获取失败 → 跳转登录页,不关遮罩避免白屏闪烁
-

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询