一起来看看Axios的源码

Axios 是前端最常用的异步请求模块,了解 Axios 源码有助于我们在开发中,更合理的去配置 Axios,也能更快的定位请求失败时的异常。

目录结构

├── /dist/                     # 项目输出目录
├── /lib/                      # 项目源码目录
│ ├── /cancel/                 # 定义取消功能
│ ├── /core/                   # 一些核心功能
│ │ ├── Axios.js               # axios的核心主类
│ │ ├── dispatchRequest.js     # 用来调用http请求适配器方法发送请求
│ │ ├── InterceptorManager.js  # 拦截器构造函数
│ │ └── settle.js              # 根据http响应状态,改变Promise的状态
│ ├── /helpers/                # 一些辅助方法
│ ├── /adapters/               # 定义请求的适配器 xhr、http
│ │ ├── http.js                # 实现http适配器
│ │ └── xhr.js                 # 实现xhr适配器
│ ├── axios.js                 # 对外暴露接口
│ ├── defaults.js              # 默认配置 
│ └── utils.js                 # 公用工具
├── package.json               # 项目信息
├── index.d.ts                 # 配置TypeScript的声明文件
└── index.js                   # 入口文件
复制代码

语法糖

Axios 的常用API

import axios from 'axios';

const apiLogin = '/login';
const apiUser = '/user';
const headers = {'x-token': 'asdfasdf'};
const data = {username: '', pwd: ''};

axios.defaults.timeout= 60 * 10000;
axios.interceptors.response.use(response => {
  return response.data;
}, error => {
  return Promise.reject(error);
})

// axios(options);
axios({ url: apiLogin, method: 'post', headers })

// axios(url[, options]);
axios(apiLogin, {mehtod: 'post', headers }

// axios[method](url[, options]) 
// 适用方法 get/delete/head/options
axios.get(apiUser, { headers })

// axios[method](url[, data[, options]]) 
// 适用方法 put/post/patch
axios.post(apiLogin, data, { headers })

axios.request({url: apiLogin, mehtod: 'post', headers })
复制代码

语法糖的源码解析

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  // 等同于 var instance = Axios.prototype.request.bind(context);
  // intance 等同于 context.request,以便于 axios(options)  或 axios(url, options) 调用
  var instance = bind(Axios.prototype.request, context);

  // 将 Axios.prototype 原型上的所有方法扩展到 instance 上,方法的上下文指向 context
  // 给 intance 扩展其它方法,以便于 axios.get()、axios.request()、axios.all() 等方式调用
  utils.extend(instance, Axios.prototype, context);

  // 将 context(axios的实例) 上的属性 defaults、 interceptors 扩展到 instance 上
  // 以实现 axios.defaults.timeout、axios.interceptors.request.use()方式调用
  utils.extend(instance, context);

  return instance;
}
复制代码

Axios 的类结构

Axios的属性和原型方法

image.png

function Axios(instanceConfig) {
  // 请求配置
  this.defaults = instanceConfig;
  // 请求和响应拦截器的配置
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// 实现 axios(config)、axios(url, config) 的调用方式
Axios.prototype.request = function request(config) {
  // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  config = mergeConfig(this.defaults, config);

  var promise;
  // ... 省略实现
  return promise;
};

Axios.prototype.getUri = function getUri(config) {};

// 实现 axios.get(url[, config]) 调用方式
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: (config || {}).data
    }));
  };
});

// 实现 axios.post(url[, data[, config]]) 调用方式
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});
复制代码

请求的基本流程

image.png

拦截器

拦截器的分类

拦截器分为 请求拦截器 和 响应拦截器,使用以下方式进行注册

  • 请求拦截器:用于修改 config 配置数据, 使用 interceptors.request.use() 注册
  • 响应拦截器:用于修改 response 响应数据, 使用 interceptors.response.use() 注册
// 请求拦截器
axios.interceptors.request.use(
(config) => { return config; }, 
(error) => { return Promise.reject(error);}
);

// 响应拦截器
axios.interceptors.response.use(
(response) => { return response; }, 
(error) => { return Promise.reject(error);}
);
复制代码

源码中如何处理拦截器?

