Axios 是什么?

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。 今天我们来进入 Axios 源码解析

Axios 功能

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

希望通过源码来慢慢理清这些功能的实现原理

Axios 使用

执行 GET 请求

  1. axios.get('/user?ID=12345')
  2. .then(function (response) {
  3. console.log(response);
  4. })
  5. 复制代码

执行 POST 请求

  1. axios.post('/user', {
  2. name: 'zxm',
  3. age: 18,
  4. })
  5. .then(function (response) {
  6. console.log(response);
  7. })
  8. 复制代码

使用方式不是本次主题的重点,具体使用方式可以参照 Axios 中文说明

源码拉下来直接进入 lib 文件夹开始解读源码

源码解读

lib/ axios.js 开始

  1. 'use strict';
  2. var utils = require('./utils');
  3. var bind = require('./helpers/bind');
  4. var Axios = require('./core/Axios');
  5. var mergeConfig = require('./core/mergeConfig');
  6. var defaults = require('./defaults');
  7. // 重点 createInstance 方法
  8. // 先眼熟一个代码 下面讲完工具函数会再具体来讲解 createInstance
  9. function createInstance(defaultConfig) {
  10. // 实例化 Axios
  11. var context = new Axios(defaultConfig);
  12. // 自定义 bind 方法 返回一个函数()=> {Axios.prototype.request.apply(context,args)}
  13. var instance = bind(Axios.prototype.request, context);
  14. // Axios 源码的工具类
  15. utils.extend(instance, Axios.prototype, context);
  16. utils.extend(instance, context);
  17. return instance;
  18. }
  19. // 传入一个默认配置 defaults 配置先不管,后面会有具体的细节
  20. var axios = createInstance(defaults);
  21. // 下面都是为 axios 实例化的对象增加不同的方法。
  22. axios.Axios = Axios;
  23. axios.create = function create(instanceConfig) {
  24. return createInstance(mergeConfig(axios.defaults, instanceConfig));
  25. };
  26. axios.Cancel = require('./cancel/Cancel');
  27. axios.CancelToken = require('./cancel/CancelToken');
  28. axios.isCancel = require('./cancel/isCancel');
  29. axios.all = function all(promises) {
  30. return Promise.all(promises);
  31. };
  32. axios.spread = require('./helpers/spread');
  33. module.exports = axios;
  34. module.exports.default = axios;
  35. 复制代码

lib/ util.js 工具方法

有如下方法:

  1. module.exports = {
  2. isArray: isArray,
  3. isArrayBuffer: isArrayBuffer,
  4. isBuffer: isBuffer,
  5. isFormData: isFormData,
  6. isArrayBufferView: isArrayBufferView,
  7. isString: isString,
  8. isNumber: isNumber,
  9. isObject: isObject,
  10. isUndefined: isUndefined,
  11. isDate: isDate,
  12. isFile: isFile,
  13. isBlob: isBlob,
  14. isFunction: isFunction,
  15. isStream: isStream,
  16. isURLSearchParams: isURLSearchParams,
  17. isStandardBrowserEnv: isStandardBrowserEnv,
  18. forEach: forEach,
  19. merge: merge,
  20. deepMerge: deepMerge,
  21. extend: extend,
  22. trim: trim
  23. };
  24. 复制代码

is开头的isXxx方法名 都是判断是否是 Xxx 类型 ,这里就不做明说 主要是看下 后面几个方法

extend 将 b 里面的属性和方法继承给 a , 并且将 b 里面的方法的执行上个下文都绑定到 thisArg

  1. // a, b,thisArg 参数都为一个对象
  2. function extend(a, b, thisArg) {
  3. forEach(b, function assignValue(val, key) {
  4. // 如果指定了 thisArg 那么绑定执行上下文到 thisArg
  5. if (thisArg && typeof val === 'function') {
  6. a[key] = bind(val, thisArg);
  7. } else {
  8. a[key] = val;
  9. }
  10. });
  11. return a;
  12. }
  13. 复制代码

抽象的话看个例子
Axios 源码解析 - 图1
这样是不是就一目了然。fn2 函数没有拿自己对象内的 age = 20 而是被指定到了 thisArg 中的 age

