默认适配器

axios/lib/adapters/http.js

核心是nodejs的http(s).request方法进行请求

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:?/;

我们先看看里面引用的一些库作用

作用
httphttp请求库
httpshttps请求库
follow-redirects替代nodejs的http和https模块,自动跟随重定向。
url解析和格式化url
zlib简单,同步压缩或解压node.js buffers.

里面还有其他的内置模块

axios/lib/core/settle.js

var createError = require('./createError');/** * Resolve or reject a Promise based on response status. * * @param {Function} resolve A function that resolves the promise. * @param {Function} reject A function that rejects the promise. * @param {object} response The response. */module.exports = function settle(resolve, reject, response) {  var validateStatus = response.config.validateStatus;  if (!validateStatus || validateStatus(response.status)) {    resolve(response);  } else {    reject(createError(      'Request failed with status code ' + response.status,      response.config,      null,      response.request,      response    ));  }};

在得到响应请求的基础上,决定返回成功或者失败的Promise态,当失败的时候会创建自定义错误

axios/lib/core/createError.js

var enhanceError = require('./enhanceError');/** * Create an Error with the specified message, config, error code, request and response. * * @param {string} message The error message. * @param {Object} config The config. * @param {string} [code] The error code (for example, 'ECONNABORTED'). * @param {Object} [request] The request. * @param {Object} [response] The response. * @returns {Error} The created error. */module.exports = function createError(message, config, code, request, response) {  var error = new Error(message);  return enhanceError(error, config, code, request, response);};

有点中转站的意思,只负责创建错误对象,至于修改的任务则交给enhanceError函数

axios/lib/core/enhanceError.js

