Axios源码分析

Axios

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

Axios

一般在前端项目中,会通过 axios create来获取一个axios的单例,并且配置请求拦截器,以及响应拦截器

需要注意的是,在项目,一般通过env环境变量的方式来获取到后端api的请求地址,在开发环境下,如果baseUrl不是'/'或'',会导致前端请求代理失效。

php 复制代码
const instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

拦截器 interceptors

挂载token

arduino 复制代码
// 设置token
async function setToken(config) {
       config.headers.Authorization = getToken()
}

instance.interceptors.request.use(async (config)=>{
    config = setToken(config)
    return config
})

自动刷新token

当token过期后,自动将请求push到一个数组中,再获取到新token时,重新发起请求

先在请求拦截其中保存每次请求的配置

难点是如何保存失败请求的pending状态。并且在刷新token并请求成功后,这些状态置为fulfilled

javascript 复制代码
let lastConfig = null
// 当前是否正在刷新token中
let isRefreshing = false
let requests = []
const instance = axios.create()
instance.interceptors.request.use(async (config)=>{
    config = setToken(config)
    lastConfig = decoup(config)
    return config
})
// 需要后端提供两个token,一个用于请求接口,一个用于刷新token
// 这里获取的就是用于请求接口的token
async function refreshToken() {
}
instance.interceptors.response.use(async (response)=> {
    // 这里code的值需要与后端协商
     if(response.data.code === ruleStatus.expired) {
         // 如果已经进入了刷新token的流程,则会将此阶段的请求,暂存到requests中
         if(!isRefreshing) {
            // 第一个token失效的请求配置
            const config = response.config
            isRefreshing = true
            // 如果发现token过期了
            const newToken = await refreshToken().catch(()=>{
            // 如果token刷新失败,则直接跳转回登陆页面
                toLogin()
            })
            if(newToken) {
                // 将刷新token期间的请求resolve
                requests.forEach(cb=> cb(newToken))
                // 晴空数组
                requests.length = 0
                // 更换新token
                config.headers.Authorization = token
                // 继续请求第一个失效的请求
                return instance(config)
            }
         } else {
             // 这里返回一个pending 状态promise,并且将resolve方法放到requests中
             // 只要requests中的方法不执行,这个promsie的状态就一直为pending
             return new Promise(resolve=> {
                 requests.push((token)=>{
                     let temp = null;
                     temp = lastConfig
                     temp.headers.Authorization = token
                     resolve(instance(temp))
                 })
             })
         }
         
         
     }
})

登陆过期

lua 复制代码
instance.interceptors.response.use(()=>{}, (error)=>{
    if(error.response && error.response.status === 401) {
        ElMessageBox.confirm(
        '抱歉,您当前登录状态已失效,点击【重新登录】跳转至登录页,【取消】则继续停留在当前页面',
        '上传提示',
        {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }
      ).then(() => {
        userStore.login()
      })
    }
})

返回类型处理

针对二进制流部分,可以根据与后端约定的格式,通过响应拦截器,直接获取到二进制流,

typescript 复制代码
instance.interceptors.response.use(async (response)=>{
    const {config} = response
    // 接口请求时传入响应预处理方法
    const resIteratee = Array.isArray(config.resIteratee) ? config.resIteratee : [config.resIteratee]
    let result = response
    while(config.resIteratee.length) {
       const fn = config.resIteratee.shift()
       const isAsync = fn instanceof Promise
       try {
              result = isAsync ? await fn(response) : fn(response)
              if(result === false) {
                   return response
              }
            } catch (err) {
               return Promise.reject(err)
               console.log(e)
            }
    }
    if (
      response.request.responseType === 'blob' ||
      response.request.responseType === 'arraybuffer' ||
      responseIncrWhiteList.includes(response.config.url as string)
    ) {
      return response.data.data
    }
    return response.data
})

Retry

利用拦截器自动重发失败请求,这里的失败指的是 500类型的错误,如果是业务层面的请求失败,可以默认是服务端逻辑,不需要重新请求。其实与自动刷新token的逻辑有一些类似 实现参考了 axios-retry的源码

