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
相关推荐
2501_915373884 小时前
Vue 3零基础入门:从环境搭建到第一个组件
前端·javascript·vue.js
沙振宇7 小时前
【Web】使用Vue3开发鸿蒙的HelloWorld!
前端·华为·harmonyos
运维@小兵7 小时前
vue开发用户注册功能
前端·javascript·vue.js
蓝婷儿8 小时前
前端面试每日三题 - Day 30
前端·面试·职场和发展
oMMh8 小时前
使用C# ASP.NET创建一个可以由服务端推送信息至客户端的WEB应用(2)
前端·c#·asp.net
一口一个橘子8 小时前
[ctfshow web入门] web69
前端·web安全·网络安全
读心悦9 小时前
CSS:盒子阴影与渐变完全解析:从基础语法到创意应用
前端·css
湛海不过深蓝10 小时前
【ts】defineProps数组的类型声明
前端·javascript·vue.js
layman052810 小时前
vue 中的数据代理
前端·javascript·vue.js
柒七爱吃麻辣烫10 小时前
前端项目打包部署流程j
前端