作为HTTP中间件的Axios拦截器:React应用的设计思路
了解 Axios 的请求和响应拦截器作为中间件是如何工作的,每个部分应包含什么内容,以及它们如何在 React 中集中处理认证头信息与 401 错误。
来源: Axios Interceptors Explained: The Missing Piece Every Frontend Developer Should Learn.
在较新的 React 项目代码库中,API 调用往往是一次复制粘贴就增加一个:每个请求都会生成自己的 Authorization 标头,每个响应都有独立的错误处理机制,而每条失败路径都会重复相同的恢复逻辑。这种做法适用于三个接口,但一旦接口数量达到三十个,就会变成维护难题。Axios 提供了内置的拦截器解决方案,一旦将它们视为 HTTP 流量的中间件,就能清楚地判断哪些功能应该放到拦截器中,哪些则应保留在组件里。
拦截器消除的重复代码
常见的起始方式是直接在用户请求中附加承载令牌:
axios.get("/users", {
headers: {
Authorization: `Bearer ${token}`,
},
});
第二个接口则逐字重复该标头内容:
axios.get("/products", {
headers: {
Authorization: `Bearer ${token}`,
},
});
写入操作也是如此,只是中间部分是请求体:
axios.post("/orders", orderData, {
headers: {
Authorization: `Bearer ${token}`,
},
});
乍看之下,这些似乎都没有什么危害。但随着项目的发展,每个请求都需要令牌,每个响应都需要错误处理,每次失败也需要相同的应对方式。正是这类贯穿全局的工作才需要拦截器来处理。
拦截器究竟是什么
拦截器是 Axios 在每个外出请求发送之前,或在你的代码接收到每个进入的响应之前会调用的函数:这是一个你可以检查或修改请求流的控制点。在响应最终满足你的 React 代码所等待的承诺之前,它也会经过自己的控制点。
整个流程始于你的应用程序:
React Application
然后经过这两个控制点,最后再回到应用程序:
↓Request Interceptor↓Axios↓Server↓Response Interceptor↓React Application
你的组件永远不会直接与服务器通信;所有操作都通过这个链式结构传递,因此这里非常适合实现统一的行为处理。
中间件——以前台为例说明
如果你写过 Express 或 Koa 代码,就会对这一概念很熟悉:那就是中间件。如果没有,可以想象去一座办公大楼的情景。你不会直接走进办公室,而是先从自己出发:
You
在途中会经过两个站点:
↓Security Check↓Reception↓Office
安全检查验证你的身份,前台确认你的预约,之后你才能进入办公室。这两个站点都不是最终目的地;它们都会对你进行额外检查,然后决定是让你继续前进还是拒绝进入。
中间件就是两个系统之间的任何一层,在请求继续传递之前执行相关操作。把办公室换成服务器,从 React 应用开始:
React App
而拦截器就是中间的那一层:
↓Middleware (Interceptor)↓Server
换言之,Axios 拦截器是用于处理客户端 HTTP 请求和响应的中间件。
请求拦截器与响应拦截器
Axios 提供了两种类型的拦截器,分别通过 axios.interceptors.request.use(...) 和 axios.interceptors.response.use(...) 注册。每次调用都会接受一个成功处理函数以及一个可选的错误处理函数,并返回一个标识符,之后可将其传递给 eject 方法以移除该拦截器。
请求拦截器
这些拦截器会在请求发送之前执行。组件会发起请求:
Component
而拦截器则会在请求发出时对其进行处理:
↓Request Interceptor↓Server
请求拦截器的典型功能包括:
- 附加身份验证令牌
- 设置自定义头部信息,如区域设置或请求标识符
- 调整请求负载内容
- 记录发出的请求信息
处理程序会接收到请求配置,必须将其(或对其的承诺)返回,请求才能继续;忘记这一点是初学者的典型错误。
响应拦截器
它们在服务器回复之后、应用程序收到结果之前运行。响应来自服务器:
Server
并在传递给组件的过程中经过拦截器:
↓Response Interceptor↓Component
常见的职责包括:
- 在一处处理错误
- 转换或解包响应数据
- 记录响应信息
- 重试失败的请求
- 将用户重定向,例如转到登录页面
成功处理程序负责处理2xx响应,错误处理程序则处理其余响应。如果无法恢复,应返回一个被拒绝的承诺,以便调用者仍能看到失败情况。
单次调用背后发生了什么
单行请求看起来似乎会让 Axios 立即发出请求:
axios.get("/users");
实际上,同样的调用:
axios.get("/users")
在您的 .then() 方法执行之前会经过更长的处理流程:
↓Request Interceptor↓HTTP Request↓Server↓HTTP Response↓Response Interceptor↓.then()↓Your Component
共享逻辑会在每次调用时运行,而调用方却无从知晓。
随着应用规模扩大,集中处理的重要性
想象一个有上百个通过手动方式编写的 API 调用的项目。用户请求会携带如下标题:
axios.get("/users", {
headers: {
Authorization: `Bearer ${token}`,
},
});
产品相关请求也是如此:
axios.get("/products", {
headers: {
Authorization: `Bearer ${token}`,
},
});
所有写入操作,比如创建订单,也是如此:
axios.post("/orders", data, {
headers: {
Authorization: `Bearer ${token}`,
},
});
如果后端更改了认证机制,就必须更新所有请求的配置,任何遗漏的请求都会变成漏洞。拦截器可一次性处理这些逻辑,而Axios会自动在所有请求中应用它。应通过axios.create()创建的实例来注册拦截器,这样它才会作用于你的API,而不会影响第三方网址。
应用场景:自动附加访问令牌
这是使用拦截器最常见的原因。应用在登录后会存储一个访问令牌,所有受保护的接口都需要该令牌。请求最初只是普通的请求:
API Call
而在请求到达服务器之前,拦截器会添加相应的头部信息:
↓Request Interceptor↓Authorization Header Added↓Server
现在每个请求都会携带该令牌。应在每次请求时于拦截器中读取令牌,而非仅在启动时读取一次,否则刷新后的令牌将无法被获取。
应用场景:全局处理401响应
访问令牌会过期。一旦发生这种情况,服务器会返回如下响应:
401 Unauthorized
如果没有拦截器,每次调用都需要单独处理。第一次 API 调用:
API 1
之后,其他每个端点都需要重复相同的处理流程:
↓401 HandlingAPI 2↓401 HandlingAPI 3↓401 Handling
而响应拦截器则可以在一个地方捕获所有的 401 Unauthorized 错误。当检测到该错误时:
401 Received
它可以刷新访问令牌,重新发送原始请求,并将成功的响应返回给调用方:
↓Refresh Access Token↓Retry Original Request↓Return Successful Response
用户永远察觉不到令牌已过期。有两个重要的防护措施:其一是在配置中标记重复发送的请求,以避免多次401错误无限循环;其二则是让同时发生的失败共享一次刷新调用,相关内容可见于为何并发的401错误会导致用户登出以及单次刷新解决方案。
何时无需使用拦截器
那些只有两三个API调用的小型应用,从间接处理方式中获得的益处微乎其微,而且隐藏的行为会让那些无法看清请求头来源的读者感到困惑。当有数十甚至数百个请求时,拦截器是保持请求处理流程清晰且一致的最佳手段之一。应将拦截器用于处理跨领域的通用问题,而与特定端点相关的逻辑则应放在该端点内部。
总结
发送请求只是HTTP层功能的一小部分。身份验证、错误处理、日志记录、响应格式化以及令牌刷新都属于跨功能需求,而拦截器则为这些功能提供了统一的处理场所。
- 将拦截器视为客户端中间件:请求拦截器负责处理发出的请求,响应拦截器则负责处理接收到的响应。
- 始终从请求拦截器中获取配置,并将无法处理的错误从响应拦截器中重新抛出。
- 将它们绑定到针对特定API创建的
axios.create()实例上。 - 对401错误的重试逻辑加以保护,防止出现循环或并发刷新的情况。