Axios源码解析(三):适配器

720 阅读7分钟

上篇 Axios 源码解析(二):通用工具方法 解析了通用工具方法部分的源码。下面继续解析适配器部分的代码,也就是 /adapters 目录。

github.com/MageeLin/ax… 中的analysis分支可以看到当前已解析完的文件。

/adapters

《Axios 源码解析(一):模块分解》中已经分析过,/adapters 目录中包含如下这些文件:

├─adapters
│      http.js
│      README.md
│      xhr.js

同样在 README.md 中,介绍了该目录的作用:

adapters/ 下的模块负责发送请求并在收到响应后处理返回的 Promise 。也就是整个 Axios 中负责对外的部分。

var settle = require('./../core/settle');

module.exports = function myAdapter(config) {
  // 在此时:
  // - 配置已与默认配置合并
  // - 请求转换器已经执行
  // - 请求拦截器已经执行

  /*
   * ------
   */
  // 使用提供的配置发出请求
  // 根据响应来 settle Promise
  return new Promise(function (resolve, reject) {
    var response = {
      data: responseData,
      status: request.status,
      statusText: request.statusText,
      headers: responseHeaders,
      config: config,
      request: request,
    };

    settle(resolve, reject, response);
    /*
     * ------
     */

    // 从这里开始:
    //  - 响应转换器开始执行
    //  - 响应拦截器开始执行
  });
};

由于 Axios 的使用环境其实就是两种,一种是在浏览器端发送 XHR 请求,另一种是在 nodejs 中发送 http 请求。所以 Axios 的适配器只有两个:http.jsxhr.js

xhr.js

在浏览器环境中,Axios 直接封装的 XMLHttpRequest,流程大致如下所示:

  1. 新建一个 XHR 对象
  2. 解析 URL
  3. 处理 dataheadersresponseType
  4. 设置超时时间
  5. 打开请求
  6. 添加 onloadendonloadendonabortonerrorontimeoutonUploadProgress 事件
  7. 发送请求

xhr 适配器的具体代码如下:

'use strict';

var utils = require('./../utils');
var settle = require('./../core/settle');
var cookies = require('./../helpers/cookies');
var buildURL = require('./../helpers/buildURL');
var buildFullPath = require('../core/buildFullPath');
var parseHeaders = require('./../helpers/parseHeaders');
var isURLSameOrigin = require('./../helpers/isURLSameOrigin');
var createError = require('../core/createError');

/**
 * @description: 浏览器环境中使用XHR对象来发送请求
 * @param {Object} config 已经合并并且标准化后的配置对象
 * @return {Promise} 返回一个promise对象
 */