先看 Promise 的链式调用

在没有添加任何拦截器时,axiospromise 链式执行机制

  1. 先将配置数据 config 包装为一个 promise 对象,使其拥有 then 方法
  2. 将执行 XMLHttpRequest 的方法包装为 promise, 使其可以链接调用
  3. 通过then方法执行,将 config 传递给 dispatchRequest
  4. 通过 dispatchRequest 执行,将 response 传递出来

XMLHttpRequest对象文档MDN

const config = {url: '/login', data: {username: 'abc'}};
// 使用 Promise.resolve 包装config为一个promise对象
let promise = Promise.resolve(config);

// 包装 xhr 请求为promise
const dispatchRequest = config => {
  return new Promise((resolve, reject) => {
     // 执行请求...
     const response = {code: 0, data: {}};
     resolve(response)
  })
}

// 定义一个 promise 队列
const chain = [dispatchRequest, undefined];

// 执行 promise 队列
promise = promise.then(chain.shift(), chain.shift());
// 等价于 promise = promise.then(dispatchRequest, undefined);
// 等价于 promise = promise.then((config) => dispatchRequest(config), undefined);
复制代码

拦截器的处理

 Axios.prototype.request = function request(config) {
  // ... 省略
  config = mergeConfig(this.defaults, config);
  // ... 省略
  
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);
   
  // 将请求拦截器添加到 chain 的前面
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
   
  // 将响应拦截器添加到 chain 的后面
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  
  /*
  chain = [
      reqSuccessFn1, reqErrorFn1, 
      reqSuccessFn2, reqErrorFn2, 
      ...
      dispatchRequest, undefined,
      resSuccessFn1, resErrorFn1, 
      resSuccessFn2, resErrorFn2,
      ...
  ]
  */ 
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};
复制代码

promise 链的执行流程

  1. 执行 请求拦截器,处理 config
  2. 处理好的 config 传递给 dispatchRequest
  3. 执行 异步请求 dispatchRequest 并返回一个 promise,传递请求结果 response
  4. 执行 响应拦截器,处理 response
  5. 返回 responseerror
configPromise
  // reqSuccessFn1, reqErrorFn1,
  .then(config => {
    return reqSuccessFn1(config)
  }, () => {
    return reqErrorFn1()
  })
  // reqSuccessFn2, reqErrorFn2,
  .then(config => {
    return reqSuccessFn2(config)
  }, (err) => {
    return reqErrorFn2(err)
  })
  // dispatchRequest, undefined,
    .then(config => {
      return dispatchRequest(config)
    }, undefined)
  // resSuccessFn1, resErrorFn1,
  .then(response => {
    return resSuccessFn1(response)
  }, (err) => {
    return resErrorFn1(err)
  })
  // resSuccessFn2, resErrorFn2,
  .then(response => {
    return resSuccessFn2(response)
  }, (err) => {
    return resErrorFn1(err)
  })
  .then(response => {
    console.log(response)
  })
  .catch(err => {
    console.log(err)
  })
复制代码

超时处理

XMLHttpRequest.timeout

  • axios 配置里有一个 timeout 配置项,默认为 0
  • timeout 会直接赋值给 xhr.timeout
  • config 里是可以自定义超时的消息内容的 config.timeoutErrorMessage
var xhr = new XMLHttpRequest();
xhr.open('GET', '/server', true);

xhr.timeout = 2000; // 超时时间,单位是毫秒 ms

xhr.onload = function () {
  // 请求完成。在此进行处理。
};

// 监听超时事件
xhr.ontimeout = function handleTimeout() {
  // 超时的消息内容
  var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
  // 这里可以看出来,config 里是可以自定义超时的消息内容的
  if (config.timeoutErrorMessage) {
    timeoutErrorMessage = config.timeoutErrorMessage;
  }
  reject(createError(
    timeoutErrorMessage,
    config,
    config.transitional && config.transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',
    request));

  // Clean up request
  request = null;
};

xhr.send(null);
复制代码

超时的异常处理

axios.defaults.timeout=30*1000;