lua 复制代码
// 判断错误是否需要重试
function isRetryableError(error) {
  return (
    error.code !== 'ECONNABORTED' &&
    (!error.response || (error.response.status >= 500 && error.response.status <= 599))
  );
}
function getCurrentState(config) {
  const currentState = config['_retry_'] || {};
  currentState.retryCount = currentState.retryCount || 0;
  config['_retry_'] = currentState;
  return currentState;
}
// 对于下一次的请求,config上的这些字段并不能拷贝到请求配置上,需要手动设置
// 这是一个axios的bug
function fixConfig(axios, config) {
  if (axios.defaults.agent === config.agent) {
    delete config.agent;
  }
  if (axios.defaults.httpAgent === config.httpAgent) {
    delete config.httpAgent;
  }
  if (axios.defaults.httpsAgent === config.httpsAgent) {
    delete config.httpsAgent;
  }
}
lua 复制代码
function retry(axios, count) {
    axios.interceptors.request((config)=>{
        const currentState = getCurrentState(config)
        return config
    })
    
    axios.interceptors.response(null, (error)=>{
        const { config } = error
        const currentState = getCurrentState(config)
        if(config.retryCount > count) {
            return Promsie.reject(error)
        }
        config.transformRequest = [(data) => data];
        fixConfig(axios, config)
        if(isRetryableError(error) && error.config.retryCount <= count) {
            return axios(config)
        } else {
            return Promsie.reject(error)
        }
    })
    return axios
}

const instance = retry(Axios.create(), 3)

请求防抖

同类型的请求,请求的config一致,大致思路为,新建一个正在请求的数组,存在时直接return Promise.reject()结束请求,请求完成后,则将此类型的请求从数组中删除

javascript 复制代码
import { eq } from 'lodash'
const currentPendingSet = []
const sameRequestPendingindex = function (config) {
    const index = currentPendingSet.findIndex((conf)=>{
          return conf.url === config.url && eq(conf.data, config.data)
    })
    return index
}
const hasSameRequestPending = function (config) {
    return sameRequestPendingindex(config) > -1
}

axios.interceptors.request((config)=>{
    if(hasSameRequestPending(config)) {
        return Promise.reject(config)
    }
    currentPendingSet.push(config)
    return config
})


axios.interceptors.response((response)=>{
    const { config } = response
    const index = sameRequestPendingindex(config)
    if(index > -1) {
        currentPendingSet.splice(index, 1)
    }
    return response.data
})

Request方法

request方法是axios的基础方法,其他例如,get\post\put\patch等,都是基于reuqest扩展实现的、他接受一个请求地址、以及config对象

他最终会调用 dispatchRequest 来发起请求,在此之前,他会执行以下逻辑

合并配置

合并axios内置配置、axios.create时传入的配置,以及用户发起请求时传入配置、用户配置的优先级最高,create配置其次、axios内置默认配置最低、优先级高的可以在合并中覆盖优先级低的

ini 复制代码
if (typeof configOrUrl === 'string') {
    config = config || {};
    config.url = configOrUrl;
  } else {
    config = configOrUrl || {};
  }
  config = mergeConfig(this.defaults, config);

mergeConfig 内部使用策略模式、获取对应属性的合并方法