module.exports = function xhrAdapter(config) {
  // 标准的新建Promise对象的写法
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // 拿到data,headers,和responseType
    var requestData = config.data;
    var requestHeaders = config.headers;
    var responseType = config.responseType;

    if (utils.isFormData(requestData)) {
      delete requestHeaders['Content-Type']; // 删掉content-type,让浏览器来设置
    }

    // 新建一个XHR对象
    var request = new XMLHttpRequest();

    // HTTP basic 认证
    if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password
        ? unescape(encodeURIComponent(config.auth.password))
        : '';
      // 编码base64字符串,构造出一个Authorization
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }

    // 构造全路径
    var fullPath = buildFullPath(config.baseURL, config.url);
    // 打开请求
    request.open(
      config.method.toUpperCase(),
      buildURL(fullPath, config.params, config.paramsSerializer),
      true
    );

    // 设置毫秒级的超时时间限制
    request.timeout = config.timeout;

    /**
     * @description: 设置loadend的回调
     */
    function onloadend() {
      if (!request) {
        return;
      }
      // 响应头处理
      var responseHeaders =
        'getAllResponseHeaders' in request
          ? parseHeaders(request.getAllResponseHeaders())
          : null;
      // 响应内容处理
      var responseData =
        !responseType || responseType === 'text' || responseType === 'json'
          ? request.responseText
          : request.response;
      // 构造出response
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request,
      };

      // 调用settle方法来处理promise
      settle(resolve, reject, response);

      // 清空request
      request = null;
    }

    // 如果request上有onloadend属性,则直接替换
    if ('onloadend' in request) {
      request.onloadend = onloadend;
    } else {
      // 否则就用onreadystatechange来模拟onloadend
      request.onreadystatechange = function handleLoad() {
        if (!request || request.readyState !== 4) {
          return;
        }

        // 请求出错,我们没有得到响应,这将由 onerror 处理。
        // 但只有一个例外:请求使用 file:协议,此时即使它是一个成功的请求,大多数浏览器也将返回状态为 0,
        if (
          request.status === 0 &&
          !(request.responseURL && request.responseURL.indexOf('file:') === 0)
        ) {
          return;
        }
        // readystate 处理器在 onerror 或 ontimeout处理器之前调用, 因此我们应该在next 'tick' 上调用onloadend
        setTimeout(onloadend);
      };
    }

    // 处理浏览器对request的取消(与手动取消不同)
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }

      reject(createError('Request aborted', config, 'ECONNABORTED', request));

      // 清空request
      request = null;
    };

    // 处理更低级别的网络错误
    request.onerror = function handleError() {
      // 真正的错误被浏览器掩盖了
      // onerror应当只可被网络错误触发
      reject(createError('Network Error', config, null, request));

      // 清空request
      request = null;
    };

    // 处理超时
    request.ontimeout = function handleTimeout() {
      var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
      if (config.timeoutErrorMessage) {
        timeoutErrorMessage = config.timeoutErrorMessage;
      }
      reject(
        createError(
          timeoutErrorMessage,
          config,
          config.transitional && config.transitional.clarifyTimeoutError
            ? 'ETIMEDOUT'
            : 'ECONNABORTED',
          request
        )
      );

      // 清空request
      request = null;
    };

    // 添加 xsrf 头
    // 只能在浏览器环境中生效
    // 在工作者线程或者RN中不生效
    if (utils.isStandardBrowserEnv()) {
      // 添加 xsrf 头
      var xsrfValue =
        (config.withCredentials || isURLSameOrigin(fullPath)) &&
        config.xsrfCookieName
          ? cookies.read(config.xsrfCookieName)
          : undefined;

      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }

    // 给request添加headers
    if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (
          typeof requestData === 'undefined' &&
          key.toLowerCase() === 'content-type'
        ) {
          // 如果data是undefined,则移除Content-Type
          delete requestHeaders[key];
        } else {
          // 否则把header添加给request
          request.setRequestHeader(key, val);
        }
      });
    }

    // 添加withCredentials
    if (!utils.isUndefined(config.withCredentials)) {
      request.withCredentials = !!config.withCredentials;
    }

    // 添加 responseType
    if (responseType && responseType !== 'json') {
      request.responseType = config.responseType;
    }

    // 处理progess
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
    }

    // 不是所有的浏览器都支持上传事件
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
    }

    // 处理手动取消
    if (config.cancelToken) {
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }

        request.abort();
        reject(cancel);
        // 清空request
        request = null;
      });
    }

    if (!requestData) {
      requestData = null;
    }

    // 发送请求
    request.send(requestData);
  });
};

http.js

nodejs 环境中,Axios 封装的是 http 库,流程大致如下所示:

  1. 转换数据格式
  2. 处理代理
  3. 解析 URL
  4. 创建请求
  5. 添加 errorenddata 等事件
  6. 发送请求

http 适配器的具体代码如下:

'use strict';

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');
var zlib = require('zlib');
var pkg = require('./../../package.json');
var createError = require('../core/createError');
var enhanceError = require('../core/enhanceError');

var isHttps = /https:?/;

/**
 * @description 设置代理用的方法
 * @param {http.ClientRequestArgs} options
 * @param {AxiosProxyConfig} proxy
 * @param {string} location
 */
function setProxy(options, proxy, location) {
  options.hostname = proxy.host;
  options.host = proxy.host;
  options.port = proxy.port;
  options.path = location;

  // basic形式的Proxy-Authorization头
  if (proxy.auth) {
    var base64 = Buffer.from(
      proxy.auth.username + ':' + proxy.auth.password,
      'utf8'
    ).toString('base64');
    options.headers['Proxy-Authorization'] = 'Basic ' + base64;
  }

  // 如果使用了代理,那么重定向时必须要经过代理
  options.beforeRedirect = function beforeRedirect(redirection) {
    redirection.headers.host = redirection.host;
    setProxy(redirection, proxy, redirection.href);
  };
}