自定义 forEach 方法遍历基本数据,数组,对象。

  1. function forEach(obj, fn) {
  2. if (obj === null || typeof obj === 'undefined') {
  3. return;
  4. }
  5. if (typeof obj !== 'object') {
  6. obj = [obj];
  7. }
  8. if (isArray(obj)) {
  9. for (var i = 0, l = obj.length; i < l; i++) {
  10. fn.call(null, obj[i], i, obj);
  11. }
  12. } else {
  13. for (var key in obj) {
  14. if (Object.prototype.hasOwnProperty.call(obj, key)) {
  15. fn.call(null, obj[key], key, obj);
  16. }
  17. }
  18. }
  19. }
  20. 复制代码

merge 合并对象的属性,相同属性后面的替换前的

  1. function merge(/* obj1, obj2, obj3, ... */) {
  2. var result = {};
  3. function assignValue(val, key) {
  4. if (typeof result[key] === 'object' && typeof val === 'object') {
  5. result[key] = merge(result[key], val);
  6. } else {
  7. result[key] = val;
  8. }
  9. }
  10. for (var i = 0, l = arguments.length; i < l; i++) {
  11. forEach(arguments[i], assignValue);
  12. }
  13. return result;
  14. }

bind -> lib/ helpers/ bind.js 这个很清楚,返回一个函数,并且传入的方法执行上下文绑定到 thisArg上。

  1. module.exports = function bind(fn, thisArg) {
  2. return function wrap() {
  3. var args = new Array(arguments.length);
  4. for (var i = 0; i < args.length; i++) {
  5. args[i] = arguments[i];
  6. }
  7. return fn.apply(thisArg, args);
  8. };
  9. };
  10. 复制代码

好勒那么 axios/util 方法我们就基本没有问题拉
看完这些工具类方法后我们在回过头看之前的 createInstance 方法

  1. function createInstance(defaultConfig) {
  2. // 实例化 Axios, Axios下面会讲到
  3. var context = new Axios(defaultConfig);
  4. // 将 Axios.prototype.request 的执行上下文绑定到 context
  5. // bind 方法返回的是一个函数
  6. var instance = bind(Axios.prototype.request, context);
  7. // 将 Axios.prototype 上的所有方法的执行上下文绑定到 context , 并且继承给 instance
  8. utils.extend(instance, Axios.prototype, context);
  9. // 将 context 继承给 instance
  10. utils.extend(instance, context);
  11. return instance;
  12. }
  13. // 传入一个默认配置
  14. var axios = createInstance(defaults);
  15. 复制代码

