axios源码学习(四):axios整个处理流程

2,784 阅读11分钟

前面用了三篇文章对axios的部分源码做了分析,现在我们在从处理流程上梳理一遍。期间会有一些跟之前的内容重复的,我可能会简单带过,具体可以去看前面的内容。

这里,我简单的准备了一段代码:

import axios from 'axios'

axios.default.baseUrl = 'www.xxx.com'
axios.default.timeout = 60000

axios.interceptors.request.use(config => {
  if (config.token) {
    config.header['token'] = config.token
    delete config.token
  }
}, err => {
  console.log(err)
  return Promise.reject(error)
})

axios.interceptors.response.use(res => {
  console.log('res: ', res)
  if (res.status === 204) return null
  return res.data
}, err => {
  console.log('err: ', err)
  switch (err.response.status) {
    // do something
  }
  return Promise.reject(error)
})

axios.get('/user', {
  params: {
    ID: 12345
  }
}).then(function (response) {
  console.log(response);
}).catch(function (error) {
  console.log(error);
})

这段代码就是平时我们最简单的使用axios发送请求会做的一些处理。先引入axios,然后设置请求前缀跟超时时间,然后设置拦截器,最后发请求会处理请求响应。

axios.default

在发请求之前,我们可以为所有的请求的公共配置做统一处理。如果直接使用导出的axios()的实例,那么我们可以通过axios.default来设置(源码把axios()的实例的default挂到axios()上);如果通过axios.create()来创建axios实例,源码中会先把传入的配置跟default的配置合并,然后我们可以通过实例本身的default属性来设置公共配置。

这里提一个问题,就是直接通过axios.Axios来创建一个实例能不能成功发送请求?其实是比较难实现的操作,因为涉及到请求配置问题,而这个问题我们可以通过分析源码中公共配置defaults来理解。

源码中,有一个比较完整的公共配置保存在lib/defaults.js中。它可以分为适配器,请求转换器,响应转换器,超时配置,缓存配置,请求头配置及状态码校验器。

适配器

适配器是一个非常关键的配置,源码实现是通过一个函数来判断环境为Node环境还是浏览器环境,然后根据判断返回基于XHLHttpRequest实现的适配器或者基于Node的http实现的适配器。

// defaults源码
// 接入适配器
// 如果在浏览器环境,存在XMLHttpRequest类,则用XMLHttpRequest类来实现
// 如果在node环境,则使用http来实现
function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') { // 判断是否存在XMLHttpRequest类
    // 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(),

  // ...
}

适配器是在dispatchRequest()方法中调用:

// dispatchRequest源码
module.exports = function dispatchRequest(config) {
  // ...

  // 返回一个adapter then之后的Promise,其实就是请求结束之后的处理
  return adapter(config).then(/* ... */)

  // ...
}

请求转换器及响应转换器

这两个转换器分别使用在请求前跟响应后,都是对数据的格式做处理。两个转换器在源码内部都是一个数组,并存有默认的处理函数。这样外部就可以通过覆盖或者添加多个处理函数。

请求转换器中默认的函数主要是对请求数据的类型做处理,内部是对数据做类型判断,然后针对ArrayBufferViewURLSearchParamsObject的实例做处理。

// defaults源码
var defaults = {
  // ...

  // `transformRequest` 允许在向服务器发送前,修改请求数据
  // 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
  // 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
  // transformRequest transformResponse是在dispatchRequest函数中的transformData函数中处理的
  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Accept'); // 替换Accpet属性
    normalizeHeaderName(headers, 'Content-Type'); // 替换Content-Type属性
    /**
     * 下面是对data做处理
     */
    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }],

  // ...
}

而响应转换器的默认函数则比较简单,只是把String类型的数据转成JSON格式。

// defaults源码
var defaults = {
  // ...

  // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
  transformResponse: [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],

  // ...
}

既然是处理请求数据跟响应数据,那也肯定是在调起请求的前后调用的:

