axios 源码笔记(二)

axios 源码解析

lib/adapter

xhr创建浏览器请求,http用来创建node请求

xhr

  1. xhrAdapter整体来说抛出了一个promise
module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject){});
};

  1. 处理传入的用户配置,如果有data,当data的类型是FormData或者Blob和File,content-type是固定的,因此不使用用户传入的content-type,通过浏览器识别设置。
    FormData、Blob和File区别:
    Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。
    File 接口提供有关文件的信息,并允许网页中的 JavaScript 访问其内容。File 对象是特殊类型的 Blob。
    FormData 用来构造http请求参数的数据结构
    综上所述:File是一种特殊的Blob,Blob可以通过构造FormData实现文件上传。
    var requestData = config.data;
    var requestHeaders = config.headers;

    if (utils.isFormData(requestData)) {
      delete requestHeaders['Content-Type']; // Let the browser set it
    }

    if (
      (utils.isBlob(requestData) || utils.isFile(requestData)) &&
      requestData.type
    ) {
      delete requestHeaders['Content-Type']; // Let the browser set it
    }
  1. 创建一个XMLHttpRequest实例
    几种转码方式对比:
    encodeURIComponent:不转译 A-Z a-z 0-9 - _ . ! ~ * ’ ( )
    encodeURI:不转译:比encodeURIComponent多 ; , / ? : @ & = + $ #
    escape:不转译 @* _ + - . /
    这块为什么先encodeURIComponent后unescape呢?
    首先,最初的版本直接btoa转换base64,如果password包含中文会报错,因此需要encodeURIComponent,中文会被转成16进制。接着unescape,由于escape和encodeURI编码方式不一致,因此十六进制被转化为拉丁文,字母数字还原,猜测可能是因为encode后字符串长度增加,通过unescape部分转化成拉丁文,减少长度。
    var request = new XMLHttpRequest();

    // HTTP basic authentication
    if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
      // 将用户信息转为base64
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }
	// buildFullPath返回了一个完整的URL
    var fullPath = buildFullPath(config.baseURL, config.url);
    // xhrReq.open(method, url, async, user, password);
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

    // Set the request timeout in MS
    request.timeout = config.timeout;
  1. 监听请求发送状态
    // Listen for ready state
    request.onreadystatechange = function handleLoad() {
      if (!request || request.readyState !== 4) {
        return;
      }

      // 在请求完成前,status的值为0。
      // 如果 XMLHttpRequest 出错,浏览器返回的 status 也为0。
      // 当请求本地文件时,请求成功后,大多数浏览器会返回0。
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        return;
      }

      // in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中
      // getAllResponseHeaders为XMLHttpRequest的方法,获取原始的header头
      // responseType:arraybuffer,blob,document,json,text,stream
      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
      };
	  // 处理了promise的resolve,reject和返回值
      settle(resolve, reject, response);

      // Clean up request
      request = null;
    };
  1. 处理取消请求,下面有处理cancelToken,为什么还需要监听onabort?通过github上提交记录发现,当出现循环请求时,chrome会取消一部分请求,这块用来处理非主动取消请求
    // 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;
    };
  1. 监听请求过程中错误及超时,并抛出
    // 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() {
      var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
      if (config.timeoutErrorMessage) {
        timeoutErrorMessage = config.timeoutErrorMessage;
      }
      reject(createError(timeoutErrorMessage, config, 'ECONNABORTED',
        request));

      // Clean up request
      request = null;
    };
  1. 处理xsrf,如果withCredentials或者请求的url同源,并且设置了xsrfCookieName,则在cookie中读取xsrfValue。如果xsrfValue存在,给requestHeaders设置xsrf
    // 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()) {
      // Add xsrf header
      // `withCredentials` 表示跨域请求时是否需要使用凭证,默认false
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;

      if (xsrfValue) {
      	//  the name of the http header that carries the xsrf token value
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }
    // Add withCredentials to request if needed
    // 首先判断withCredentials是否定义,如果直接通过config.withCredentials来判断,那么如果config.withCredentials为false时,无法设置request.withCredentials
    if (!utils.isUndefined(config.withCredentials)) {
      request.withCredentials = !!config.withCredentials;
    }
  1. 处理请求头,如果没有parmas,删除content-type,其他的正常设置
    // 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);
        }
      });
    }
  1. 设置responseType,通过try-catch形式是因为:老版本的XMLHttpRequest只能返回文本类型,不能设置返回类型,但是如果请求返回类型为json,也可以继续执行
    // 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;
        }
      }
    }
  1. 监听上传进度和下载进度,但是不是所有浏览器都支持上传
    // 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);
    }
  1. 设置取消请求,config.cancelToken实质上是cancelToken的实例,cancelToken实例上有promise方法,promise抛出了resolve方法供外部调用。
    if (config.cancelToken) {
      // Handle cancellation
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }

        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }

外部调用方式:

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    userCancel = c;
  })
});

执行顺序:new CancelToken => 构造resolvePromise => 执行executor => CancelToken中的cancel方法赋给userCancel => 用户调用userCancel => 创建 Cancel实例并返回原因 => resolvePromise(config.cancelToken.promise.then)执行 => 调用 onCanceled

  1. 处理请求参数并发送请求
if (!requestData) {
      requestData = null;
    }

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

http

  1. 首先创建了一个promise,并封装了promise的resovle和reject
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);
    };
  });
};
  1. 处理UA和data,如果没有UA,取package的version组成UA。
    if (!headers['User-Agent'] && !headers['user-agent']) {
      headers['User-Agent'] = 'axios/' + pkg.version;
    }
	// 使用http创建请求
	// http 是stream的实例,因此stream格式的数据不用处理
    if (data && !utils.isStream(data)) {
      // Node.js 创建的流都是运作在字符串和 Buffer(或 Uint8Array)上,因此如果是buffer不需要处理,否则要转成buffer
      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;
    }
  1. 处理auth
    var auth = undefined;
    if (config.auth) {
      var username = config.auth.username || '';
      // 这里是否需要转成base64?
      var password = config.auth.password || '';
      auth = username + ':' + password;
    }

    // Parse url
    var fullPath = buildFullPath(config.baseURL, config.url);
    // 获取到url对象,已废弃,使用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;
    }
  1. 请求分为http和https,填写不同的参数
    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;
    }
  1. 构造proxy,proxy定义代理服务器的主机名称和端口
 proxy: {
    host: '127.0.0.1',
    port: 9000,
    auth: {
      username: 'mikeymike',
      password: 'rapunz3l'
    }
  }

auth 表示 HTTP 基础验证应当用于连接代理,并提供凭据,这将会设置一个 Proxy-Authorization 头,覆写掉已有的通过使用 header 设置的自定义 Proxy-Authorization头。
使用“false”来禁用代理,忽略环境变量

    var proxy = config.proxy;
    if (!proxy && proxy !== false) {
      // 生成 http_proxy 或 https_proxy
      var proxyEnv = protocol.slice(0, -1) + '_proxy';
      // 可以使用传统的“http_proxy”和“https_proxy”环境变量
      var proxyUrl = process.env[proxyEnv] || process.env[proxyEnv.toUpperCase()];
      if (proxyUrl) {
      	// 生成代理url对象
        var parsedProxyUrl = url.parse(proxyUrl);
        // 可以定义一个‘no_proxy’环境变量,以逗号分隔不应代理的域列表
        var noProxyEnv = process.env.no_proxy || process.env.NO_PROXY;
        var shouldProxy = true;

        if (noProxyEnv) {
          // 获取不需要代理的url数组,然后去掉空格
          var noProxy = noProxyEnv.split(',').map(function trim(s) {
            return s.trim();
          });
		  // !some 只要存在下面某一项,那么就不需要代理
          shouldProxy = !noProxy.some(function proxyMatch(proxyElement) {
            // 不存在proxyElement直接return
            if (!proxyElement) {
              return false;
            }
            if (proxyElement === '*') {
              return true;
            }
            // 以 .开头说明不包含http协议
            // 截取当前hostname的后半段使其和代理hostname长度一致,判断域名是否一致
            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]
            };
          }
        }
      }
    }
  1. 设置 proxy
    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
      // buffer.toString 支持传入:utf-8,utf16le,latin1,base64,hex,ascii,binary,ucs2,转化成对应的格式
      if (proxy.auth) {
        var base64 = Buffer.from(proxy.auth.username + ':' + proxy.auth.password, 'utf8').toString('base64');
        options.headers['Proxy-Authorization'] = 'Basic ' + base64;
      }
    }
  1. 组装参数,maxRedirects 和 maxBodyLength为follow-redirects参数
    var transport;
    var isHttpsProxy = isHttpsRequest && (proxy ? isHttps.test(proxy.protocol) : true);
    if (config.transport) {
      transport = config.transport;
    } else if (config.maxRedirects === 0) {
      // 如果maxRedirects为0说明不支持重定向,使用原始的http
      transport = isHttpsProxy ? https : http;
    } else {
      if (config.maxRedirects) {
      	// maxRedirects默认值21
        options.maxRedirects = config.maxRedirects;
      }
      transport = isHttpsProxy ? httpsFollow : httpFollow;
    }

    if (config.maxBodyLength > -1) {
      // 最大请求体长度,默认10mb
      options.maxBodyLength = config.maxBodyLength;
    }
  1. 创建请求
    var req = transport.request(options, function handleResponse(res) {
      if (req.aborted) return;

      // uncompress the response body transparently if required
      var stream = res;

      // return the last request in case of redirects
      var lastRequest = res.req || req;


      // http method HEAD:接口返回200,HEAD方法允许客户端仅向服务器请求某个资源的响应头,而不会下载资源,和GET相比,减少了响应体
      // http code 204:请求成功,但没有数据返回,浏览器不用刷新页面,也不用导向新的页面
      // 这些情况下,无需压缩response
      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':
        // add the unzipper to the body stream processing pipeline
          stream = stream.pipe(zlib.createUnzip());

          // remove the content-encoding in order to not confuse downstream operations
          delete res.headers['content-encoding'];
          break;
        }
      }

      var response = {
        status: res.statusCode,
        statusText: res.statusMessage,
        headers: res.headers,
        config: config,
        request: lastRequest
      };
	  // http 是stream的实例,因此不需要处理
	  // stream:运行在字符串和buffer(或Uint8Array)上,stream实质上是一个过程,从一个文件读取,输出到另一个文件。在传输较大的数据时,大数据会被切割成chunks,chunks可以理解为由一个个buffer组成
	  // buffer:原始定义 二进制数据在文件系统里的传输,实质上,buffer是一个个物理地址,数据暂存。
      if (config.responseType === 'stream') {
        response.data = stream;
        // 处理返回值,如果在是有效的,resolve,否则reject错误
        settle(resolve, reject, response);
      } else {
        var responseBuffer = [];
        stream.on('data', function handleStreamData(chunk) {
          responseBuffer.push(chunk);

          // maxContentLength存在,并且当前传送的buffer大小大于maxContentLength,不再传输,并抛出reject
          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);
          // 如果传入的responseType不是arraybuffer,说明可能是'json', 'text'等,配合传入的responseEncoding进行转码
          if (config.responseType !== 'arraybuffer') {
            responseData = responseData.toString(config.responseEncoding);
            // 如果没设置responseEncoding,或者responseEncoding为utf8时(utf8为默认值),去除bom标识(utf8格式的文件,开头会引入标识说明编码方式,不是真实的数据)
            if (!config.responseEncoding || config.responseEncoding === 'utf8') {
              responseData = utils.stripBOM(responseData);
            }
          }

          response.data = responseData;
          settle(resolve, reject, response);
        });
      }
    });
  1. 处理error和timeout,设置超时时间,当网络不好时,长时间未响应会阻塞后续请求的处理,因此在超时后,取消当前请求,挂起进程
    // Handle errors
    req.on('error', function handleRequestError(err) {
      if (req.aborted && err.code !== 'ERR_FR_TOO_MANY_REDIRECTS') 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));
      });
    }
  1. 处理cancel
    if (config.cancelToken) {
      // config.cancelToken为cancelToken的实例,处理完用户定的cancel方法后,取消请求
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (req.aborted) return;

        req.abort();
        reject(cancel);
      });
    }
  1. 结束请求,这个data是用户传入的config,如果是stream,通过pipe执行后续的操作,否则结束请求
	if (utils.isStream(data)) {
      data.on('error', function handleStreamError(err) {
        reject(enhanceError(err, config, null, req));
      }).pipe(req);
    } else {
      req.end(data);
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值