主题
自定义 axios(Day20 3/3)
本页对应 Day20-Axios,共 6 段视频,约 2 小时 37 分。
学习目标
- 能够根据请求方法判断是否允许携带请求体,并仅对
post、put、patch构造并发送请求体。 - 能够分别处理请求成功、HTTP 状态异常、网络失败、超时和取消请求的响应结果,并提取
config、data、headers、request、status等关键信息。 - 能够通过
timeout配置实现请求超时控制,并在超时后返回包含超时时间与错误信息的失败对象。 - 能够使用
cancelToken与abort实现取消请求,并判断错误是否由取消操作引起。 - 能够为封装对象添加
get、post等快捷方法,正确传递 URL、请求体和请求配置。 - 能够实现请求拦截器与响应拦截器,说明其链式调用原理、执行顺序及成功/失败回调的传递机制。
知识点
一、请求体只允许 post/put/patch 携带
为什么需要:GET 请求即使写了 data,浏览器和服务器通常也不按请求体处理;转写稿里演示 GET 带 body 时请求失败。Axios 源码也只在 POST、PUT、PATCH 这类方法上拼请求体。
怎么想:先给 body 一个默认值 null,再判断 config.method 是否在允许数组里。只有允许时才把 config.data 赋给 body。
怎么做:
js
let body = null;
const allowMethods = ['post', 'put', 'patch'];
if (allowMethods.includes(config.method)) {
body = config.data;
}易错点:不要想当然把 delete 加进去;转写稿明确说按 axios 源码默认只处理这三个。GET 即使传了 data,也不应该继续拼请求体,否则容易触发服务端拒绝。
二、响应结果处理:成功、网络失败、状态码失败
为什么需要:调用方希望成功时拿到统一对象,失败时也能拿到 config、request、错误信息,而不是直接抛一个裸 XHR。
怎么想:成功结果整理成 { config, data, headers, request, status, statusText };请求根本没发出去时,reject 一个网络错误对象;拿到响应但状态码不在 2xx,reject 一个带状态码的错误对象。
怎么做:
js
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
config,
data: xhr.response,
headers: parseHeaders(xhr.getAllResponseHeaders()),
request: xhr,
status: xhr.status,
statusText: xhr.statusText
});
} else {
reject({
code: 'ERR_BAD_REQUEST',
config,
message: `Request failed with status code ${xhr.status}`,
name: 'AxiosError',
request: xhr
});
}响应头处理是转写稿里的调试重点:getAllResponseHeaders() 得到的是带 \r\n 的纯字符串,要先 split('\r\n')、过滤空行,再按 ": " 切分,最后用 reduce 合成对象:
js
function parseHeaders(raw) {
return raw.split('\r\n')
.filter(item => item)
.map(item => item.split(': '))
.reduce((pre, item) => {
pre[item[0]] = item[1];
return pre;
}, {});
}易错点:网络失败和 404 失败不是同一种信息;headers 在成功结果里应是对象,不是原始字符串;reduce 的初始值不能漏。
三、超时设置与取消请求
为什么需要:请求可能长时间不返回,也可能用户主动取消。两者都应走失败回调,并给出可判断的错误对象。
怎么想:超时用 xhr.timeout = config.timeout,监听 ontimeout 后 reject;取消请求用 cancelToken,它本质是一个 Promise,执行取消函数会改变状态,内部再调用 xhr.abort(),触发 onabort 后 reject 取消错误。
怎么做:
js
if (config.timeout) {
xhr.timeout = config.timeout;
}
xhr.ontimeout = function () {
reject({
code: 'ECONNABORTED',
config,
message: `timeout of ${config.timeout}ms exceeded`,
name: 'AxiosError',
request: xhr
});
};
if (config.cancelToken) {
config.cancelToken.then(() => {
xhr.abort();
});
}
xhr.onabort = function () {
reject({
code: 'ERR_CANCELED',
config,
message: 'canceled',
name: 'CanceledError',
request: xhr
});
};易错点:转写稿调试时发现,没有 cancelToken 却直接 .then 会报错,所以必须先判断存在;abort() 后要依赖 onabort 把外层 Promise 改成失败;判断是否取消可看错误码,不必执着于自定义类。
四、get/post 快捷方法与拦截器
为什么需要:对外要像 axios 一样能 axios.get()、axios.post();拦截器则用于在请求前、响应后统一加工数据。
怎么想:get(url, config) 内部调用 this.request({ ...config, method: 'get' });post(url, data, config) 再合并 data。拦截器分别存进请求、响应两个 handlers 数组;请求拦截器用 unshift 加到执行链前面,响应拦截器用 push 加到后面,因此请求拦截器后添加的先执行,响应拦截器先添加的先执行。
怎么做:
js
get(url, config = {}) {
return this.request({ ...config, method: 'get', url });
}
post(url, data, config = {}) {
return this.request({ ...config, method: 'post', url, data });
}
const chain = [[this.dispatchRequest.bind(this, config), undefined]];
requestInterceptors.forEach(item => {
chain.unshift([item.onResolved, item.onRejected]);
});
responseInterceptors.forEach(item => {
chain.push([item.onResolved, item.onRejected]);
});
let p = Promise.resolve(config);
while (chain.length) {
p = p.then(...chain.shift());
}
return p;易错点:转写稿里 get 写在类里,this 指向实例,所以用 this.request;如果普通函数调用就要注意绑定。拦截器链中每一项都要成对,缺省用 undefined 占位;最终返回的是整条链执行完的 Promise,所以响应拦截器会在最后执行。
课堂画面