scss 复制代码
  // 每种属性对应的默认值
  var mergeMap = {
    'url': valueFromConfig2,
    'method': valueFromConfig2,
    'data': valueFromConfig2,
    'baseURL': defaultToConfig2,
    'transformRequest': defaultToConfig2,
    'transformResponse': defaultToConfig2,
    'paramsSerializer': defaultToConfig2,
    'timeout': defaultToConfig2,
    'timeoutMessage': defaultToConfig2,
    'withCredentials': defaultToConfig2,
    'adapter': defaultToConfig2,
    'responseType': defaultToConfig2,
    'xsrfCookieName': defaultToConfig2,
    'xsrfHeaderName': defaultToConfig2,
    'onUploadProgress': defaultToConfig2,
    'onDownloadProgress': defaultToConfig2,
    'decompress': defaultToConfig2,
    'maxContentLength': defaultToConfig2,
    'maxBodyLength': defaultToConfig2,
    'beforeRedirect': defaultToConfig2,
    'transport': defaultToConfig2,
    'httpAgent': defaultToConfig2,
    'httpsAgent': defaultToConfig2,
    'cancelToken': defaultToConfig2,
    'socketPath': defaultToConfig2,
    'responseEncoding': defaultToConfig2,
    'validateStatus': mergeDirectKeys
  };
  // 
  utils.forEach(Object.keys(config1).concat(Object.keys(config2)), function computeConfigValue(prop) {
    var merge = mergeMap[prop] || mergeDeepProperties;
    var configValue = merge(prop);
    (utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue);
  });

axios默认配置

axios内置的配置