// 单个请求处理超时
axios(url).catch(error => {
  const { message } = error;
  if(error.message.includes('timeout')){
    console.log('数据响应超时,请刷新重试')
  }
})
// 全局处理超时
axios.interceptors.response.use(function (response) {
  return response;
}, function (error) {
 if(error.message.includes('timeout')){
    console.log('数据响应超时')
  }
  return Promise.reject(error);
});
复制代码

取消请求

Axios 中取消请求的用法

取消所有请求

请求配置通过使用同一个 cancelToken,可以一次性取消所有请求

const CancelToken = axios.CancelToken;

const source = CancelToken.source();
axios
  .get('/cancel/server', {
    cancelToken: source.token
  })
  .then(function (response) {
    console.log(response.data)
  })
  .catch(function (err) {
    console.log(err)
  });

axios
  .get('/cancel/server',{
    cancelToken: source.token
  })
  .then(function (response) {
    console.log(response.data)
  })
  .catch(function (err) {
    console.log(err)
  });

  setTimeout(() => {
    source.cancel('我取消了所有的请求');
  }, 6*1000)
复制代码

取消单个请求

请求配置中,使用不同 cancelToken 可以按需取消不同的请求

let cancel1;
let cancel2;

axios
.get('/cancel/server',{
  cancelToken: new CancelToken(function executor(c) {
    cancel1 = c;
  })
})
.then(function (response) {
  console.log(response.data)
})
.catch(function (err) {
  console.log(err)
});

axios
.get('/cancel/server',{
  cancelToken: new CancelToken(function executor(c) {
    cancel2 = c;
  })
})
.then(function (response) {
  console.log(response.data)
})
.catch(function (err) {
  console.log(err)
});

setTimeout(() => {
  cancel1('我取消了 1 的请求');
}, 3*1000)

setTimeout(() => {
  cancel2('我取消了 2 的请求');
}, 6*1000)
复制代码

取消请求的源码解析

CancelToken

源码路径 lib/cancel/CancelToken.js

  • new CancelToken 产出一个 cancelToken 实例,实例上有一个 promise 对象
  • new CancelToken 接收的参数为一个函数,这个函数执行,会将 promise 状态变成 resolved,从而触发 promise 链中注册的 then 方法
  • CancelToken.source() 会返回一个 CancelToken 实例 token 和一个取消的函数 cancel
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  // 生成一个promise 属性(一个promise对象),把resolve 方法赋值给 resolvePromise 变量
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  // 接收的函数的参数是一个取消的异常消息
  // 函数执行时,会将 this.promise 状态改为 resolved,并传递取消的消息对象给注册的 then 方法
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }
    // 生成取消的消息对象,赋值给 reason 属性
    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

// CancelToken.source() 静态方法会返回一个 {token实例、取消方法}
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};
复制代码

cancelToken 的监听和触发

文件 lib/adapters/xhr.js

  • 如果 cancelToken 注册的 cancel 函数执行,则会将 promsie 的状态变为 resolved
  • resolved 之后,then 方法注册的回调会触发,会调用 xhr.abort() 执行,从而取消请求
  • reject(cancel) 会将 cancel 消息对象传递出去,外面请求通过 catch 方法接收到 取消消息对象cancel
module.exports = function xhrAdapter(config) {
    return new Promise(function dispatchXhrRequest(resolve, reject) {
      // ...
      var request = new XMLHttpRequest();
      // ...
      // 168行
      if (config.cancelToken) {
      // 如果请求还没有返回,执行cancel会触发这里的then回调执行
        config.cancelToken.promise.then(function onCanceled(cancel) {
          if (!request) {
            return;
          }
          request.abort();
          // cancel 为接收到的取消消息对象
          reject(cancel);
          // request 实例置为null
          request = null;
        });
      }
      // ...
    })
  }
复制代码

如果cancel执行时,还没有发起请求,或请求已经完成,那么会直接中断执行,抛出一个取消的消息,被外层的promise.catch 所捕获

文件 lib/core/dispatchRequest.js