代码示例
常见错误
- GET 也生成请求体 → 原因:只判断
config.data,未按请求方法限制 → 修法:仅POST/PUT/PATCH处理 body,其余方法跳过或置空。 - 拦截器注册后请求直接失败 → 原因:
use回调没有return config/response,Promise 链被中断 → 修法:成功回调必须放行,失败回调按需return Promise.reject(error)。 - 取消请求时报
cancelToken未定义或.then报错 → 原因:未判断config.cancelToken是否存在,或未注册状态回调 → 修法:先判空,再cancelToken.then(...)中调用xhr.abort(),并监听onabort返回取消错误。 - 超时没有进入自定义错误处理 → 原因:未设置
xhr.timeout或未监听ontimeout→ 修法:从config.timeout赋值,在ontimeout中reject统一错误对象并带超时信息。 - 给自定义 axios 加
get/post后this指向不对 → 原因:方法挂到实例/类上后未绑定,this不是预期对象 → 修法:用bind(context),或在内部统一走request并固定method。 - 响应 headers 仍是长字符串 → 原因:直接使用
getAllResponseHeaders()未解析 → 修法:按\r\n切割、过滤空项、按首个冒号拆分并 reduce 成对象。 - 404、网络失败、超时、取消混为一个错误 → 原因:未区分请求未发出、状态码非 2xx、timeout、abort → 修法:分别构造错误对象,至少保留
config/request/code/message/name,取消用isCancel判断。
练习
- 给自定义 axios 增加请求方法白名单,验证 GET 携带
data不生成请求体,POST/PUT/PATCH 正常携带。 - 实现响应 headers 字符串转对象,兼容空行和值中含冒号的情况。
- 在延迟接口下分别触发超时和主动取消,输出可区分的错误对象。
- 给自定义 axios 增加
get(url, config)和post(url, data, config),内部统一走request且固定method。 - 实现最小拦截器:request/response 各注册两个并打印执行顺序,再实现按索引
eject移除。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 只有post、put、patch才允许携带请求体 | 05:14 |
| 2 | 响应结果处理 | 30:47 |
| 3 | 超时设置 | 04:56 |
| 4 | 取消请求 | 26:52 |
| 5 | get方法和post方法 | 28:45 |
| 6 | 实现拦截器 | 1:00:53 |
小结
- 自定义 axios 的核心是统一
request入口、配置合并和错误归一。 - 请求体处理要受 HTTP 方法约束,不能只看
data是否存在。 - 成功响应对象应稳定包含
data/status/statusText/headers/config/request。 - 失败对象要能区分网络失败、状态码错误、超时和取消,否则排错成本高。
- headers 解析应封装成可测试的小函数,不要塞进主流程。
- 取消请求本质是 Promise 状态驱动 +
xhr.abort()+onabort回调,必须判空并防重复。 - 拦截器本质是 Promise 链:请求拦截器
unshift、响应拦截器push,顺序由链决定。 get/post等语法糖只做参数适配和method固定,最终仍应落到request。