/** * Update an Error with the specified config, error code, and response. * * @param {Error} error The error to update. * @param {Object} config The config. * @param {string} [code] The error code (for example, 'ECONNABORTED'). * @param {Object} [request] The request. * @param {Object} [response] The response. * @returns {Error} The error. */module.exports = function enhanceError(error, config, code, request, response) {  error.config = config;  if (code) {    error.code = code;  }  error.request = request;  error.response = response;  error.isAxiosError = true;  error.toJSON = function() {    return {      // Standard      message: this.message,      name: this.name,      // Microsoft      description: this.description,      number: this.number,      // Mozilla      fileName: this.fileName,      lineNumber: this.lineNumber,      columnNumber: this.columnNumber,      stack: this.stack,      // Axios      config: this.config,      code: this.code    };  };  return error;};

返回包装传入的错误对象,尽可能多得信息赋值到错误对象上,其中包括部分浏览器才提供的错误信息

/*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;    // Set User-Agent (required by some servers)    // Only set header if it hasn't been set in config    // See https://github.com/axios/axios/issues/69    if (!headers['User-Agent'] && !headers['user-agent']) {      headers['User-Agent'] = 'axios/' + pkg.version;    }    // ---省略部分代码---  });};

传入请求的配置信息,返回新的Promise,并且将改变状态的触发函数赋值到变量,当请求头不包含User-Agent或者user-agent的时候默认赋值axios的特有信息,pkg是axios的package.json.

/*eslint consistent-return:0*/module.exports = function httpAdapter(config) {  return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {    // ---省略部分代码---        if (data && !utils.isStream(data)) {      if (Buffer.isBuffer(data)) {        // Nothing to do...      } 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        ));      }      // Add Content-Length header if data exists      headers['Content-Length'] = data.length;    }        // ---省略部分代码---  });};

在有data数据并且不为流的情况下,根据data的数据类型做对应转换并设置Content-Lengthdata的长度,都不符合的情况下返回错误态Promise并且中断流程.

/*eslint consistent-return:0*/module.exports = function httpAdapter(config) {  return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {    // ---省略部分代码---        // HTTP basic authentication    var auth = undefined;    if (config.auth) {      var username = config.auth.username || '';      var password = config.auth.password || '';      auth = username + ':' + password;    }    // Parse 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: {        httpsAgent: config.httpsAgent,        httpAgent: config.httpAgent      },      auth: auth    };        if (config.socketPath) {      options.socketPath = config.socketPath;    } else {      options.hostname = parsed.hostname;      options.port = parsed.port;    }        // ---省略部分代码---  });};
  1. 如果配置含有auth,则拼接用户名和密码,可以为空;
  2. 拼装完整的请求地址和获得请求协议;
  3. 有种情况验证信息不包含在配置而在请求地址上需要做兼容处理;
  4. 如果有auth信息的情况下要删除Authorization头,即"用户名+冒号+密码"用BASE64算法加密后的字符串;
  5. 如果是https协议下获取配置的httpsAgent信息,否则拿httpAgent信息;
  6. 将所有信息放入options中;
  7. 设置socket路径或者hostnameport;

常规验证流程

HTTP Authorization请求标头包含用于向服务器认证用户代理的凭证,通常在服务器响应401 Unauthorized状态和WWW-Authenticate标题后。

当服务器收到请求的时候,当设置了需要验证信息,如果请求头带有Authorization,会检查里面的内容是否在用户列表中

如果请求头带有Authorization,会检查里面的内容是否在用户列表中

  • 有并且验证通过则返回正常响应
  • 否则返回401状态码,浏览器会弹出对话框让用户输入账号密码

上面还提到几个方法,分别是

axios/lib/core/buildFullPath.js

var isAbsoluteURL = require('../helpers/isAbsoluteURL');var combineURLs = require('../helpers/combineURLs');/** * Creates a new URL by combining the baseURL with the requestedURL, * only when the requestedURL is not already an absolute URL. * If the requestURL is absolute, this function returns the requestedURL untouched. * * @param {string} baseURL The base URL * @param {string} requestedURL Absolute or relative URL to combine * @returns {string} The combined full path */module.exports = function buildFullPath(baseURL, requestedURL) {  if (baseURL && !isAbsoluteURL(requestedURL)) {    return combineURLs(baseURL, requestedURL);  }  return requestedURL;};

返回完整且规范的绝对地址

axios/lib/helpers/isAbsoluteURL.js

/** * Determines whether the specified URL is absolute * * @param {string} url The URL to test * @returns {boolean} True if the specified URL is absolute, otherwise false */module.exports = function isAbsoluteURL(url) {  // A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).  // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed  // by any combination of letters, digits, plus, period, or hyphen.  return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);};

正则判断是否绝对路径

axios/lib/helpers/combineURLs.js

/** * Creates a new URL by combining the specified URLs * * @param {string} baseURL The base URL * @param {string} relativeURL The relative URL * @returns {string} The combined URL */module.exports = function combineURLs(baseURL, relativeURL) {  return relativeURL    ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')    : baseURL;};

拼装并且规范合并地址

axios/lib/helpers/buildURL.js

var utils = require('./../utils');function encode(val) {  return encodeURIComponent(val).    replace(/%40/gi, '@').    replace(/%3A/gi, ':').    replace(/%24/g, '$').    replace(/%2C/gi, ',').    replace(/%20/g, '+').    replace(/%5B/gi, '[').    replace(/%5D/gi, ']');}/** * Build a URL by appending params to the end * * @param {string} url The base of the url (e.g., http://www.google.com) * @param {object} [params] The params to be appended * @returns {string} The formatted url */module.exports = function buildURL(url, params, paramsSerializer) {  /*eslint no-param-reassign:0*/  if (!params) {    return url;  }  var serializedParams;  if (paramsSerializer) {    serializedParams = paramsSerializer(params);  } else if (utils.isURLSearchParams(params)) {    serializedParams = params.toString();  } else {    var parts = [];    utils.forEach(params, function serialize(val, key) {      if (val === null || typeof val === 'undefined') {        return;      }      if (utils.isArray(val)) {        key = key + '[]';      } else {        val = [val];      }      utils.forEach(val, function parseValue(v) {        if (utils.isDate(v)) {          v = v.toISOString();        } else if (utils.isObject(v)) {          v = JSON.stringify(v);        }        parts.push(encode(key) + '=' + encode(v));      });    });    serializedParams = parts.join('&');  }  if (serializedParams) {    var hashmarkIndex = url.indexOf('#');    if (hashmarkIndex !== -1) {      url = url.slice(0, hashmarkIndex);    }    url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams;  }  return url;};

我们可以看到除了引入工具模块之后本身模块还自定义了一个encode方法

buildURL里面还有一个paramsSerializer入参,在注释上没看到,但是可以猜测到应该是一个参数序列化的方法

第一步设置serializedParams的流程上有三个分支条件:

  1. 如果有paramsSerializer方法则直接用来处理params参数
  2. 如果paramsURLSearchParams对象就直接调用toString方法
  3. 否则直接调用utils.forEach,根据类型在回调函数做一层转换,最终输出一份&拼接的字符串参数

第二步如果上面能得到serializedParams的情况,根据url规则拼接上去

第三步返回拼接后的url或者原始url

接下来看下面源码

/*eslint consistent-return:0*/module.exports = function httpAdapter(config) {  return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {    // ---省略部分代码---        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);        // no_proxy环境变量        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) {            // 不存在返回false            if (!proxyElement) {              return false;            }            // 通配符返回true            if (proxyElement === '*') {              return true;            }            // 判断proxyElement与请求url的域名是否相等            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          };          if (parsedProxyUrl.auth) {            var proxyUrlAuth = parsedProxyUrl.auth.split(':');            proxy.auth = {              username: proxyUrlAuth[0],              password: proxyUrlAuth[1]            };          }        }      }    }    // 如果有代理配置,添加到options    if (proxy) {      options.hostname = proxy.host;      options.host = proxy.host;      options.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : '');      options.port = proxy.port;      options.path = protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path;      // 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;      }    }        // ---省略部分代码---  });};

这块代码属于定义代理服务器,示例如下

proxy: {    host: '127.0.0.1',    port: 9000,    auth: {        username: 'mikeymike',        password: 'rapunz3l'    }},

具体做了什么可以一目了然

/*eslint consistent-return:0*/module.exports = function httpAdapter(config) {  return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {    // ---省略部分代码---        var transport;    // 是否https代理    var isHttpsProxy = isHttpsRequest && (proxy ? isHttps.test(proxy.protocol) : true);    // 如果配置了直接使用    if (config.transport) {      transport = config.transport;    } else if (config.maxRedirects === 0) {      // 最大重定向次数为0判断使用https模块还是http模块      transport = isHttpsProxy ? https : http;    } else {      // 如果允许重定向      if (config.maxRedirects) {        options.maxRedirects = config.maxRedirects;      }      // 直接判断使用https重定向模块还是http重定向模块      transport = isHttpsProxy ? httpsFollow : httpFollow;    }    // 如果设置了长度并且大于-1则添加到options上    if (config.maxContentLength && config.maxContentLength > -1) {      options.maxBodyLength = config.maxContentLength;    }        // ---省略部分代码---  });};

根据协议决定使用对应的请求库,并且设定最大重定向次数和请求内容长度

/*eslint consistent-return:0*/module.exports = function httpAdapter(config) {  return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {    // ---省略部分代码---        // Create the request    var req = transport.request(options, function handleResponse(res) {      // 如果终止则中断流程      if (req.aborted) return;      // uncompress the response body transparently if required      var stream = res;      switch (res.headers['content-encoding']) {      /*eslint default-case:0*/      case 'gzip':      case 'compress':      case 'deflate':        // add the unzipper to the body stream processing pipeline        stream = (res.statusCode === 204) ? stream : stream.pipe(zlib.createUnzip());        // remove the content-encoding in order to not confuse downstream operations        delete res.headers['content-encoding'];        break;      }      // return the last request in case of redirects      var lastRequest = res.req || req;      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 = [];        stream.on('data', function handleStreamData(chunk) {          responseBuffer.push(chunk);          // make sure the content length is not over the maxContentLength if specified          if (config.maxContentLength > -1 && Buffer.concat(responseBuffer).length > 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);          }          response.data = responseData;          settle(resolve, reject, response);        });      }    });        // ---省略部分代码---  });};

主要做了几件事:

  1. 如果带有压缩指示的content-encoding,根据状态码是否204决定需不需要进行压缩,然后删除头避免混淆后续操作

    • HTTP协议中 204 No Content 成功状态响应码表示目前请求成功,但客户端不需要更新其现有页面
  2. 拼装response对象
  3. 根据responseType决定怎么解析响应数据,然后更新response:

    • stream则直接赋值
    • 否则利用stream事件解析
/*eslint consistent-return:0*/module.exports = function httpAdapter(config) {  return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {    // ---省略部分代码---       // Handle errors    req.on('error', function handleRequestError(err) {      if (req.aborted) return;      reject(enhanceError(err, config, null, req));    });    // Handle request timeout    if (config.timeout) {      // Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system.      // And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET.      // At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up.      // And then these socket which be hang up will devoring CPU little by little.      // ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect.      req.setTimeout(config.timeout, function handleRequestTimeout() {        req.abort();        reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req));      });    }    if (config.cancelToken) {      // Handle cancellation      config.cancelToken.promise.then(function onCanceled(cancel) {        if (req.aborted) return;        req.abort();        reject(cancel);      });    }    // Send the request    if (utils.isStream(data)) {      data.on('error', function handleStreamError(err) {        reject(enhanceError(err, config, null, req));      }).pipe(req);    } else {      req.end(data);    }  });};

对请求失败,超时,取消做对应操作处理,如果data本身是steam类型则直接监听

http adapter源码至此为止了

axios/lib/adapters/xhr.js

核心是浏览器的XMLHttpRequest对象

因为大多数方法都已经在http adapter讲过了,所以这里可以快速过一下

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 createError = require('../core/createError');module.exports = function xhrAdapter(config) {  return new Promise(function dispatchXhrRequest(resolve, reject) {    var requestData = config.data;    var requestHeaders = config.headers;    // 如果是FormData对象删除Content-Type让浏览器自定义    if (utils.isFormData(requestData)) {      delete requestHeaders['Content-Type']; // Let the browser set it    }    // 自己封装最原始的请求    var request = new XMLHttpRequest();    // HTTP basic authentication    if (config.auth) {      var username = config.auth.username || '';      var password = config.auth.password || '';      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);    // Set the request timeout in MS    request.timeout = config.timeout;    // Listen for ready state    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;      }      // Prepare the response      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;      var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;      var response = {        data: responseData,        status: request.status,        statusText: request.statusText,        headers: responseHeaders,        config: config,        request: request      };      settle(resolve, reject, response);      // Clean up request      request = null;    };    // Handle browser request cancellation (as opposed to a manual cancellation)    request.onabort = function handleAbort() {      if (!request) {        return;      }      reject(createError('Request aborted', config, 'ECONNABORTED', request));      // Clean up request      request = null;    };    // 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(createError('Network Error', config, null, request));      // Clean up request      request = null;    };    // Handle timeout    request.ontimeout = function handleTimeout() {      reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',        request));      // Clean up request      request = null;    };    // Add xsrf header    // This is only done if running in a standard browser environment.    // Specifically not if we're in a web worker, or react-native.    if (utils.isStandardBrowserEnv()) {      var cookies = require('./../helpers/cookies');      // Add xsrf header      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?        cookies.read(config.xsrfCookieName) :        undefined;      if (xsrfValue) {        requestHeaders[config.xsrfHeaderName] = xsrfValue;      }    }    // Add headers to the request    if ('setRequestHeader' in request) {      utils.forEach(requestHeaders, function setRequestHeader(val, key) {        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {          // Remove Content-Type if data is undefined          delete requestHeaders[key];        } else {          // Otherwise add header to the request          request.setRequestHeader(key, val);        }      });    }    // Add withCredentials to request if needed    if (config.withCredentials) {      request.withCredentials = true;    }    // Add responseType to request if needed    if (config.responseType) {      try {        request.responseType = config.responseType;      } catch (e) {        // Expected DOMException thrown by browsers not compatible XMLHttpRequest Level 2.        // But, this can be suppressed for 'json' type as it can be parsed by default 'transformResponse' function.        if (config.responseType !== 'json') {          throw e;        }      }    }    // Handle progress if needed    if (typeof config.onDownloadProgress === 'function') {      request.addEventListener('progress', config.onDownloadProgress);    }    // Not all browsers support upload events    if (typeof config.onUploadProgress === 'function' && request.upload) {      request.upload.addEventListener('progress', config.onUploadProgress);    }    if (config.cancelToken) {      // Handle cancellation      config.cancelToken.promise.then(function onCanceled(cancel) {        if (!request) {          return;        }        request.abort();        reject(cancel);        // Clean up request        request = null;      });    }    if (requestData === undefined) {      requestData = null;    }    // Send the request    request.send(requestData);  });};

内容大致一样,就不说了