kotlin 复制代码
function getDefaultAdapter() {
  var adapter;
  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 = {
// transitionalDefaults
// export default {
// silentJSONParsing: true,
// forcedJSONParsing: true,
// clarifyTimeoutError: false
// };
  transitional: transitionalDefaults,
  // 自定义请求,xhr or http //用于区分平台,
  // nodejs平台使用request,borwser使用XMLHttpRequest
  adapter: getDefaultAdapter(),
  // 默认的请求拦截器
  transformRequest: [function transformRequest(data, headers) {
    // 格式化属性名
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');
    
    // 获取当前的
    var contentType = headers && headers['Content-Type'] || '';
    var hasJSONContentType = contentType.indexOf('application/json') > -1;
    var isObjectPayload = utils.isObject(data);

    if (isObjectPayload && utils.isHTMLForm(data)) {
      data = new FormData(data);
    }

    var isFormData = utils.isFormData(data);

    if (isFormData) {
      if (!hasJSONContentType) {
        return data;
      }
      return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;
    }
    // 处理各种数据类型
    if (utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
    // 返回
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    // 如果使用了params参数
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    // 是否为多个文件
    var isFileList;

    if (isObjectPayload) {
      if (contentType.indexOf('application/x-www-form-urlencoded') !== -1) {
        return toURLEncodedForm(data, this.formSerializer).toString();
      }

      if ((isFileList = utils.isFileList(data)) || contentType.indexOf('multipart/form-data') > -1) {
        var _FormData = this.env && this.env.FormData;
        // {'files[]': data}是一个固定格式表示data为一个数组,方便转换为x-www-form-url-encoded的格式 
        return toFormData(
          isFileList ? {'files[]': data} : data,
          _FormData && new _FormData(),
          this.formSerializer
        );
      }
    }
    // json数据处理
    if (isObjectPayload || hasJSONContentType ) {
      setContentTypeIfUnset(headers, 'application/json');
      // 将js对象转换为json字符串
      return stringifySafely(data);
    }

    return data;
  }],
   // 默认响应配置
  transformResponse: [function transformResponse(data) {
    // 
    var transitional = this.transitional || defaults.transitional;
    var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
    var JSONRequested = this.responseType === 'json';
    // 这里
    if (data && utils.isString(data) && ((forcedJSONParsing && !this.responseType) || JSONRequested)) {
      var silentJSONParsing = transitional && transitional.silentJSONParsing;
      var strictJSONParsing = !silentJSONParsing && JSONRequested;
        // 将后端返回的json字符串格式化成js对象
      try {
        return JSON.parse(data);
      } catch (e) {
      // 当发生错误后,使用Axios内置错误类型将错误抛出
        if (strictJSONParsing) {
          if (e.name === 'SyntaxError') {
            throw AxiosError.from(e, AxiosError.ERR_BAD_RESPONSE, this, null, this.response);
          }
          throw e;
        }
      }
    }
    return data;
  }],

  /**
   * A timeout in milliseconds to abort a request. If set to 0 (default) a
   * timeout is not created.
   */
  timeout: 0,

  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',

  maxContentLength: -1,
  maxBodyLength: -1,

  env: {
    FormData: platform.classes.FormData,
    Blob: platform.classes.Blob
  },

  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  },

  headers: {
    common: {
      'Accept': 'application/json, text/plain, */*'
    }
  }
};

浏览器平台请求逻辑

axios浏览器平台的核心请求逻辑

ini 复制代码
'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 transitionalDefaults = require('../defaults/transitional');
var AxiosError = require('../core/AxiosError');
var CanceledError = require('../cancel/CanceledError');
var parseProtocol = require('../helpers/parseProtocol');
var platform = require('../platform');

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
  // 请求数据
    var requestData = config.data;
    // 请求头
    var requestHeaders = config.headers;
    // 服务器返回类型
    var responseType = config.responseType;
    // 
    var onCanceled;
    // 请求完成后执行此方法
    function done() {
      if (config.cancelToken) {
        config.cancelToken.unsubscribe(onCanceled);
      }

      if (config.signal) {
        config.signal.removeEventListener('abort', onCanceled);
      }
    }

    if (utils.isFormData(requestData) && utils.isStandardBrowserEnv()) {
      delete requestHeaders['Content-Type']; // Let the browser set it
    }
    // 实例化一个xhr对象
    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)) : '';
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }

    var fullPath = buildFullPath(config.baseURL, config.url);
    // 方法初始化一个新创建的请求,或重新初始化一个请求。 如果再次调用,则相当于调用了abort()
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    
    // Set the request timeout in MS
    request.timeout = config.timeout;
    
    function onloadend() {
      if (!request) {
        return;
      }
      // Prepare the response
      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
      };

      settle(function _resolve(value) {
        resolve(value);
        done();
      }, function _reject(err) {
        reject(err);
        done();
      }, response);

      // Clean up request
      request = null;
    }
    // 一些低版本的浏览器并不支持 onloadend
    if ('onloadend' in request) {
      // Use onloadend if available
      request.onloadend = onloadend;
    } else {
      // Listen for ready state to emulate onloadend
      request.onreadystatechange = function handleLoad() {
         // readyState 4 : 数据传输已经完成,,或者传输过程中出现问题
         // 3 正在接收后端响应的数据
         // 2 收到响应头,准备开始接收数据
         // 1 已经调用过open()方法
         // 0 request 已经完成实例化
        if (!request || request.readyState !== 4) {
          return;
        }

        // 当status等于0时
        // 1、request已经完成实例化
        // 2、request被abort中断
        if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
          return;
        }
        // readystate handler is calling before onerror or ontimeout handlers,
        // so we should call onloadend on the next 'tick'
        setTimeout(onloadend);
      };
    }

    // Handle browser request cancellation (as opposed to a manual cancellation)
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }

      reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, 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(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));

      // Clean up request
      request = null;
    };

    // Handle timeout
    request.ontimeout = function handleTimeout() {
      var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
      var transitional = config.transitional || transitionalDefaults;
      if (config.timeoutErrorMessage) {
        timeoutErrorMessage = config.timeoutErrorMessage;
      }
      reject(new AxiosError(
        timeoutErrorMessage,
        transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
        config,
        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()) {
      // 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 (!utils.isUndefined(config.withCredentials)) {
      request.withCredentials = !!config.withCredentials;
    }

    // Add responseType to request if needed
    if (responseType && responseType !== 'json') {
      request.responseType = config.responseType;
    }

    // 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 || config.signal) {
      // Handle cancellation
      // eslint-disable-next-line func-names
      onCanceled = function(cancel) {
        if (!request) {
          return;
        }
        reject(!cancel || cancel.type ? new CanceledError(null, config, req) : cancel);
        request.abort();
        request = null;
      };

      config.cancelToken && config.cancelToken.subscribe(onCanceled);
      if (config.signal) {
        config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
      }
    }

    if (!requestData) {
      requestData = null;
    }
    // 获取请求协议 http https file
    var protocol = parseProtocol(fullPath);

    if (protocol && platform.protocols.indexOf(protocol) === -1) {
      reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config));
      return;
    }

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