// dispatchRequest源码
module.exports = function dispatchRequest(config) {
  // ...

  // Transform request data
  // 对请求的数据(请求data及请求头)进行处理
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // ...

  // 返回一个adapter then之后的Promise,其实就是请求结束之后的处理
  return adapter(config).then(function onAdapterResolution(response) {
    // ...

    // Transform response data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    // ...
  })

  // ...
}

缓存配置

源码中缓存的公共配置只有两个,分别是配置cookie名称及请求头属性名xsrfCookieNamexsrfHeaderName。其中xsrfCookieName表示存储token的cookie名称,xsrfHeaderName表示请求headers中token对应的header名称。

这两个配置用在请求上。当准备请求前,axios会到cookie中获取以xsrfCookieName的值为key的缓存值,然后在请求header上设置以xsrfHeaderName的值为key的token值。

如果xsrfCookieName没有设置,则不会在请求头上设置这个属性。

// xhr.js源码
if (utils.isStandardBrowserEnv()) {
  var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
    cookies.read(config.xsrfCookieName) :
    undefined;

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

请求头配置

// default.js源码
defaults.headers = { // 设置默认头部
  common: {
    'Accept': 'application/json, text/plain, */*'
  }
};

utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  defaults.headers[method] = {};
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});

状态码校验器

状态码校验器的作用是用来判断响应是成功还是失败。默认是以2xx段为成功,但是如果项目需要可以直接自定义校验器函数。

// default.js源码
validateStatus: function validateStatus(status) { // 校验状态码成功或失败
  return status >= 200 && status < 300;
}

axios.default虽然没有导出外部,但是作者提供了通过axios.create()另外创建Axios实例。但是直接使用axios.Axios就需要手动引入axios.default,比较麻烦,所以不太推荐这么使用。

发请求

回到我准备的那段代码上。那段代码设置了请求的baseUrltimeout,分别是请求地址的前缀跟请求超时时间,然后又设置了请求拦截器跟响应拦截器。最后使用axios.get发了一个请求。

axios.get()内部其实是调用了Axios.prototype.request,把参数跟指定为getmethod传给request

// Axios.js源码
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});

Axios.prototype.request中主要分两步处理。第一步是合并配置,第二步是准备一个数组,默认数组中放入了dispatchRequest函数及一个null(这两个元素为一对)。然后分别向数组的头部跟尾部插入请求拦截器函数及相应拦截器函数。然后新建一个Promise实例并直接resolve传入请求配置,从头遍历数组,把成对的函数作为Promise实例的then函数的两个函数。

这样请求配置就会依次经过请求拦截器,然后调用dispatchRequest函数并传入拦截器返回的请求配置,最后得到相应数据再依次经过相应拦截器。

dispatchRequest函数内部主要做了整理请求配置,然后调用适配器发请求,然后接收相应数据,整理后返回出来。

Axios.prototype.request在第一篇中有分析过源码,这里不再详细说明,dispatchRequest函数的源码逻辑也比较简单,这里也不展开说明。

适配器

适配器是axios中最核心的部分,因为它就是用来发请求的。axios中支持node环境跟浏览器环境。在前面有写到,其实是判断了环境之后,在引入对应的请求模块。node环境使用node的http模块,而浏览器环境使用XMLHttpRequest。两个请求模块的处理大同小异,这里只分析一下使用XMLHttpRequest发请求的部分源码。

使用XMLHttpRequest发请求的适配器函数为xhrAdapter,内部其实是返回了一个Promise实例。

// xhr.js源码
module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // ...
  }
}

dispatchXhrRequest函数中,先整理好请求数据,对一些特殊请求做处理,并拼接完整的请求url。

// xhr.js dispatchXhrRequest函数promise 内部源码
var requestData = config.data;
var requestHeaders = config.headers;

// 判断data是否为FormData,是则删掉Content-Type
if (utils.isFormData(requestData)) {
  delete requestHeaders['Content-Type']; // Let the browser set it
  // 删除Content-Type ,浏览器会根据请求数据是FormData默认设置Content-Type为multipart/form-data
}

// ...

// HTTP basic authentication
// 如果需要用户认证就设置认证信息
// btoa编码用户认证信息
if (config.auth) {
  var username = config.auth.username || '';
  var password = config.auth.password || '';
  requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}

// 拼接请求url
var fullPath = buildFullPath(config.baseURL, config.url);

// ...

// Add xsrf header
// 判断是否是标准的浏览器环境
if (utils.isStandardBrowserEnv()) {
  // 判断withCredentials为true则支持携带cookie,或者判断请求地址跟页面地址是否同域,且设置了xsrfCookieName键名
  // 是则读取cookie并在请求头上带上
  var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
    cookies.read(config.xsrfCookieName) :
    undefined;

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

// Add headers to the request
// 如果存在设置请求头的方法setRequestHeader,则把config中header的部分设置进去
// 当key为content-type且requestData为空时,不设置content-type
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
// 如果config.withCredentials有值,则设置请求实例上的withCredentials
if (!utils.isUndefined(config.withCredentials)) {
  request.withCredentials = !!config.withCredentials;
}

// Add responseType to request if needed
// 如果config.responseType有值,则尝试设置,不支持则抛出错误
// 参考阮一峰的http://www.ruanyifeng.com/blog/2012/09/xmlhttprequest_level_2.html
// 这里做try catch是因为老版本的XMLHttpRequest只能返回文本类型,不能设置返回类型。
// 但是如果请求返回类型为json,也可以继续执行
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;
    }
  }
}

// ...

// 如果requestData没有,则设置为null
if (!requestData) {
  requestData = null;
}

如果有设置cancelToken,还要给cancelToken.promise设置then函数的执行。

// xhr.js dispatchXhrRequest函数promise 内部源码
// 如果有设置cancelToken,就设置then中的处理
if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    // 如果请求已经清空,就不再处理
    if (!request) {
      return;
    }

    // 取消请求,然后reject调Promise,再清空request
    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}

然后就是创建XMLHttpRequest实例,发请求,监听请求的状态,设置请求的监听函数。关于请求的写法,这里不做太多介绍,直接看源码:

// xhr.js dispatchXhrRequest函数promise 内部源码
// ...

// 建一个http请求
var request = new XMLHttpRequest();

// ...

// 初始化请求
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

// Set the request timeout in MS
// 设置最大请求时间
request.timeout = config.timeout;

request.onreadystatechange = function handleLoad() {
  // 如果request没有了或者readyState不等于4,就不处理
  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
  // request.responseURL - 返回响应的序列化(serialized)URL,如果该 URL 为空,则返回空字符串。
  if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
    return;
  }

  // Prepare the response
  // request.getAllResponseHeaders - 以字符串的形式返回所有用 CRLF 分隔的响应头,如果没有收到响应,则返回 null。
  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)
// 监听请求取消,然后reject掉Promise,传入取消请求的Error实例
request.onabort = function handleAbort() {
  if (!request) {
    return;
  }

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

  // Clean up request
  request = null;
};

// Handle low level network errors
// 监听请求失败,然后reject掉Promise,传入响应失败的Error实例
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;
};

// ...

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

// ...

// Send the request
// 设置完请求属性参数,就发送请求
request.send(requestData);

上面可以看到,在request.onreadystatechange中,当拿到响应是,会调settle方法,把响应内容跟Promise实例的resolvereject传入。内部其实是在处理响应的状态,判断响应属于成功还是失败。

// settle.js
module.exports = function settle(resolve, reject, response) {
  var validateStatus = response.config.validateStatus;
  // 没有状态码,或者没有校验函数,或者有校验函数且校验为true时,走resolve
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    // 否则走reject,创建一个请求Error实例
    reject(createError(
      'Request failed with status code ' + response.status,
      response.config,
      null,
      response.request,
      response
    ));
  }
};

这里就能确定请求的响应应该到响应拦截器的成功拦截函数还是失败拦截函数中。然后就走完整个axios请求。

结语

axios源码不难读,而且有很多值得学习积累的内容。有机会我还会继续读其他源码,尝试继续整理一些源码分析。希望能留下一些有用的积累。