// 23行
module.exports = function dispatchRequest(config) {
  // 请求未开始
  throwIfCancellationRequested(config);
  // ...
  return adapter(config).then(function onAdapterResolution(response) {
    // 请求已完成,返回成功
    throwIfCancellationRequested(config);
   // ...
    return response;
  }, function onAdapterRejection(reason) {
    // 请求已完成,返回异常
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);
      // ...
    }
    return Promise.reject(reason);
  });
};
复制代码

throwIfCancellationRequested 会调用 cancelToken.throwIfRequested()

// lib/core/dispatchRequest.js
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

// lib/cancel/CancelToken.js
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    // 抛出取消的消息
    throw this.reason;
  }
};
复制代码

数据转换器和Content-Type

Axios 中有三个数据转换器,两个用于请求数据转换,一个用于响应数据转换,请求数据转换器又分为url数据转换器和body(请求体)数据转换器。

  • transformRequest 请求数据转换器,对请求体(body)中的数据格式化,修改headers
  • paramsSerializer 对配置数据 params 做转换,params 一般用于get/delete/head请求,其它请求也可以设置 params 用于链接传参 ,主要指定数据序列化(字符串化)的规则
  • transformResponse 响应数据转换器,对响应数据进行解析或格式化

注意 transformRequesttransformResponse 的配置是一个函数数组,而 paramsSerializer 值是一个函数

transformRequest 与 transformResponse

文件: lib/defaults.js 中有 transformRequesttransformResponse 的默认配置

transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');
    // 如果body的类型是 FormData、Buffer、数据流、文件则返回 data,对headers不做处理,FormData浏览器会自动添加 contentType
    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;
    }
    // 如果是URLSearchParams对象,没有设置则设置 contentType为 'application/x-www-form-urlencoded;charset=utf-8' 数据被编码成以&分隔的键值对
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    // 如果是 JSON对象,没有设置则设置 contentType 为 application/json, 并将data进行序列化
    if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {
      setContentTypeIfUnset(headers, 'application/json');
      return JSON.stringify(data);
    }
    return data;
  }],

transformResponse: [function transformResponse(data) {
  var transitional = this.transitional;
  var silentJSONParsing = transitional && transitional.silentJSONParsing;
  var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
  var strictJSONParsing = !silentJSONParsing && this.responseType === 'json';

  if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) {
    try {
      return JSON.parse(data);
    } catch (e) {
      // JSON.parse 出错后,是否抛出异常信息,是的话,将抛出异常,不返回原始数据
      if (strictJSONParsing) {
        if (e.name === 'SyntaxError') {
          throw enhanceError(e, this, 'E_JSON_PARSE');
        }
        throw e;
      }
    }
  }

  return data;
}],
复制代码
  • contentType 对于 delete, get,head 是没有必要设置的,因为他们的参数是在url中传递的,只有 请求体 body 中的数据才需要设置 contentType
  • 如果不设置 contentType, post, put, patch默认设置为 application/x-www-form-urlencoded
  • 如果 body 的类型是 FormData, axios 会删除 contentType, 浏览器会默认设置 contentTypemultipart/form-data;

覆盖和追加 transformRequesttransformResponse

import axios from 'axios'

// 追加请求转换器
axios.defaults.transformRequest.push((data, headers) => {
  // ...处理data
  return data;
});

// 覆盖请求转换器
axios.defaults.transformRequest = [(data, headers) => {
  // ...处理data
  return data;
}];
// transformResponse 同理
复制代码

paramsSerializer

paramsSerializer 的调用时机

源码文件 lib/adapters/xhr.js 32行

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
     // ...
     // 在建立连接时,将 params 序列化成字符串,拼接在 url 后面
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    // ...
  });
};
复制代码

buildURL 在源码文件 lib/helpers/buildURL.js

