当前位置 : 首页 > 购买指南

Axios请求适配器:XHR与HTTP模块实现详解

<|begin▁of▁sentence|># 1. 前言 在上一篇文章中,我们分析了`axios` 的请求响应流程,以及拦截器的实现原理。我们知道,在发送请求的过程中,配置对象`config` 已经经历了请求拦截器`request interceptor` 和 请求转换`transformRequest` 的处理,那么接下来就该真正地把请求发出去了,而发出请求的正是我们的**请求适配器**。 在`axios` 中,如果用户没有手动配置适配器,那么`axios` 就会根据当前环境来自动选择适配器,**它优先选择`XMLHttpRequest(浏览器端)`,如果没有就选择`http(node端)**`。那么,`axios` 内部是如何来实现的呢?接下来,我们就对这块内容进行分析。 # 2. 如何选择请求适配器 我们通过`axios` 发送请求都是通过`axios.request` 方法,而该方法最终调用的是`dispatchRequest` 方法,在该方法中有这样一段代码,如下: ```javascript // 如果已经配置了适配器,那么就使用配置的适配器 var adapter = config.adapter || defaults.adapter; ``` 从代码中可以看到,`axios` 允许用户手动配置适配器,如果配置了就使用用户配置的适配器,如果没有配置就使用默认的适配器`defaults.adapter`。而默认的适配器是在`lib/defaults.js` 文件中,我们看看它内部是如何定义的,如下: ```javascript function getDefaultAdapter() { var adapter; // 判断XMLHttpRequest对象是否存在,如果存在则使用该对象来发送请求 if (typeof XMLHttpRequest !== 'undefined') { // For browsers use XHR adapter adapter = require('./adapters/xhr'); } else if ( typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]' ) { // For node use HTTP adapter adapter = require('./adapters/http'); } return adapter; } var defaults = { adapter: getDefaultAdapter(), // ... }; ``` 从代码中可以看到:`axios` 会判断当前环境是否存在`XMLHttpRequest` 对象,如果存在就使用`XHR` 适配器来发送请求,如果不存在就判断当前环境是否为`node` 环境,如果是就使用`http` 适配器来发送请求。所以,`axios` 的默认适配器在浏览器端就是`XHR` 适配器,在`node` 端就是`http` 适配器。 # 3. XHR 适配器 `XHR` 适配器的源码是在`lib/adapters/xhr.js` 文件中,由于该文件代码较多,我们就分部分来解读。 ## 3.1 模块引入 首先,该模块引入了一些工具函数,如下: ```javascript var utils = require('./../utils'); var settle = require('./../core/settle'); var buildURL = require('./../helpers/buildURL'); var buildFullPath = require('../core/buildFullPath'); var parseHeaders = require('./../helpers/parseHeaders'); var isURLSameOrigin = require('./../helpers/isURLSameOrigin'); var createCookie = require('./../helpers/createCookie'); ``` - `utils`:工具函数; - `settle`:根据响应状态改变`Promise` 的状态; - `buildURL`:构建带查询参数的`URL`; - `buildFullPath`:构建完整的请求`URL`; - `parseHeaders`:将字符串解析为响应头对象; - `isURLSameOrigin`:判断请求`URL` 是否与当前页面同源; - `createCookie`:创建`cookie`; ## 3.2 导出模块 接下来,该模块导出了一个函数,如下: ```javascript module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { // ... }); }; ``` 从代码中可以看到:该模块导出了一个函数,该函数接收配置对象`config` 作为参数,并且返回一个`promise`。 ## 3.3 函数内部实现 接下来,我们看看该函数内部是如何实现的。 ### 3.3.1 初始化变量 首先,函数内部初始化了一些变量,如下: ```javascript var requestData = config.data; var requestHeaders = config.headers; var responseType = config.responseType; // 如果请求数据是FormData类型,则删除Content-Type请求头,让浏览器自动设置 if (utils.isFormData(requestData)) { delete requestHeaders['Content-Type']; } ``` ### 3.3.2 创建 XHR 对象 接下来,创建`XHR` 对象,如下: ```javascript var request = new XMLHttpRequest(); ``` ### 3.3.3 监听 readystatechange 事件 然后,监听`XHR` 对象的`readystatechange` 事件,如下: ```javascript request.onreadystatechange = function handleLoad() { if (!request || request.readyState !== 4) { return; } // 请求出错或者超时,该事件也会触发,但是此时status为0,所以需要排除 if ( request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0) ) { return; } // 响应头 var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null; // 响应数据 var responseData = !responseType || responseType === 'text' || responseType === 'json' ? request.responseText : request.response; var response = { data: responseData, status: request.status, statusText: request.statusText, headers: responseHeaders, config: config, request: request, }; // 根据响应状态改变Promise的状态 settle(resolve, reject, response); // 清理请求对象,避免内存泄漏 request = null; }; ``` 从代码中可以看到:当`XHR` 对象的`readyState` 变为`4` 时,表示请求已经完成,此时就可以获取响应头和响应数据了。然后,根据响应状态改变`Promise` 的状态。 ### 3.3.4 监听错误事件 接下来,监听`XHR` 对象的错误事件,如下: ```javascript request.onerror = function handleError() { reject(createError('Network Error', config, null, request)); request = null; }; ``` ### 3.3.5 监听超时事件 然后,监听`XHR` 对象的超时事件,如下: ```javascript request.ontimeout = function handleTimeout() { reject( createError( 'timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', request ) ); request = null; }; ``` ### 3.3.6 取消请求 如果配置了取消请求,那么还需要监听取消事件,如下: ```javascript if (config.cancelToken) { config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } request.abort(); reject(cancel); request = null; }); } ``` ### 3.3.7 发送请求 最后,发送请求,如下: ```javascript request.send(requestData); ``` ## 3.4 完整代码 以上就是`XHR` 适配器的完整实现,由于代码较长,这里就不贴完整代码了,有兴趣的同学可以自行查看源码。 # 4. http 适配器 `http` 适配器的源码是在`lib/adapters/http.js` 文件中,由于该文件代码较多,我们就分部分来解读。 ## 4.1 模块引入 首先,该模块引入了一些工具函数,如下: ```javascript var utils = require('./../utils'); var settle = require('./../core/settle'); var buildFullPath = require('../core/buildFullPath'); var buildURL = require('./../helpers/buildURL'); var http = require('http'); var https = require('https'); var httpFollow = require('follow-redirects').http; var httpsFollow = require('follow-redirects').https; var URL = require('url').URL; var zlib = require('zlib'); var VERSION = require('./../env/data').version; var createError = require('../core/createError'); var enhanceError = require('../core/enhanceError'); var defaults = require('../defaults'); var Cancel = require('../cancel/Cancel'); ``` - `utils`:工具函数; - `settle`:根据响应状态改变`Promise` 的状态; - `buildFullPath`:构建完整的请求`URL`; - `buildURL`:构建带查询参数的`URL`; - `http`:`node` 的`http` 模块; - `https`:`node` 的`https` 模块; - `httpFollow`:支持重定向的`http` 模块; - `httpsFollow`:支持重定向的`https` 模块; - `URL`:`node` 的`URL` 模块; - `zlib`:`node` 的`zlib` 模块,用于解压缩; - `VERSION`:`axios` 的版本号; - `createError`:创建错误对象; - `enhanceError`:增强错误对象; - `defaults`:默认配置; - `Cancel`:取消请求的类; ## 4.2 导出模块 接下来,该模块导出了一个函数,如下: ```javascript module.exports = function httpAdapter(config) { return new Promise(function dispatchHttpRequest( resolvePromise, rejectPromise ) { // ... }); }; ``` 从代码中可以看到:该模块导出了一个函数,该函数接收配置对象`config` 作为参数,并且返回一个`promise`。 ## 4.3 函数内部实现 接下来,我们看看该函数内部是如何实现的。 ### 4.3.1 初始化变量 首先,函数内部初始化了一些变量,如下: ```javascript var resolve = function resolve(value) { resolvePromise(value); }; var reject = function reject(value) { rejectPromise(value); }; var data = config.data; var headers = config.headers; // 如果用户设置了User-Agent请求头,则使用用户设置的,否则使用默认的 if (!headers['User-Agent'] && !headers['user-agent']) { headers['User-Agent'] = 'axios/' + VERSION; } ``` ### 4.3.2 解析 URL 接下来,解析`URL`,如下: ```javascript var parsed = new URL(config.url); var protocol = parsed.protocol || 'http:'; ``` ### 4.3.3 判断是否是 https 然后,判断是否是`https`,如下: ```javascript var isHttps = protocol === 'https:'; ``` ### 4.3.4 选择 http 模块 接下来,选择`http` 模块,如下: ```javascript var http = isHttps ? https : http; ``` ### 4.3.5 处理重定向 然后,处理重定向,如下: ```javascript if (config.maxRedirects) { http = isHttps ? httpsFollow : httpFollow; } ``` ### 4.3.6 创建请求选项 接下来,创建请求选项,如下: ```javascript var options = { path: buildURL(parsed.pathname, config.params, config.paramsSerializer), method: config.method.toUpperCase(), headers: headers, agent: config.agent, auth: config.auth, // ... }; ``` ### 4.3.7 发送请求 然后,发送请求,如下: ```javascript var req = http.request(options, function handleResponse(res) { // ... }); ``` ### 4.3.8 处理响应 接下来,处理响应,如下: ```javascript var stream = res; // 如果响应是gzip压缩的,则解压缩 if (res.headers['content-encoding'] === 'gzip') { stream = res.pipe(zlib.createGunzip()); } // 响应数据 var responseBuffer = []; stream.on('data', function handleStreamData(chunk) { responseBuffer.push(chunk); }); stream.on('error', function handleStreamError(err) { if (req.aborted) return; reject(enhanceError(err, config, null, req)); }); stream.on('end', function handleStreamEnd() { var responseData = Buffer.concat(responseBuffer); if (config.responseType !== 'arraybuffer') { responseData = responseData.toString(config.responseEncoding); if (!config.responseEncoding || config.responseEncoding === 'utf8') { responseData = utils.stripBOM(responseData); } } var response = { data: responseData, status: res.statusCode, statusText: res.statusMessage, headers: res.headers, config: config, request: req, }; settle(resolve, reject, response); }); ``` ### 4.3.9 处理错误 然后,处理错误,如下: ```javascript req.on('error', function handleRequestError(err) { if (req.aborted && err.code !== 'ERR_CANCELED') { return; } reject(enhanceError(err, config, null, req)); }); ``` ### 4.3.10 处理超时 接下来,处理超时,如下: ```javascript if (config.timeout) { req.setTimeout(config.timeout, function handleTimeout() { req.abort(); reject( createError( 'timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req ) ); }); } ``` ### 4.3.11 取消请求 如果配置了取消请求,那么还需要监听取消事件,如下: ```javascript if (config.cancelToken) { config.cancelToken.promise.then(function onCanceled(cancel) { if (req.aborted) { return; } req.abort(); reject(cancel); }); } ``` ### 4.3.12 发送数据 最后,发送数据,如下: ```javascript req.end(data); ``` ## 4.4 完整代码 以上就是`http` 适配器的完整实现,由于代码较长,这里就不贴完整代码了,有兴趣的同学可以自行查看源码。 # 5. 总结 本篇文章我们分析了`axios` 的请求适配器,包括`XHR` 适配器和`http` 适配器。通过分析,我们知道了`axios` 是如何根据当前环境来选择适配器的,以及`XHR` 适配器和`http` 适配器的内部实现。 在下一篇文章中,我们将分析`axios` 的取消请求功能,看看`axios` 是如何实现取消请求的。

栏目列表