Skip to content

自定义 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,所以响应拦截器会在最后执行。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

常见错误 ​

  1. GET 也生成请求体 → 原因:只判断 config.data,未按请求方法限制 → 修法:仅 POST/PUT/PATCH 处理 body,其余方法跳过或置空。
  2. 拦截器注册后请求直接失败 → 原因:use 回调没有 return config/response,Promise 链被中断 → 修法:成功回调必须放行,失败回调按需 return Promise.reject(error)。
  3. 取消请求时报 cancelToken 未定义或 .then 报错 → 原因:未判断 config.cancelToken 是否存在,或未注册状态回调 → 修法:先判空,再 cancelToken.then(...) 中调用 xhr.abort(),并监听 onabort 返回取消错误。
  4. 超时没有进入自定义错误处理 → 原因:未设置 xhr.timeout 或未监听 ontimeout → 修法:从 config.timeout 赋值,在 ontimeout 中 reject 统一错误对象并带超时信息。
  5. 给自定义 axios 加 get/post 后 this 指向不对 → 原因:方法挂到实例/类上后未绑定,this 不是预期对象 → 修法:用 bind(context),或在内部统一走 request 并固定 method。
  6. 响应 headers 仍是长字符串 → 原因:直接使用 getAllResponseHeaders() 未解析 → 修法:按 \r\n 切割、过滤空项、按首个冒号拆分并 reduce 成对象。
  7. 404、网络失败、超时、取消混为一个错误 → 原因:未区分请求未发出、状态码非 2xx、timeout、abort → 修法:分别构造错误对象,至少保留 config/request/code/message/name,取消用 isCancel 判断。

练习 ​

  1. 给自定义 axios 增加请求方法白名单,验证 GET 携带 data 不生成请求体,POST/PUT/PATCH 正常携带。
  2. 实现响应 headers 字符串转对象,兼容空行和值中含冒号的情况。
  3. 在延迟接口下分别触发超时和主动取消,输出可区分的错误对象。
  4. 给自定义 axios 增加 get(url, config) 和 post(url, data, config),内部统一走 request 且固定 method。
  5. 实现最小拦截器:request/response 各注册两个并打印执行顺序,再实现按索引 eject 移除。

原视频对照 ​

#视频时长
1只有post、put、patch才允许携带请求体05:14
2响应结果处理30:47
3超时设置04:56
4取消请求26:52
5get方法和post方法28:45
6实现拦截器1:00:53

小结 ​

  1. 自定义 axios 的核心是统一 request 入口、配置合并和错误归一。
  2. 请求体处理要受 HTTP 方法约束,不能只看 data 是否存在。
  3. 成功响应对象应稳定包含 data/status/statusText/headers/config/request。
  4. 失败对象要能区分网络失败、状态码错误、超时和取消,否则排错成本高。
  5. headers 解析应封装成可测试的小函数,不要塞进主流程。
  6. 取消请求本质是 Promise 状态驱动 + xhr.abort() + onabort 回调,必须判空并防重复。
  7. 拦截器本质是 Promise 链:请求拦截器 unshift、响应拦截器 push,顺序由链决定。
  8. get/post 等语法糖只做参数适配和 method 固定,最终仍应落到 request。