ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

26.7—— request,路由请求

2026/8/15 13:38:08 拓冰建站 浏览量
26.7—— request,路由请求 请求request流程6号7号8号9号10号拦截器思路基础数据一个“待处理请求”的记录表MappendingRequestskey是请求唯一标识value是取消函数唯一标识函数对参数键排序再序列化JSON.stringify(dataObject.keys(data).sort())每次请求进来先检查记录表有没有“相同标识”的请求如果有说明上一次请求还没完成直接调用对应的取消函数把它取消掉给当前的请求注入 CancelToken并登记在 pendingRequests 里。CancelToken 只能使用一次如果这个请求被取消并重新发起旧的token 已经失效需要重新生成。请求完成后无论成功或失败在响应拦截器里把它从记录表里删除问题当用户切换页面时怎么把页面上所有还在飞的请求一次性全取消掉在哪里取消axios还是组件可以利用记录表pendingRequests吗给页面一个唯一的标志并存放到 pendingRequests 的value里。取消所有请求思路给请求打标签发送请求时用组件的唯一表示比如路由路径请求分组在路由守卫里把当前页面的标志主动注入到所有请求中axios.defaults.pageId to.path在组件挂载时记录把“当前页面标志pageId”存起来或提供一个方法能把请求注册到当前页面下。从axios.defaults.pageId获取pageId给当前的请求注入 CancelToken和pageId并登记在 pendingRequests 里在组件卸载时取消离开页面时遍历记录表pendingRequests把属于这个页面的所有请求取消并清理。在路由守卫的 from 离开时自动取消问题如何取消指定的请求思路给请求再添加一个requestType标签这样既能按页面取消也能精准取消pendingRequests 的 value 结构{cancel:Function,pageId:string,requestType?:string// 可选标签}新增按页面类型取消函数在遍历记录表pendingRequests判断value.pageId pageId(参数) value.requestType requestType(参数)如果是的话就取消请求和删除记录表里的请求在组件里使用取消指定的请求// 导出请求axios.get(/api/export,{requestType:export});// 列表刷新请求axios.get(/api/list,{requestType:list-refresh});// 用户重新点击导出只取消上一次导出不影响列表刷新cancelRequestsByType(currentPageId,export);问题当我们主动取消了请求axios会抛出一个 Cancel 错误。在拦截器里我们要怎么区分“正常的取消”不需要报错提示和“真正的接口错误”需要弹窗提示思路先用axios.isCancel(error)判断错误对象是否为取消请求生产的Cancel类型。进一步检查该请求是否在我们维护的 pendingRequests Map 中即是否为我们主动取消的。我们主动取消的请求前一定还在表中因为是我们调用cancel()后才抛出的错误而其他来源的取消如浏览器中断、第三方库取消则不会在表中双重判断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 每次重试都要创建新的旧的已经失效了思路基础数据全局默认配置含重试次数基础延迟毫秒重试条件网络错误/超时/服务端错误 返回 true其余返回 false重置配置合并默认的配置和config的重置配置参数初始化重置次数:config.__retryCount || 0)不满足重试条件直接抛return Promise.reject(error)重试次数用完重置次数大于等于重置配置的次数直接抛return Promise.reject(error)计算延迟等待后重新发送请求await new Promise(resolve setTimeout(resolve, delay));因为前面有 await所以当前拦截器函数会暂停 delay 毫秒等 Promise 完成后再继续执行后面的代码创建新 CancelToken、增加计数、重发请求。本质它是一个异步的“等待”而不是阻塞整个 JavaScript 线程。等待结束后检查是否页面已切换比较config.__pageId和axios.defaults.pageId如果不相等页面已切换终止重试清理登记表返回一个特殊错误不让他触发业务层的错误return Promise.reject({ __canceledByNavigation: true, message: 页面已切换重试终止, });创建新的 CancelToken更新 pendingRequests要先清理旧登记记录重试次数config.__retryCount 1重新发送请求return axios(config)问题Token 无感刷新什么是“无感”用户正在填一个复杂表单点提交。此时 token 恰好过期通常只有2小时有效期后端返回 401。有感弹出一个登录框“请重新登录”。用户心态炸裂填的东西全丢了。无感前端自动用 refresh_token 去换一个新的 access_token然后把刚才那个失败的请求重新发一遍。整个过程用户完全没有感知。核心思路请求拦截器主动检查发请求前先检查access_token是否只剩5分钟。如果是且没在刷新中就先刷新再发响应拦截器被动兜底收到401时触发刷新重试原请求刷新锁核心机制用一个 Promise 队列保证即使10个请求同时401也只有一个刷新请求飞出去其他都排队退出登录刷新失败refresh_token 也过期时清除token跳转登录页token思路基础数据Token 相关工具函数1. 从 store 获取 accessToken函数可以改为localStorage存储更解耦2. 从 store 获取 refreshToken函数可以改为localStorage存储更解耦3. 保存 Token 函数到 store 参数为accessToken, refreshToken可以改为localStorage存储更解耦4. 清除权限函数clearAuth()清除accessToken, refreshToken跳转到登录页5. 检查 token 是否快过期函数提前 5 分钟isTokenExpiringSoon()6. 是否正在刷新refreshing7. 排队等待的请求refreshSubscribers []8. token 刷新完成后执行的函数onRefreshed(newToken)遍历refreshSubscribers 用新的token重试所有请求重置refreshSubscribers []9. token 刷新失败函数onRefreshFailed(error)遍历refreshSubscribers 返回报错重置refreshSubscribers []执行刷新refreshTokens()如果没有刷新 tokenrefreshToken就清除权限返回Promise.reject(new Error(无 refresh token))调刷新接口注意用 axios 原生方法不走我们自己封装的 service避免死循环获取新的accessToken, newRefreshToken并保存返回accessToken失败就clearAuth()throw 错误修改请求拦截器在原有基础上追加放在最后面新增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修改响应拦截器在原有 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);已有刷新正在进行排队等待返回 PromiserefreshSubscribers.push({})push内容如下resolve 方法参数(token)设置请求头config.__isRetryRequest设置为trueresolve(service(config));reject方法参数(err)从请求队列里删除reject(err);问题如果确实需要两次请求或多次相同请求发出怎么办场景并发文件上传同时上传两个相同的文件不同目录、不同用途并发创建订单业务允许用户同时创建两笔内容相同的订单并发导出同时导出两个相同参数的报表可能格式不同、用途不同方案 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 │ │ │ ├── 第一轮所有节点放入 Mapid, 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 遮罩 权限未加载时按钮默认显示 └── 菜单获取失败 → 跳转登录页不关遮罩避免白屏闪烁-