前言
第 1 篇解决了"服务器怎么验证你是谁",但立刻冒出下一个问题:拿到 token 之后,怎么让每个请求都自动带上它? 登录接口在 user.js 里,业务接口在 repo.js 里,如果一个一个接口手动写 headers.Authorization,写 10 个接口就重复 10 遍,漏一个就 401。axios 的拦截器就是来解决这个的。这篇讲透三件事:axios.create() 到底创建了什么、拦截器为什么是"钩子"而不是"自己拦自己"、请求发出去和响应回来的对象各有什么。axios 的基础封装(实例化、api 目录、/api 前缀、mock 方案)已在《Axios-与-MockJS-接口请求的封装与模拟》讲过,这篇聚焦鉴权链路。
一、axios.create():不是单例,是"配置副本"
create 是 axios 的实例工厂——它基于默认配置"捏"出一个全新的独立实例:
// src/api/config.js
import axios from 'axios'
const instance = axios.create({
baseURL: '/api', // 统一前缀
timeout: 5000, // 5 秒超时
})
export default instance
它不是单例模式。 单例要求"全局唯一",而 create 想捏几个就捏几个,每个实例的 baseURL、timeout、拦截器队列互不相干:
const a = axios.create({ baseURL: '/a' });
const b = axios.create({ baseURL: '/b' });
a === b; // false,独立对象
// a 加拦截器,b 和默认 axios 完全不受影响
a.interceptors.request.use(...); // a 有 1 个
b.interceptors.request.handlers.length; // 0
axios.interceptors.request.handlers.length; // 0
顺带澄清:你天天用的 axios.get()、axios.post(),其实就是 axios 自带的默认实例。create() 的价值在于按场景捏多套独立配置——比如"登录接口不带 token、业务接口带 token",就是两个实例各挂各的拦截器。
你项目里 config.js 只 create 了一次并 export default,靠 ES Module 模块缓存让所有 import 拿到同一个对象——这是"模块级单例效果",但是模块机制保证的,不是 axios 的设计。
二、拦截器:不是"自己拦自己",是钩子
新手最容易的困惑:axios 既能发请求又能拦请求,自己发自己拦,图什么?答案是:拦截器不是 axios 拦自己,而是 axios 给你留了两个"插槽",把你的代码插进"发送前"和"接收后"。
用快递公司比喻——axios 是快递公司,每次请求是一个包裹:
你的代码 →→→→→→ axios 发出包裹 →→→→→→ 服务器
│ │
request 拦截器 response 拦截器
(寄件打包台) (收件收发室)
- request 拦截器 = 寄件打包台:每次寄包裹都先过这里,你统一往里塞东西(token)、贴标签
- response 拦截器 = 收件收发室:服务器回寄时先到你这,你统一拆包检查(是不是 401?要不要丢?),再交给代码
axios 只负责运送;打包台和收发室里的活,都是你安排的。这就是 AOP(面向切面编程)/ 中间件思想:不动每个请求本身,而是给"发送/接收"这两个动作加统一切面。
三、request 拦截器:自动带上 Authorization
这是鉴权的核心一环:
// src/api/config.js
instance.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
}
return config; // 必须 return,否则请求发不出去!
});
三个要点:
config是请求配置对象——axios 照它组装 HTTP 报文,你在这里改它,就是改即将发出的报文- 改的是
headers——Authorization: Bearer <token>是 HTTP 标准的携带凭证方式,后端 mock/user.js 里req.headers["authorization"].split(" ")[1]取的正是Bearer后的那段 - 必须
return config——这叫"放行"。不 return,请求就永远卡在拦截器里。这也是新手最常见的坑
有了它,业务接口一行都不用管 token:
// src/api/user.js
export const login = async (data) => {
const res = await axios.post('/login', data);
return res.data;
};
// src/api/repo.js
export const getRepo = async () => {
const res = await axios.get('/repo');
return res.data;
};
这就是"一次封装,处处鉴权":写一遍,所有请求自动带凭证。
四、response 拦截器:统一处理 401
request 拦截器用成功回调(加头),response 拦截器通常用失败回调接住 HTTP 错误——token 过期时后端返回 401,在这里统一处理,而不是每个接口各写一遍跳登录:
instance.interceptors.response.use(
(res) => res, // 成功:原样放行
(err) => {
if (err.response?.status === 401) { // 凭证无效/过期
useAuthStore.getState().logout(); // 清空登录状态(zustand,见第 3 篇)
window.location.href = '/login'; // 跳回登录页
}
return Promise.reject(err); // 继续抛给调用方处理
}
);
注意:你现在 config.js 的 response 拦截器还只是
return res的"空转"状态。补上 401 分支,token 过期时前端才会自动清状态、跳登录——这是这个 demo 里最值得补的一环。
五、发出去的是什么:config → HTTP 报文
config 是"说明书",axios 照着它组装真实报文:
config(说明书) 实际发出的 HTTP 报文
──────────────────────── ─────────────────────────────
method: 'post' ──▶ POST /api/login?v=1
url: '/login' ↑ method + baseURL + url + params
baseURL: '/api'
params: {v:1} Accept: application/json
headers: {Accept, Authorization} ─▶ Authorization: Bearer xxx
data: {username, password} ─▶ 请求体: {"username":"admin","password":"123456"}
config 里核心字段就那几个:
| 字段 | 作用 |
|---|---|
method | 请求方法 |
url + baseURL | 路径 + 前缀,拼出完整地址 |
params | URL 查询参数,拼成 ?v=1 |
data | 请求体(POST 传给后端的内容,后端 req.body 就是它) |
headers | 请求头,拦截器加 token 就是改这里 |
timeout | 超时毫秒数 |
最易混的两个是 params 和 data:params 拼在 URL 问号后,data 放请求体里——一个给 GET 用,一个给 POST 用。
六、回来的是什么:response 的 6 个字段
响应回来,axios 包成一个 response 对象,只有 6 个 key:
{
data: { code: 0, user: {...}, token: '...' }, // ★ 响应体,最常用
status: 200, // HTTP 状态码
statusText: 'OK', // 状态文字
headers: {...}, // 响应头
config: {...}, // 这次请求用的完整配置(axios 回显)
request: {...}, // 底层请求对象
}
你页面里读的 res.code,其实已经是剥过壳的——因为接口文件统一 return res.data:
页面 await 得到 r = response ← 完整 6 字段对象
│
user.js: return res.data ← 剥掉外壳,只剩 {code, user, token}
│
Login.jsx: res.code === 0 ← 直接读业务字段
为什么要剥壳?因为你只关心服务器的业务数据(code/user/token),不关心 status/headers 这些 HTTP 层细节——剥掉后页面代码更干净,这正是 axios"默默做了很多"的体现。
七、整条鉴权链路串起来
把第 1 篇和第 2 篇拼成完整闭环:
浏览器 POST /api/login (admin/123456)
→ request 拦截器(此时无 token,跳过)
→ mock/user.js: jwt.sign 签发 token
→ 前端 setAuth 存 token/user(zustand,见第 3 篇)
→ 浏览器 GET /api/repo
→ request 拦截器:自动加 Authorization: Bearer <token>
→ mock/user.js: jwt.verify 验签 → 放行返回数据
→ 若 token 过期 → 后端返回 401 → response 拦截器清状态跳登录
axios 在这里的角色是统一装卸工:把"带凭证"和"处理失效"这两个所有请求都要做的重复劳动,收拢到拦截器这一处——省事、不漏、好维护。
小结
axios.create()是实例工厂,不是单例;捏独立副本,配置/拦截器隔离;模块导出实现的"单例效果"是 ES Module 机制,不是 axios 设计- 拦截器是钩子不是"自己拦自己":把公共逻辑插进"发送前/接收后"两个切面,AOP 思想
- request 拦截器自动加
Authorization: Bearer <token>,必须return config才算放行 - response 拦截器用失败回调统一接 401:清状态、跳登录
- config 是说明书,axios 照它组报文;核心字段
method/url/baseURL/params/data/headers/timeout,params拼 URL、data放请求体 - response 6 字段:
data/status/statusText/headers/config/request,接口封装统一return res.data剥壳
一句话:axios 拦截器把"带 token"和"处理 401"这两个重复劳动收拢到一处,让鉴权从"每个接口自己写"变成"一次封装,处处鉴权"。