总结:createInstance 函数返回了一个函数 instance.

  1. instance 是一个函数 Axios.prototype.request 且执行上下文绑定到 context。
  2. instance 里面还有 Axios.prototype 上面的所有方法,并且这些方法的执行上下文也绑定到 context。
  3. instance 里面还有 context 上的方法。

    Axios 实例源码

    1. 'use strict';
    2. var utils = require('./../utils');
    3. var buildURL = require('../helpers/buildURL');
    4. var InterceptorManager = require('./InterceptorManager');
    5. var dispatchRequest = require('./dispatchRequest');
    6. var mergeConfig = require('./mergeConfig');
    7. function Axios(instanceConfig) {
    8. this.defaults = instanceConfig;
    9. this.interceptors = {
    10. request: new InterceptorManager(),
    11. response: new InterceptorManager()
    12. };
    13. }
    14. // 核心方法 request
    15. Axios.prototype.request = function request(config) {
    16. // ... 单独讲
    17. };
    18. // 合并配置将用户的配置 和默认的配置合并
    19. Axios.prototype.getUri = function getUri(config) {
    20. config = mergeConfig(this.defaults, config);
    21. return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
    22. };
    23. // 这个就是给 Axios.prototype 上面增加 delete,get,head,options 方法
    24. // 这样我们就可以使用 axios.get(), axios.post() 等等方法
    25. utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
    26. Axios.prototype[method] = function(url, config) {
    27. // 都是调用了 this.request 方法
    28. return this.request(utils.merge(config || {}, {
    29. method: method,
    30. url: url
    31. }));
    32. };
    33. });
    34. utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
    35. Axios.prototype[method] = function(url, data, config) {
    36. return this.request(utils.merge(config || {}, {
    37. method: method,
    38. url: url,
    39. data: data
    40. }));
    41. };
    42. });
    43. module.exports = Axios;
    44. 复制代码

    上面的所有的方法都是通过调用了 this.request 方法
    那么我们就来看这个 request 方法,个人认为是源码内的精华也是比较难理解的部分,使用到了 Promise 的链式调用,也使用到了中间件的思想。

    1. function Axios(instanceConfig) {
    2. this.defaults = instanceConfig;
    3. this.interceptors = {
    4. request: new InterceptorManager(),
    5. response: new InterceptorManager()
    6. };
    7. }
    8. Axios.prototype.request = function request(config) {
    9. // Allow for axios('example/url'[, config]) a la fetch API
    10. if (typeof config === 'string') {
    11. config = arguments[1] || {};
    12. config.url = arguments[0];
    13. } else {
    14. config = config || {};
    15. }
    16. // 合并配置
    17. config = mergeConfig(this.defaults, config);
    18. // 请求方式,没有默认为 get
    19. config.method = config.method ? config.method.toLowerCase() : 'get';
    20. // 重点 这个就是拦截器的中间件
    21. var chain = [dispatchRequest, undefined];
    22. // 生成一个 promise 对象
    23. var promise = Promise.resolve(config);
    24. // 将请求前方法置入 chain 数组的前面 一次置入两个 成功的,失败的
    25. this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    26. chain.unshift(interceptor.fulfilled, interceptor.rejected);
    27. });
    28. // 将请求后的方法置入 chain 数组的后面 一次置入两个 成功的,失败的
    29. this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    30. chain.push(interceptor.fulfilled, interceptor.rejected);
    31. });
    32. // 通过 shift 方法把第一个元素从其中删除,并返回第一个元素。
    33. while (chain.length) {
    34. promise = promise.then(chain.shift(), chain.shift());
    35. }
    36. return promise;
    37. };
    38. 复制代码

    看到这里有点抽象,没关系。我们先讲下拦截器。在请求或响应被 thencatch 处理前拦截它们。使用方法参考 Axios 中文说明 ,大致使用如下。

    1. // 添加请求拦截器
    2. axios.interceptors.request.use(function (config) {
    3. // 在发送请求之前做些什么
    4. return config;
    5. }, function (error) {
    6. // 对请求错误做些什么
    7. return Promise.reject(error);
    8. });
    9. // 添加响应拦截器
    10. axios.interceptors.response.use(function (response) {
    11. // 对响应数据做点什么
    12. return response;
    13. }, function (error) {
    14. // 对响应错误做点什么
    15. return Promise.reject(error);
    16. });
    17. 复制代码

    通过 promise 链式调用一个一个函数,这个函数就是 chain 数组里面的方法

    1. // 初始的 chain 数组 dispatchRequest 是发送请求的方法
    2. var chain = [dispatchRequest, undefined];
    3. // 然后 遍历 interceptors
    4. // 注意 这里的 forEach 不是 Array.forEach, 也不是上面讲到的 util.forEach. 具体 拦截器源码 会讲到
    5. // 现在我们只要理解他是遍历给 chain 里面追加两个方法就可以
    6. this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    7. chain.unshift(interceptor.fulfilled, interceptor.rejected);
    8. });
    9. this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    10. chain.push(interceptor.fulfilled, interceptor.rejected);
    11. });
    12. 复制代码

    然后添加了请求拦截器和相应拦截器 chain 会是什么样子呢 (重点)

    1. chain = [ 请求拦截器的成功方法,请求拦截器的失败方法,dispatchRequest undefined, 响应拦截器的成功方法,响应拦截器的失败方法 ]。
    2. 复制代码

    好了,知道具体使用使用之后是什么样子呢?回过头去看 request 方法
    每次请求的时候我们有一个

    1. while (chain.length) {
    2. promise = promise.then(chain.shift(), chain.shift());
    3. }
    4. 意思就是将 chainn 内的方法两两拿出来执行 成如下这样
    5. promise.then(请求拦截器的成功方法, 请求拦截器的失败方法)
    6. .then(dispatchRequest, undefined)
    7. .then(响应拦截器的成功方法, 响应拦截器的失败方法)
    8. 复制代码

    现在看是不是清楚了很多,拦截器的原理。现在我们再来看 InterceptorManager 的源码

    InterceptorManager 拦截器源码

    lib/ core/ InterceptorManager.js

  1. 'use strict';
  2. var utils = require('./../utils');
  3. function InterceptorManager() {
  4. // 存放方法的数组
  5. this.handlers = [];
  6. }
  7. // 通过 use 方法来添加拦截方法
  8. InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  9. this.handlers.push({
  10. fulfilled: fulfilled,
  11. rejected: rejected
  12. });
  13. return this.handlers.length - 1;
  14. };
  15. // 通过 eject 方法来删除拦截方法
  16. InterceptorManager.prototype.eject = function eject(id) {
  17. if (this.handlers[id]) {
  18. this.handlers[id] = null;
  19. }
  20. };
  21. // 添加一个 forEach 方法,这就是上述说的 forEach
  22. InterceptorManager.prototype.forEach = function forEach(fn) {
  23. // 里面还是依旧使用了 utils 的 forEach, 不要纠结这些 forEach 的具体代码
  24. // 明白他们干了什么就可以
  25. utils.forEach(this.handlers, function forEachHandler(h) {
  26. if (h !== null) {
  27. fn(h);
  28. }
  29. });
  30. };
  31. module.exports = InterceptorManager;
  32. 复制代码