中断请求能力

示例代码 -1

javascript 复制代码
import axios from 'axios'
let cancelFunc;
let cancelToken = axios.CancelToken(function(cancelFn) {
       cancelFunc = cancelFn
})
axios({
    method: 'POST',
    url:'xxx',
    isCancelToken: cancelToken
})

cancelToken.promise.then(()=>{
    // do something ...
})
// 调用cancelFunc取消请求
cancelFunc()

示例代码 - 2

php 复制代码
import axios from 'axios'
const cancelObj = axios.CancelToken.source()
axios({
    method: 'POST',
    url:'xxx',
    isCancelToken: cancelObj.token
})

cancelObj.cancel()

axios是如何实现请求中断的

发起

在XHR.js中有以下逻辑,用于处理传入CancelToken的情况

scss 复制代码
// 当用户传入cancelToken时,会执行此逻辑 
if (config.cancelToken || config.signal) {
      // Handle cancellation
      // eslint-disable-next-line func-names
      onCanceled = function(cancel) {
        if (!request) {
          return;
        }
        reject(!cancel || cancel.type ? new CanceledError(null, config, req) : cancel);
        request.abort();
        request = null;
      };
      // 如果有cancel方法则会在cancelToken中订阅这个状态
      config.cancelToken && config.cancelToken.subscribe(onCanceled);
      // 浏览器默认的中断逻辑,比如用户直接在request上执行了abort方法
      if (config.signal) {
        config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
      }
    }

在CancelToken.js文件,是一个简单的状态机+订阅模式

有几个关键方法,和有意思的地方

  • resolvePromise 利用外部作用于,将promise状态的控制权传递给外界
  • this.promise.then = function () {} 在方法中,又新建一个promise
javascript 复制代码
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }
  // 关键
   // 这个变量很关键,他获取到了 CancelToken 内置promise的resolve方法,将
  // Promise的状态控制权交给了外部
  var resolvePromise;

  this.promise = new Promise(function promiseExecutor(resolve) {
  // 将this。promise的状态控制权交给外界
    resolvePromise = resolve;
  });
  // 获取当前调用实例
  var token = this;
  // 执行所有订阅此状态的回调函数
  // eslint-disable-next-line func-names
  this.promise.then(function(cancel) {
    if (!token._listeners) return;

    var i = token._listeners.length;

    while (i-- > 0) {
      token._listeners[i](cancel);
    }
    token._listeners = null;
  });
  // 这是第二个比较有意思的点
  // 首先:他处理了实例调用 promise.then的逻辑,使得我们可以在请求被取消时做点什么
  // 原理是利用新建一个Promise, 利用promie.then微任务总是比同步任务后执行,来保证
  // 用户传入的回调永远在最后执行
  // eslint-disable-next-line func-names
  this.promise.then = function(onfulfilled) {
    var _resolve;
    // eslint-disable-next-line func-names
    var promise = new Promise(function(resolve) {
      token.subscribe(resolve);
      _resolve = resolve;
    }).then(onfulfilled);

    promise.cancel = function reject() {
      token.unsubscribe(_resolve);
    };

    return promise;
  };
  // executor 内传入的方法,就是在new CancelToken时从回调函数中获取的 cancel 变量,
  // 他是一个方法,利用前面保存的 resolvePromise 让用户能够修改此时内置promise的状态
  // 从pending -> fulfilled
  executor(function cancel(message, config, request) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new CanceledError(message, config, request);
    resolvePromise(token.reason);
  });
}

/**
 * Throws a `CanceledError` if cancellation has been requested.
 */
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

/**
 * Subscribe to the cancel signal
 */

CancelToken.prototype.subscribe = function subscribe(listener) {
  if (this.reason) {
    listener(this.reason);
    return;
  }

  if (this._listeners) {
    this._listeners.push(listener);
  } else {
    this._listeners = [listener];
  }
};

/**
 * Unsubscribe from the cancel signal
 */