/*eslint consistent-return:0*/
module.exports = function httpAdapter(config) {
  return new Promise(function dispatchHttpRequest(
    resolvePromise,
    rejectPromise
  ) {
    var resolve = function resolve(value) {
      resolvePromise(value);
    };
    var reject = function reject(value) {
      rejectPromise(value);
    };
    var data = config.data;
    var headers = config.headers;

    // 设置 User-Agent (某些服务端强制要求)
    // 详见 https://github.com/axios/axios/issues/69
    if ('User-Agent' in headers || 'user-agent' in headers) {
      // 当不需要UA头时
      if (!headers['User-Agent'] && !headers['user-agent']) {
        delete headers['User-Agent'];
        delete headers['user-agent'];
      }
      // 当需要UA头时就指定一个
    } else {
      // 只有在config中没有指定UA时才设置
      headers['User-Agent'] = 'axios/' + pkg.version;
    }

    // 转换数据格式
    if (data && !utils.isStream(data)) {
      if (Buffer.isBuffer(data)) {
        // 什么都不做...
      } else if (utils.isArrayBuffer(data)) {
        data = Buffer.from(new Uint8Array(data));
      } else if (utils.isString(data)) {
        data = Buffer.from(data, 'utf-8');
      } else {
        return reject(
          createError(
            'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream',
            config
          )
        );
      }

      // 如果data存在,则需要设置Content-Length
      headers['Content-Length'] = data.length;
    }

    // HTTP basic authentication
    var auth = undefined;
    if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password || '';
      auth = username + ':' + password;
    }

    // 解析 url
    var fullPath = buildFullPath(config.baseURL, config.url);
    var parsed = url.parse(fullPath);
    var protocol = parsed.protocol || 'http:';

    if (!auth && parsed.auth) {
      var urlAuth = parsed.auth.split(':');
      var urlUsername = urlAuth[0] || '';
      var urlPassword = urlAuth[1] || '';
      auth = urlUsername + ':' + urlPassword;
    }

    if (auth) {
      delete headers.Authorization;
    }

    var isHttpsRequest = isHttps.test(protocol);
    var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;

    var options = {
      path: buildURL(
        parsed.path,
        config.params,
        config.paramsSerializer
      ).replace(/^\?/, ''),
      method: config.method.toUpperCase(),
      headers: headers,
      agent: agent,
      agents: { http: config.httpAgent, https: config.httpsAgent },
      auth: auth,
    };

    if (config.socketPath) {
      options.socketPath = config.socketPath;
    } else {
      options.hostname = parsed.hostname;
      options.port = parsed.port;
    }

    var proxy = config.proxy;
    if (!proxy && proxy !== false) {
      var proxyEnv = protocol.slice(0, -1) + '_proxy';
      var proxyUrl =
        process.env[proxyEnv] || process.env[proxyEnv.toUpperCase()];
      if (proxyUrl) {
        var parsedProxyUrl = url.parse(proxyUrl);
        var noProxyEnv = process.env.no_proxy || process.env.NO_PROXY;
        var shouldProxy = true;

        if (noProxyEnv) {
          var noProxy = noProxyEnv.split(',').map(function trim(s) {
            return s.trim();
          });

          shouldProxy = !noProxy.some(function proxyMatch(proxyElement) {
            if (!proxyElement) {
              return false;
            }
            if (proxyElement === '*') {
              return true;
            }
            if (
              proxyElement[0] === '.' &&
              parsed.hostname.substr(
                parsed.hostname.length - proxyElement.length
              ) === proxyElement
            ) {
              return true;
            }

            return parsed.hostname === proxyElement;
          });
        }

        if (shouldProxy) {
          proxy = {
            host: parsedProxyUrl.hostname,
            port: parsedProxyUrl.port,
            protocol: parsedProxyUrl.protocol,
          };

          if (parsedProxyUrl.auth) {
            var proxyUrlAuth = parsedProxyUrl.auth.split(':');
            proxy.auth = {
              username: proxyUrlAuth[0],
              password: proxyUrlAuth[1],
            };
          }
        }
      }
    }

    // 如果代理存在时要进行专门处理
    if (proxy) {
      options.headers.host =
        parsed.hostname + (parsed.port ? ':' + parsed.port : '');
      setProxy(
        options,
        proxy,
        protocol +
          '//' +
          parsed.hostname +
          (parsed.port ? ':' + parsed.port : '') +
          options.path
      );
    }

    var transport;
    var isHttpsProxy =
      isHttpsRequest && (proxy ? isHttps.test(proxy.protocol) : true);
    if (config.transport) {
      transport = config.transport;
    } else if (config.maxRedirects === 0) {
      transport = isHttpsProxy ? https : http;
    } else {
      if (config.maxRedirects) {
        options.maxRedirects = config.maxRedirects;
      }
      transport = isHttpsProxy ? httpsFollow : httpFollow;
    }

    if (config.maxBodyLength > -1) {
      options.maxBodyLength = config.maxBodyLength;
    }

    // 创建 request
    var req = transport.request(options, function handleResponse(res) {
      if (req.aborted) return;

      // 在需要的情况下,自动解压响应体
      var stream = res;

      // 如果重定向,则返回最后一次请求的信息
      var lastRequest = res.req || req;

      // 如果没有内容, HEAD请求禁止解压
      if (
        res.statusCode !== 204 &&
        lastRequest.method !== 'HEAD' &&
        config.decompress !== false
      ) {
        switch (res.headers['content-encoding']) {
          /*eslint default-case:0*/
          case 'gzip':
          case 'compress':
          case 'deflate':
            // 给处理流程中添加未压缩的body stream
            stream = stream.pipe(zlib.createUnzip());

            // 移除content-encoding, 目的是避免拒绝下载操作
            delete res.headers['content-encoding'];
            break;
        }
      }

      var response = {
        status: res.statusCode,
        statusText: res.statusMessage,
        headers: res.headers,
        config: config,
        request: lastRequest,
      };

      if (config.responseType === 'stream') {
        response.data = stream;
        settle(resolve, reject, response);
      } else {
        var responseBuffer = [];
        var totalResponseBytes = 0;
        stream.on('data', function handleStreamData(chunk) {
          responseBuffer.push(chunk);
          totalResponseBytes += chunk.length;

          // 确保内容长度不超过指定的最大长度
          if (
            config.maxContentLength > -1 &&
            totalResponseBytes > config.maxContentLength
          ) {
            stream.destroy();
            reject(
              createError(
                'maxContentLength size of ' +
                  config.maxContentLength +
                  ' exceeded',
                config,
                null,
                lastRequest
              )
            );
          }
        });

        stream.on('error', function handleStreamError(err) {
          if (req.aborted) return;
          reject(enhanceError(err, config, null, lastRequest));
        });

        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);
            }
          }

          response.data = responseData;
          settle(resolve, reject, response);
        });
      }
    });

    // 处理错误
    req.on('error', function handleRequestError(err) {
      if (req.aborted && err.code !== 'ERR_FR_TOO_MANY_REDIRECTS') return;
      reject(enhanceError(err, config, null, req));
    });

    // 处理请求超时
    if (config.timeout) {
      // 如果`req`接口无法处理其他类型,将强制用一个整数的超时时间。
      var timeout = parseInt(config.timeout, 10);

      if (isNaN(timeout)) {
        reject(
          createError(
            'error trying to parse `config.timeout` to int',
            config,
            'ERR_PARSE_TIMEOUT',
            req
          )
        );

        return;
      }

      // 有时响应将非常缓慢,甚至没有响应,连接事件将被事件循环系统打断
      // 此时触发定时器回调,在连接前将调用abort(),然后获取"socket hang up" 和ECONNRESET码
      // 此时,如果出现大量的请求,nodejs会在幕后挂起一些socket。并且数目会不断增长。
      // 然后这些挂起的socket将一点点占用 CPU。
      // ClientRequest.setTimeout 将在指定毫秒内启动,并且可以确保连接之后触发abort()。
      req.setTimeout(timeout, function handleRequestTimeout() {
        req.abort();
        reject(
          createError(
            'timeout of ' + timeout + 'ms exceeded',
            config,
            config.transitional && config.transitional.clarifyTimeoutError
              ? 'ETIMEDOUT'
              : 'ECONNABORTED',
            req
          )
        );
      });
    }

    if (config.cancelToken) {
      // 处理取消操作
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (req.aborted) return;

        req.abort();
        reject(cancel);
      });
    }

    // 发送请求
    if (utils.isStream(data)) {
      data
        .on('error', function handleStreamError(err) {
          reject(enhanceError(err, config, null, req));
        })
        .pipe(req);
    } else {
      req.end(data);
    }
  });
};

总结

在工程中,把相似但又不同、且可替代的部分抽取出来,形成一个专用的模块,对外的接口统一,这是相当优秀的设计模式。

下一篇 Axios 源码解析(四):核心工具方法(1)来解析 Axios 中耦合度较高的工具方法。