dispatchRequest 源码

lib/ core/ dispatchRequest .js

  1. 'use strict';
  2. var utils = require('./../utils');
  3. var transformData = require('./transformData');
  4. var isCancel = require('../cancel/isCancel');
  5. var defaults = require('../defaults');
  6. var isAbsoluteURL = require('./../helpers/isAbsoluteURL');
  7. var combineURLs = require('./../helpers/combineURLs');
  8. // 请求取消时候的方法,暂不看
  9. function throwIfCancellationRequested(config) {
  10. if (config.cancelToken) {
  11. config.cancelToken.throwIfRequested();
  12. }
  13. }
  14. module.exports = function dispatchRequest(config) {
  15. throwIfCancellationRequested(config);
  16. // 请求没有取消 执行下面的请求
  17. if (config.baseURL && !isAbsoluteURL(config.url)) {
  18. config.url = combineURLs(config.baseURL, config.url);
  19. }
  20. config.headers = config.headers || {};
  21. // 转换数据
  22. config.data = transformData(
  23. config.data,
  24. config.headers,
  25. config.transformRequest
  26. );
  27. // 合并配置
  28. config.headers = utils.merge(
  29. config.headers.common || {},
  30. config.headers[config.method] || {},
  31. config.headers || {}
  32. );
  33. utils.forEach(
  34. ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
  35. function cleanHeaderConfig(method) {
  36. delete config.headers[method];
  37. }
  38. );
  39. // 这里是重点, 获取请求的方式,下面会将到
  40. var adapter = config.adapter || defaults.adapter;
  41. return adapter(config).then(function onAdapterResolution(response) {
  42. throwIfCancellationRequested(config);
  43. // 难道了请求的数据, 转换 data
  44. response.data = transformData(
  45. response.data,
  46. response.headers,
  47. config.transformResponse
  48. );
  49. return response;
  50. }, function onAdapterRejection(reason) {
  51. // 失败处理
  52. if (!isCancel(reason)) {
  53. throwIfCancellationRequested(config);
  54. // Transform response data
  55. if (reason && reason.response) {
  56. reason.response.data = transformData(
  57. reason.response.data,
  58. reason.response.headers,
  59. config.transformResponse
  60. );
  61. }
  62. }
  63. return Promise.reject(reason);
  64. });
  65. };