CancelToken.prototype.unsubscribe = function unsubscribe(listener) {
  if (!this._listeners) {
    return;
  }
  var index = this._listeners.indexOf(listener);
  if (index !== -1) {
    this._listeners.splice(index, 1);
  }
};

/**
 * Returns an object that contains a new `CancelToken` and a function that, when called,
 * cancels the `CancelToken`.
 */
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

module.exports = CancelToken;

参数处理

在合并完配置后,将会进入参数处理部分

ini 复制代码
Axios.prototype.request = function request(configOrUrl, config) {
  /*eslint no-param-reassign:0*/
  // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof configOrUrl === 'string') {
    config = config || {};
    config.url = configOrUrl;
  } else {
    config = configOrUrl || {};
  }
  // 将配置请求与用户传入
  config = mergeConfig(this.defaults, config);

  // Set config.method
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }

  var transitional = config.transitional;
   // 这里是对传入参数类型进行校验,validators是一个对象,里面存放着number、string、boolean等类型
   // 这里使用的是boolean校验器
  if (transitional !== undefined) {
    validator.assertOptions(transitional, {
       // 忽略JSON.parse(response.body)的错误
      silentJSONParsing: validators.transitional(validators.boolean),
      // 当responseType!== json时将response转化为json
      forcedJSONParsing: validators.transitional(validators.boolean),
      // 当请求超时返回ETIMEDOUT而不是ECONNABORTED
      clarifyTimeoutError: validators.transitional(validators.boolean)
    }, false);
  }

  // filter out skipped interceptors
  var requestInterceptorChain = [];
  var synchronousRequestInterceptors = true;
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
      return;
    }

    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;

    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  var promise;

  if (!synchronousRequestInterceptors) {
    var chain = [dispatchRequest, undefined];

    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    chain = chain.concat(responseInterceptorChain);

    promise = Promise.resolve(config);
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift());
    }

    return promise;
  }


  var newConfig = config;
  while (requestInterceptorChain.length) {
    var onFulfilled = requestInterceptorChain.shift();
    var onRejected = requestInterceptorChain.shift();
    try {
      newConfig = onFulfilled(newConfig);
    } catch (error) {
      onRejected(error);
      break;
    }
  }

  try {
    promise = dispatchRequest(newConfig);
  } catch (error) {
    return Promise.reject(error);
  }

  while (responseInterceptorChain.length) {
    promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
  }

  return promise;
};

axios在项目中使用的新玩法

在日常开发中,难免会有部分接口的返回数据格式,或者层级与其他接口不一致,如果在返回拦截器中对值做了处理,则可能为此接口单独添加额外的配置,用来在拦截器中做判断。

而在一些开源的项目中,我发现他们项目的axios拦截器非常简单,仅仅只是挂载了一个token,而文件处理,以及请求前的一些操作,则是利用一个包装器的概念实现的,利用这种方式,上面的问题就迎刃而解了,代码如下:

对于axios自带的拦截器,只进行挂载token的操作。

javascript 复制代码
export const BASIC_CONFIG = {
  baseURL: import.meta.env.$BASIC_BASE_URL,
  headers: {
    "Content-Type": "application/json"
  },
  timeout: 10000
}

export const request = axios.create(BASIC_CONFIG)

// 只使用了一个请求拦截器,挂载token
request.interceptors.request.use(
  config => {
    if (localStorage.getItem("token")) {
      config.headers.Authorization = localStorage.getItem("token")
    }
    return config
  },
  err => Promise.reject(err)
)

普通请求包装器

