axios浏览器端请求是如何实现的?

本文是"axios源码系列"第五篇,你可以查看以下链接了解过去的内容。

  1. axios 是如何实现取消请求的?
  2. 你知道吗?axios 请求是 JSON 响应优先的
  3. axios 跨端架构是如何实现的?
  4. axios 拦截器机制是如何实现的?

本文我们将讨论 axios 在浏览器端是如何实现的。

通过之前的学习,我们已经了解到 axios 内部是通过适配器模式,同时支持在浏览器端和 Node.js 端使用。

  • 在浏览器环境是通过封装 XMLHttpRequest API 实现请求的,源代码位于 lib/adapters/xhr.js
  • 在 Node.js 环境则是通过封装 http/https 模块实现请求的,源代码位于 lib/adapters/http.js

本文我们只讲解浏览器端实现 xhr.js。

总体结构

打开 xhr.js,大概浏览一下文件内容,就能梳理大概的实现逻辑。

js 复制代码
// /v1.6.8/lib/adapters/xhr.js#L48
export default isXHRAdapterSupported && function (config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // ...
  }
}

首先,能观察到的是:导出时会先判断当前是否是浏览器环境(isXHRAdapterSupported)。如果不是(即位于 Node.js 环境),那么 xhr.js 最终会解析结果是。

js 复制代码
export default false

这样在打包 Node.js 环境包时,其后的浏览器端实现就成功 Tree Shaking 掉了。

其次,能发现我们的请求结果最终是以 Promise 对象形式返回,内部我们通过 resolve(response) 返回请求结果,使用 reject(err) 抛出错误。这也能让我们使用 .then() .actch 或 await 这些特定于 Promise API 的操作。

再来看一下,内部实现的情况。

先会看到创建一个 XMLHttRequest 实例对象

js 复制代码
// /v1.6.8/lib/adapters/xhr.js#L76
let request = new XMLHttpRequest();

接下来,利用传入的 config 参数发起请求。

js 复制代码
// /v1.6.8/lib/adapters/xhr.js#L87
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

// Set the request timeout in MS
request.timeout = config.timeout;

// Send the request
request.send(requestData || null);

最后,就是监听 request 上的各种事件处理请求了。这块内容属于核心内容,我们仔细来讲。

处理请求

在处理请求的过程当中,我们会处理包括 abort、timeout、error、readystatechange/onloaded 在内的事件,同时还有下载和上传的进度事件 progress(分别在 request 和 request.update 上注册)。

针对进度事件,为了避免行文冗长,在本文我们不会讲解。有兴趣的读者可以自行阅读源码,实现比较简短,不难理解。

abort 事件

js 复制代码
// /v1.6.8/lib/adapters/xhr.js#L146-L156
// Handle browser request cancellation (as opposed to a manual cancellation)
request.onabort = function handleAbort() {
  if (!request) {
    return;
  }

  reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));

  // Clean up request
  request = null;
};

当调用 request.abort() 方法时,就会触发 abort 事件。在 axios 中,当我们使用 AbortController API 作为 signal 参数代入,并调用 signal.abort() 时,内部其实就在调用 request.abort(),实现取消请求的逻辑。

在 onabort 方法内部,实际上就是调用 reject(AxiosError) 返回请求被取消的结果。

注意,在开始会有一个 if (!request) 的保护判断,在 axios 内部,被处理后的请求会被手动置空(request = null),这样能够一些意料之外的 BUG 出现(其他事件同理,不再赘述)。

timeout 事件

js 复制代码
// /v1.6.8/lib/adapters/xhr.js#L168-L183
// Handle timeout
request.ontimeout = function handleTimeout() {
  let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
  if (config.timeoutErrorMessage) {
    timeoutErrorMessage = config.timeoutErrorMessage;
  }
  reject(new AxiosError(
    timeoutErrorMessage,
    AxiosError.ECONNABORTED,
    config,
    request));

  // Clean up request
  request = null;
};

timeout 事件处理更加简单,在根据配置确定好错误信息 后,直接 reject(new AxiosError()) 返回请求请求超时的结果。

还记得之前 request.timeout 的设置吗?

js 复制代码
// Set the request timeout in MS
request.timeout = config.timeout;

config.timeout 默认值是 0,也是没有超时限制。