看了这么多,我们还没看到是通过什么来发送请求的,现在我们看看在最开始实例化 createInstance 方法中我们传入的 defaults 是什么
var axios = createInstance(defaults);

lib/ defaults.js

  1. 'use strict';
  2. var utils = require('./utils');
  3. var normalizeHeaderName = require('./helpers/normalizeHeaderName');
  4. var DEFAULT_CONTENT_TYPE = {
  5. 'Content-Type': 'application/x-www-form-urlencoded'
  6. };
  7. function setContentTypeIfUnset(headers, value) {
  8. if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
  9. headers['Content-Type'] = value;
  10. }
  11. }
  12. // getDefaultAdapter 方法是来获取请求的方式
  13. function getDefaultAdapter() {
  14. var adapter;
  15. // process 是 node 环境的全局变量
  16. if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
  17. // 如果是 node 环境那么久通过 node http 的请求方法
  18. adapter = require('./adapters/http');
  19. } else if (typeof XMLHttpRequest !== 'undefined') {
  20. // 如果是浏览器啥的 有 XMLHttpRequest 的就用 XMLHttpRequest
  21. adapter = require('./adapters/xhr');
  22. }
  23. return adapter;
  24. }
  25. var defaults = {
  26. // adapter 就是请求的方法
  27. adapter: getDefaultAdapter(),
  28. // 下面一些请求头,转换数据,请求,详情的数据
  29. // 这也就是为什么我们可以直接拿到请求的数据时一个对象,如果用 ajax 我们拿到的都是 jSON 格式的字符串
  30. // 然后每次都通过 JSON.stringify(data)来处理结果。
  31. transformRequest: [function transformRequest(data, headers) {
  32. normalizeHeaderName(headers, 'Accept');
  33. normalizeHeaderName(headers, 'Content-Type');
  34. if (utils.isFormData(data) ||
  35. utils.isArrayBuffer(data) ||
  36. utils.isBuffer(data) ||
  37. utils.isStream(data) ||
  38. utils.isFile(data) ||
  39. utils.isBlob(data)
  40. ) {
  41. return data;
  42. }
  43. if (utils.isArrayBufferView(data)) {
  44. return data.buffer;
  45. }
  46. if (utils.isURLSearchParams(data)) {
  47. setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
  48. return data.toString();
  49. }
  50. if (utils.isObject(data)) {
  51. setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
  52. return JSON.stringify(data);
  53. }
  54. return data;
  55. }],
  56. transformResponse: [function transformResponse(data) {
  57. /*eslint no-param-reassign:0*/
  58. if (typeof data === 'string') {
  59. try {
  60. data = JSON.parse(data);
  61. } catch (e) { /* Ignore */ }
  62. }
  63. return data;
  64. }],
  65. /**
  66. * A timeout in milliseconds to abort a request. If set to 0 (default) a
  67. * timeout is not created.
  68. */
  69. timeout: 0,
  70. xsrfCookieName: 'XSRF-TOKEN',
  71. xsrfHeaderName: 'X-XSRF-TOKEN',
  72. maxContentLength: -1,
  73. validateStatus: function validateStatus(status) {
  74. return status >= 200 && status < 300;
  75. }
  76. };
  77. defaults.headers = {
  78. common: {
  79. 'Accept': 'application/json, text/plain, */*'
  80. }
  81. };
  82. utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  83. defaults.headers[method] = {};
  84. });
  85. utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  86. defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
  87. });
  88. module.exports = defaults;

总结

  1. Axios 的源码走读一遍确实可以看到和学习到很多的东西。
  2. Axios 还有一些功能:请求的取消,请求超时的处理。这里我没有全部说明。
  3. Axios 通过在请求中添加 toke 并验证方法,让客户端支持防御 XSRF Django CSRF 原理分析

    最后

    如果看的还不是很明白,不用担心,这基本上是我表达,书写的不够好。因为在写篇文章时我也曾反复的删除,重写,总觉得表达的不够清楚。为了加强理解和学习大家可以去 github 将代码拉下来对照着来看。这样在对 Axios 源码解析时会更加清晰

from:叫我小明呀 Axios 源码解释