javascript 复制代码
/* 
  普通请求包装器,用于包装普通请求,做一些所有请求的统一的处理
*/
export function basicRequestWrapper(options, { loading = true }) {
  const _loading = loading ? Loading.service({ lock: true }) : { close: noop }
  return request(options)
    .then(res => {
      const { code, msg, data } = res.data
      if (code === "200") {
        return data
      }
      if (["50001", "50002", "50003"].includes(code)) {
        if (hasToLoginBox) {
          return
        }
        Loading.closeAll()

        hasToLoginBox = true
        return ElMessageBox.confirm(
          "登录状态已过期,点击确定按钮去重新登录。",
          "系统提示",
          {
            type: "error",
            confirmButtonText: "确认",
            showCancelButton: false
          }
        ).then(() => {
          localStorage.removeItem("token")
          hasToLoginBox = false
          window.location.href = "/login"
        })
      }
      if (msg) {
        ElMessage({ type: "error", message: msg })
      }
      throw res.data
    })
    .finally(() => _loading.close())
}

对于二进制流的包装器

javascript 复制代码
/* 
  二进制流包装器,适用于文件下载,图片等
*/
/**
 * @param {import("axios").AxiosRequestConfig} options
 * @param {{ loading: boolean }}
 */
export function streamRequestWrapper(options, { loading, fileType }) {
  const _loading = loading ? Loading.service({ lock: true }) : { close: noop }
  return request({ ...options, responseType: "arraybuffer" })
    .then(res => {
      const { data, headers } = res
      if (
        typeof data === "object" &&
        data.code &&
        ["50001", "50002", "50003"].includes(data.code)
      ) {
        if (hasToLoginBox) {
          return
        }
        Loading.closeAll()

        hasToLoginBox = true
        return ElMessageBox.confirm(
          "登录状态已过期,点击确定按钮去重新登录。",
          "系统提示",
          {
            type: "error",
            confirmButtonText: "确认",
            showCancelButton: false
          }
        ).then(() => {
          localStorage.removeItem("token")
          hasToLoginBox = false
          window.location.href = "/login"
        })
      }

      return { data, headers }
    })
    .finally(() => _loading.close())
}

针对常用请求防止再做一层封装

typescript 复制代码
import { basicRequestWrapper, streamRequestWrapper } from "./request"

export const postRequest = ({ url, data = {}, loading }) => {
  return basicRequestWrapper(
    { url, data: JSON.stringify(data), method: "POST" },
    { loading }
  )
}
// formData的请求
export const formRequest = ({ url, data = {}, loading }) => {
  return basicRequestWrapper(
    {
      url,
      data: Object.entries(data).reduce((data, [key, value]) => {
        Array.isArray(value)
          ? value.forEach(file => data.append(key, file))
          : data.append(key, value)
        return data
      }, new FormData()),
      method: "POST",
      headers: { "Content-Type": null }
    },
    { loading }
  )
}

// 请求文件
export const fileRequest = ({ url, data = {}, loading }) => {
  return streamRequestWrapper(
    { url: url + resolveURLQuery(data) },
    { loading }
  ).then(({ data, headers }) => {
    return new Promise(resolve => {
      const type = headers["content-type"].split(";")[0].trim()
      const blob = new Blob([data], { type })
      let reader = new FileReader()
      reader.onload = function (e) {
        resolve(e.target.result)
      }
      reader.readAsDataURL(blob)
    })
  })
}

Fetch

Fetch是一个现代的概念,等同于XML Request, 他提供了许多与XMLHttpRequest 相同的功能,但被设计成更具可扩展性和高效。

  • fetch()的功能与 XMLHttpRequest 基本相同,但有三个主要的差异。

  • fetch()使用 Promise,不使用回调函数,因此大大简化了写法,写起来更简洁。

  • fetch()采用模块化设计,API 分散在多个对象上(Response 对象、Request 对象、Headers 对象),更合理一些;相比之下,XMLHttpRequest 的 API 设计并不是很好,输入、输出、状态都在同一个接口管理,容易写出非常混乱的代码。

  • fetch()通过数据流(Stream 对象)处理数据,可以分块读取,有利于提高网站性能表现,减少内存占用,对于请求大文件或者网速慢的场景相当有用。XMLHTTPRequest 对象不支持数据流,所有的数据必须放在缓存里,不支持分块读取,必须等待全部拿到后,再一次性吐出来。

最简使用

fetch()接收一个url,默认发起get请求,返回一个Response对象