module.exports = function buildURL(url, params, paramsSerializer) {
  // 如果你没有配置 params,则返回url,不处理
  if (!params) {
    return url;
  }

  var serializedParams;
  // 如果配置了 paramsSerializer 序列化函数,则调用函数
  if (paramsSerializer) {
    serializedParams = paramsSerializer(params);
  // 如果配置的 params 是 URLSearchParams 对象,则直接 toString
  } else if (utils.isURLSearchParams(params)) {
    serializedParams = params.toString();
  // 如果你没有指定 paramsSerializer 序列化方式,则使用默认的规则进行序列化
  } else {
    var parts = [];
    
    // 两层 forEach 循环 params
    utils.forEach(params, function serialize(val, key) {
       // 【重要】 如果 value 是  null 或 undefinded, 则会删除当前这个key,有时候服务端也需要值为 null 的key
      if (val === null || typeof val === 'undefined') {
        return;
      }
     // 【重要】如果 value 是数组,则key后面拼接上 [] 字符,有时服务端需要 [] 中带索引
      if (utils.isArray(val)) {
        key = key + '[]';
      } else {
      // 不是数组类型的value,会先变成数组,方便下面循环
        val = [val];
      }
       
      // 循环 value 数组
      utils.forEach(val, function parseValue(v) {
        // 日期对象会转换为 IOSString
        if (utils.isDate(v)) {
          v = v.toISOString();
        // 其它对象为直接使用 JSON.stringify
        } else if (utils.isObject(v)) {
          v = JSON.stringify(v);
        }
        // 将 key和value都进行 encode,但会保留 : $ , + [ ] 这几个符号
        parts.push(encode(key) + '=' + encode(v));
      });
    });
    // 通过 & 拼接起来
    serializedParams = parts.join('&');
  }
  // 如果生成了 序列化的字符串
  if (serializedParams) {
    // 如果url带有 # 号,请求时,会将 # 及后面的字符都去掉
    var hashmarkIndex = url.indexOf('#');
    if (hashmarkIndex !== -1) {
      url = url.slice(0, hashmarkIndex);
    }
    // url 中有 ? 号,则用 & 拼接,没有则用 ? 拼接
    url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams;
  }
  // 返回拼接好的 url
  return url;
};
复制代码

重要

params 序列化方式会直接影响服务端的接收,这里要和服务端约定好序列化的规则,特别是数组序列化为字符串的规则

不同的规则,会产出不同的字符串,会直接影响服务端数据接收方式,我们可以使用 qs 个包来进行数据的序列化

qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' })
// 'a[0]=b&a[1]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' })
// 'a[]=b&a[]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })
// 'a=b&a=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'comma' })
// 'a=b,c'
复制代码

示例

为单个请求指定 params 的序列化规则

import Qs from 'qs'

axios.get('/userList', {
 params: {
     page: 1,
     pageSize: 20,
     keywords: 'a',
     ids: [1, 2, 3]
 },
paramsSerializer: function(params) {
    return Qs.stringify(params, {arrayFormat: 'brackets'})
  },
}
复制代码

请所有请求指定 params 的序列化规则

import Qs from 'qs'
axios.defaults.paramsSerializer = params => Qs.stringify(params, {arrayFormat: 'brackets'})
复制代码

header的配置

种配置 header 的方式

import axios from 'axios'

// 通用header
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; // xhr标识

// 针对某种请求类型设置的header
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';

// 设置某次请求的header
axios.get(url, {
  headers: {
    'Authorization': 'whr1',
  },
})
复制代码

源码文件 lib/core/dispatchRequest.js 38行

  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers
  );
复制代码

header 配置的优先级

具体请求的配置 > 方法类型配置 > common 配置

JWT中如何配置 header ?

如果你的项目的用户认证是JWT方式,那么用户认证token一般是通过 header 传递给服务端进行校验的,一般配置流程如下:

  1. 用户登录
  2. 服务端响应登录数据和token
  3. 前端存储 token 到本地,一般为 localstorage 也可以是 cookie
  4. 后面需要认证的请求都把 token 带到header里传给服务端
  5. 服务端校验token是否有效,有效则正常响应,无效则返回异常消息
// 配置请求时, header 中带上 token
axios.interceptors.request.use(function (config) {
  const token = window.localstorage.getItem('token');
  if(token){
    config.headers.common['Authorization'] = token;
  }
  return config;
});

// 业务中 登录后缓存 token
axios.post( '/login', { username: '', pwd: '' }).then(res => {
    const token = res.data.token
    window.localstorage.setItem('token', token)
})
复制代码

Guess you like

Origin juejin.im/post/7061829864072052767