js 复制代码
// /v1.6.8/lib/defaults/index.js#L123-L127
/**
 * A timeout in milliseconds to abort a request. If set to 0 (default) a
 * timeout is not created.
 */
timeout: 0,

error 事件

js 复制代码
// /blob/v1.6.8/lib/adapters/xhr.js#L158-L166
// Handle low level network errors
request.onerror = function handleError() {
  // Real errors are hidden from us by the browser
  // onerror should only fire if it's a network error
  reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));

  // Clean up request
  request = null;
};

error 事件处理逻辑比 timeout 还简单,只不过错误信息变成了"Network Error"。

abort、timeout 和 error 这三个事件都有一个共同特点,即请求发出后还没收到响应,请求就意外中断了。

  • abort 事件是手动取消请求造成的
  • timeout 则是在超过前端设置最长等待时间后,自动触发的
  • error 则是由于网络原因,自动触发的。这里的网络原因包括网络意外中断、请求了一个无效地址或是跨域错误(CORS)等情况

至此,我们已经将 request 实例上所有没有响应返回的错误都监听了。接下来,就是处理请求完成的场景(即有响应返回,不管是 2xx 状态码,还是 4xx、5xx 状态码)。

请求完成事件

js 复制代码
// /v1.6.8/lib/adapters/xhr.js#L123-L144
if ('onloadend' in request) {
  // Use onloadend if available
  request.onloadend = onloadend;
} else {
  // Listen for ready state to emulate onloadend
  request.onreadystatechange = function handleLoad() {
    if (!request || request.readyState !== 4) {
      return;
    }

    // The request errored out and we didn't get a response, this will be
    // handled by onerror instead
    // With one exception: request that using file: protocol, most browsers
    // will return status as 0 even though it's a successful request
    if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
      return;
    }
    // readystate handler is calling before onerror or ontimeout handlers,
    // so we should call onloadend on the next 'tick'
    setTimeout(onloadend);
  };
}

看起来很长(而且还没贴出 onloaded 实现),但其实可以简化成:

js 复制代码
request.onload = onloaded

之所以写成现在这样,是为了向前兼容。

最早 axios 只用 request.onreadystatechange 实现请求完成处理。

js 复制代码
request.onreadystatechange = function handleLoad() {
  if (!request || request.readyState !== 4) {
    return;
  }

  // The request errored out and we didn't get a response, this will be
  // handled by onerror instead
  // With one exception: request that using file: protocol, most browsers
  // will return status as 0 even though it's a successful request
  if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
    return;
  }
  // readystate handler is calling before onerror or ontimeout handlers,
  // so we should call onloadend on the next 'tick'
  setTimeout(onloadend);
};

实现中:

  1. 首先,保证请求是完成状态(request.readyState === 4,请求有响应或者无响应)
  2. 其次,排除已被处理了的错误事件 abort、timeout 和 error。这 3 个事件都是请求无响应的,最终 request.status 值为 0
  3. 最后,将 onloadend 放入 setTimeout 中,因为 onreadystatechange 事件是在之前触发的,为了保证 onloaded 函数执行是在错误事件处理之后,就用了 setTimeout 放在下一次事件队列中

当然,后面再有 onloaded 标准事件支持之后,就可以替换了。

js 复制代码
if ('onloadend' in request) {
  // Use onloadend if available
  request.onloadend = onloadend;
}

相当于说,onloadend 是 onreadystatechange 的平替。其实这里从语义上讲,直接适用 onload 事件即可,因为我们已经处理完所有的异常场景了。

js 复制代码
// better
request.onload = onloaded

这些说完之后,就可以看看 onloaded 的函数内容了。

js 复制代码
// /v1.6.8/lib/adapters/xhr.js#L92-L121
function onloadend() {
  if (!request) {
    return;
  }
  // Prepare the response
  const responseHeaders = AxiosHeaders.from(
    'getAllResponseHeaders' in request && request.getAllResponseHeaders()
  );
  const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
    request.responseText : request.response;
  
  // 1)
  const response = {
    data: responseData,
    status: request.status,
    statusText: request.statusText,
    headers: responseHeaders,
    config,
    request
  };
  
  // 2)
  settle(function _resolve(value) {
    resolve(value);
    done();
  }, function _reject(err) {
    reject(err);
    done();
  }, response);

  // Clean up request
  request = null;
}