typescript 复制代码
 // fetch
 const data = await fetch(baseUrl + '/api/getRecommend').catch((err)=>{
     console.log('Failed', err)
 })
 console.log(data.json())

Response对象

响应头上的同步信息

fetch()请求成功后返回,对应服务器的 HTTP 回应。

属性 类型 意义
ok boolean 请求是否成功,true 对应HTTP状态码 200 - 299,false 对应其他的状态码,400 - 504, 300系列的状态码,如果请求成功,会被fetch自动转为200补充常见状态码
status number HTTP状态码- 1**: 服务器接收到请求,需要请求者继续操作
  • 2**:成功,操作被成功接收并处理
  • 3**:重定向,需要进一步操作完成请求
  • 4**:客户端错误
  • 5**:服务器错误 | | statusText | string | 响应状态信息,如OK, | | url | string | 请求url, 存在跳转时,返回最终url | | type | string | - basic: 普通请求
  • cors: 跨域请求
  • error: 网络错误
  • opaque: 如果fetch()请求的type属性设为no-cors,就会返回这个值。表示发出的是简单的跨域请求,类似<form>表单的那种跨域请求。
  • opaqueredirect: 如果fetch()请求的redirect属性设为manual,就会返回这个值 | | redirected | boolean | 是否发生过跳转 |

数据获取

与XMLHttpRequest可以直接获取到数据不同,fetch需要调用以下方法

response.

  • json() 获取json数据,如果返回的数据不是这个格式,将会报错
  • blob() 获取到Blob对象
  • formData() 获取表单对象
  • arrayBuffer() 获取二进制对象
  • text() 获取字符串数据
javascript 复制代码
 const imageType = await fetch('/image' + imageUrl)
    // console.log(await imageType.clone().json()) // 报错
    console.log(await imageType.clone().text(),'text')
    console.log(await imageType.clone().arrayBuffer(), 'Arraybuffer')
    console.log(await imageType.clone().blob(), 'blob')
    // console.log(await imageType.clone().formData(), 'formdata') // 报错

text()

其他类型都是可以正常获取到对应类型的,text()有一点特殊:

如果返回的对象转换为string类型无意义的话,比如这里返回的是一个图片,那么他将获取到当前页面的html

如果是JSON的话,则会返回一个json字符串

分段读取数据Response.body

response.body 是Response对象暴露出的底层接口,返回一个 ReadableStream 对象

javascript 复制代码
 // fetch获取图片
    const image = await fetch('/image' + imageUrl)
    const reader = image.body.getReader()
    async function readImage () {
      const {done, value} = await reader.read()
      console.log(value)
      if(done) {
        console.log('done')
        return
      }
      window.requestAnimationFrame(readImage)  
    }
    readImage()

fetch 配置

fetch(url, options) 接收两个参数,分别是请求地址、配置参数

options完整案例

javascript 复制代码
const response = fetch(url, {
  method: "GET", // 请求方式
  headers: {"Content-Type": "text/plain;charset=UTF-8"}, // 请求头
  body: undefined, // 请求体 可以接受Blob, FormData Text Json
  referrer: "about:client", // 请求来源,默认为about:client
  referrerPolicy: "no-referrer-when-downgrade", // 属性用于设定Referer标头的规则。
  // 
  mode: "cors",  // 属性指定请求的模式。 
  credentials: "same-origin", // 属性指定是否发送 Cookie。
  cache: "default", // 属性指定如何处理缓存。
  redirect: "follow", // 属性指定 HTTP 跳转的处理方法
  integrity: "", // 属性指定一个哈希值,用于检查 HTTP 回应传回的数据是否等于这个预先设定的哈希值
  keepalive: false, // 属性用于页面卸载时,告诉浏览器在后台保持连接,继续发送数据
  signal: undefined // 请求中断信号
});

取消fetch请求

scss 复制代码
const controller = new AbortController();
const signal = controller.signal
fetch(url, {
    signal: controller.signal
})

controller.abort(); // 取消
console.log(signal.aborted); // true
相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax