首页 / 文章

作为HTTP中间件的Axios拦截器:React应用的设计思路

了解 Axios 的请求和响应拦截器作为中间件是如何工作的,每个部分应包含什么内容,以及它们如何在 React 中集中处理认证头信息与 401 错误。

发布者:REACTAPP.TOP阅读约 6 分钟1357 词

来源: 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错误的重试逻辑加以保护,防止出现循环或并发刷新的情况。