虽然看起来很长,但 onloadend 要做的事情就 2 件。

  1. 组装 reponse 对象。这是 axios 自身自定义的返回对象。比如包含请求响应数据(rresponse.data),还包含像状态码、响应头、请求配置和请求实例(XMLHttpRequest 实例)信息
  2. 判断 response 信息,决定请求成功或是失败。这一步是基于 settle() 工具函数

settle() 工具函数实现如下:

js 复制代码
// /v1.6.8/lib/core/settle.js#L14-L27
export default function settle(resolve, reject, response) {
  const validateStatus = response.config.validateStatus;
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    reject(new AxiosError(
      'Request failed with status code ' + response.status,
      [AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4],
      response.config,
      response.request,
      response
    ));
  }
}

settle 核心逻辑就是判断 response.status 参数值,默认 2xx 会认为是请求成功,否则就是请求失败。

这里借助了 config.validateStatus 工具函数,它的实现是:

js 复制代码
// /v1.6.8/lib/defaults/index.js#L140-L142
validateStatus: function validateStatus(status) {
  return status >= 200 && status < 300;
},

当然,你也可以在发起 axios 请求时自定义。比如:

js 复制代码
axios.get('/user/12345', {
  validateStatus: function (status) {
    return status < 500; // Resolve only if the status code is less than 500
  }
})

当然,针对失败请求,axios 还做了错误信息的区分。

js 复制代码
reject(new AxiosError(
  'Request failed with status code ' + response.status,
  [AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4],
  response.config,
  response.request,
  response
));

[AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4] 表示 4xx 属于 ERR_BAD_REQUEST 信息,5xx 错误属于 ERR_BAD_RESPONSE 信息。

回到 onloaded 中调用 settle 的地方。

js 复制代码
// /v1.6.8/lib/adapters/xhr.js#L111-L117
settle(function _resolve(value) {
  resolve(value);
  done();
}, function _reject(err) {
  reject(err);
  done();
}, response);

会看到这里还有一个 done 的收尾工作。它的作用就是清理之前传入 config.signal 时注册的事件。

js 复制代码
// /v1.6.8/lib/adapters/xhr.js#L54-L62
function done() {
  if (config.signal) {
    config.signal.removeEventListener('abort', onCanceled);
  }
}

至此,我们就讲完了 axios 的浏览器端所有实现的代码。

总结

本文我们讲解了 axios 在浏览器端的实现。axios 内部是通过适配器模式,同时支持在浏览器端和 Node.js 端使用。其中,浏览端是使用 XMLHttpRequest API 封装实现的。

axios 内部首先使用 new XMLHttpRequest() 创建了一个实例 request,继而先后注册了 abort、timeout、error 和 onloaded 事件处理函数进行处理。

  • abort、timeout、error 针对的是请求无响应的报错场景。
  • onloaded 则是用来处理请求有响应的处理(如果不考虑向前兼容,可以使用 onload 替代)

在请求完成处理函数 onloadend() 内部,会根据 response.status + config.validateStatus() 方式判断请求成功还是失败。

希望本文的内容对大家理解 axios 在浏览器端的表现有更深的理解。感谢你的阅读,再见。

相关推荐
gopher9511几秒前
HTML详解
前端·html
Tiny20171 分钟前
前端模块化CommonJs、ESM、AMD总结
前端
吕永强3 分钟前
CSS相关属性和显示模式
前端·css·css3
结衣结衣.8 分钟前
python中的函数介绍
java·c语言·开发语言·前端·笔记·python·学习
全栈技术负责人9 分钟前
前端提升方向
前端
赵锦川9 分钟前
css三角形:css画箭头向下的三角形
前端·css
qbbmnnnnnn14 分钟前
【WebGis开发 - Cesium】如何确保Cesium场景加载完毕
前端·javascript·vue.js·gis·cesium·webgis·三维可视化开发
f8979070701 小时前
layui动态表格出现 横竖间隔线
前端·javascript·layui
鱼跃鹰飞1 小时前
Leecode热题100-295.数据流中的中位数
java·服务器·开发语言·前端·算法·leetcode·面试
二十雨辰2 小时前
[uni-app]小兔鲜-04推荐+分类+详情
前端·javascript·uni-app