axios 详细讲解 , 每个步骤带简单 demo

3,106 阅读7分钟

为什么 使用axios

  1. 支持浏览器和node.js
  2. 支持promise
  3. 能拦截请求和响应
  4. 能转换请求和响应数据
  5. 能取消请求
  6. 自动转换JSON数据
  7. 浏览器端支持防止CSRF(跨站请求伪造)

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项目的入口文件,我们先来看下axios.js的源码 能够实现axios的多种使用方式的核心是 createInstance 方法:

模拟 axios.js 入口文件 createInstance 的方法

  1. bind 方法

    主要是 apply 的 使用 可以看这篇文章

    module.exports = function bind(fn, thisArg) {
        // 返回的是一个函数
        return function wrap() {
            // args 其实就是 参数的集合
            var args = new Array(arguments.length);
            for (var i = 0; i < args.length; i++) {
            args[i] = arguments[i];
            }
            // 改变 this 的指向 可以理解成 fn 里边的 this 是 thisArg
            return fn.apply(thisArg, args);
        };
    };
    
  2. forEach 方法

    function forEach(obj, fn) {
        // 如果没值 直接返回
        if (obj === null || typeof obj === 'undefined') {
            return;
        }
        // 如果还没有可写的东西,就强制一个数组
        if (typeof obj !== 'object') {
            obj = [obj];
        }
    
        if (isArray(obj)) {
            // 如果是数组
            for (var i = 0, l = obj.length; i < l; i++) {
                // 这里用到了call 参数 null 说明不改变 this 的指向
                fn.call(null, obj[i], i, obj);
            }
        } else {
            // 如果是对象
            for (var key in obj) {
                // 自身的属性不包含原型上的
                if (Object.prototype.hasOwnProperty.call(obj, key)) {
                    // 传参的时候相当于 把 key val 单做参数调用
                    fn.call(null, obj[key], key, obj);
                }
            }
        }
    }
    
  3. extend 方法

    我脑瓜子比较笨, 在这里用到了 bind 方法 我花了很长时间理解 为什么要这么写

    function extend(a, b, thisArg) {
        // 循环 B 把 B 的 key val 进行回调的形式 赋给 A
        // 合并对象 把 B 的属性给 A
        forEach(b, function assignValue(val, key) {
            if (thisArg && typeof val === 'function') {
                // 注意 是函数的时候 调用 bind
                a[key] = bind(val, thisArg);
            } else {
                // 把 B 的属性给 A
                a[key] = val;
            }
        });
        return a;
    }
    

    createInstance这个方法的 倒数第二行 用到了 extend 方法 但是 extend 返回的是 Function 所以 createInstance这个方法的 return 也是一个 Function ,这个Function还会有Axios.prototype上的每个方法作为静态方法,且这些方法的上下文都是指向同一个对象。

    // 首先创建 Axios 构造函数
    function Axios(instanceConfig) {
        this.defaults = instanceConfig;
        // interceptors 拦截器
        this.interceptors = {
            request: function() {},
            response: function () {}
        };
    }
    
    // 原型上的  request 方法
    Axios.prototype.request = function request(config) {
        console.log(this)
    }
    
    // 原型上的  get post... 方法
    Axios.prototype.get = function request(config) {
        console.log(this)
    }
    
    // 创建
    function createInstance(defaultConfig) {
        var context = new Axios({
            name: 'wang'
        });
    }
    // 如果到这 我们看 request 方法中的 this 是 指向了 Axios.prototype
    
    // 但是我们想用 构造函数的 this 呢
    function createInstance(defaultConfig) {
        var context = new Axios({
            name: 'wang'
        });
    
        // 在调用 bind 方法之后 request 中的 this 指向了 实例
        // 而且是在执行的时候改变的
        var instance = bind(Axios.prototype.request, context);
    }
    
    // 如果看到这里 我们应该明白为什么 使用 bind 这个函数了
    
    function createInstance(defaultConfig) {
        var context = new Axios(defaultConfig);
        // 这一行 就是 在调用 createInstance 这个方法的时候  return 其实 Axios.prototype.request 这个方法
        var instance = bind(Axios.prototype.request, context);
        // instance 合并 原型上的方法,而且原型上所有的方法的this 都指向 实例
        utils.extend(instance, Axios.prototype, context);
    
        // 把实例上的 属性 赋值给 instance
        // 其实是new Axios().defaults 和 new Axios().interceptors
        // 也就是为什么默认配置 axios.defaults 和拦截器  axios.interceptors 可以使用的原因
        utils.extend(instance, context);
        return instance;
    }
    

axios 核心方法 request 之拦截器

首先我们看一下这个方法都干了什么

  1. 设置 我们传入的 配置config
  2. 添加请求拦截器requestInterceptors、响应拦截器 responseInterceptors
  3. 发送请求 dispatchRequest

我先把 拦截器的代码注释给写一下, 如果没有明白看我下边写的小栗子 就能彻底明白拦截器

    Axios.prototype.request = function request(config) {
        // 判断 config 参数是否是 字符串,如果是则认为第一个参数是 URL,第二个参数是真正的config
        if (typeof config === 'string') {
            config = arguments[1] || {};
            // 把 url 放置到 config 对象中,便于之后的 mergeConfig
            config.url = arguments[0];
        } else {
            // 如果 config 参数是否是 字符串,则整体都当做config
            config = config || {};
        }

        // 合并默认配置和传入的配置
        config = mergeConfig(this.defaults, config);

        // 设置请求方法 如果传进来有方法用传进来的
        if (config.method) {
            config.method = config.method.toLowerCase();
            // 如果没有 用默认的
        } else if (this.defaults.method) {
            config.method = this.defaults.method.toLowerCase();
            // 默认的也没有就用 get 方法
        } else {
            config.method = 'get';
        }

        // 创建拦截器链. dispatchRequest 是重中之重,后续重点
        var chain = [dispatchRequest, undefined];

        // 初始化一个promise对象,状态为resolved,接收到的参数为已经处理合并过的config对象
        var promise = Promise.resolve(config);

        // push各个拦截器方法 注意:interceptor.fulfilled 或 interceptor.rejected 是可能为undefined
        this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
            // 请求拦截器逆序 注意此处的 forEach 是自定义的拦截器的forEach方法
            chain.unshift(interceptor.fulfilled, interceptor.rejected);
        });

        this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
            // 响应拦截器顺序 注意此处的 forEach 是自定义的拦截器的forEach方法
            chain.push(interceptor.fulfilled, interceptor.rejected);
        });

        // 循环拦截器的链
        while (chain.length) {
            promise = promise.then(chain.shift(), chain.shift()); // 每一次向外弹出拦截器
        }
        return promise;
    };

在这个方法中 用到了 InterceptorManager.js

    'use strict';
    var utils = require('./../utils');
    // 拦截器的初始化 其实就是一组钩子函数
    function InterceptorManager() {
        this.handlers = [];
    }

    // 调用拦截器实例的use时就是往钩子函数中push方法
    InterceptorManager.prototype.use = function use(fulfilled, rejected) {
        this.handlers.push({
            fulfilled: fulfilled,
            rejected: rejected
        });
        // 这个我也不知道干啥的
        return this.handlers.length - 1;
    };

    // 拦截器是可以取消的,根据use的时候返回的ID,把某一个拦截器方法置为null
    // 不能用 splice 或者 slice 的原因是 删除之后 id 就会变化,导致之后的顺序或者是操作不可控
    InterceptorManager.prototype.eject = function eject(id) {
        if (this.handlers[id]) {
            this.handlers[id] = null;
        }
    };

    // 这就是在 Axios的request方法中 中循环拦截器的方法 forEach 循环执行钩子函数
    InterceptorManager.prototype.forEach = function forEach(fn) {
        utils.forEach(this.handlers, function forEachHandler(h) {
            if (h !== null) {
                fn(h);
            }
        });
    };
    module.exports = InterceptorManager;

添加拦截器的过程

在我们项目当中 执行下边代码 说明添加了一个 请求拦截器

axios.interceptors.request.use((config) => {
    // 思考  为什么 用 return
    return config
}, error => {
    return error
})

这个时候 会调用 InterceptorManager.js 的 use 方法

    InterceptorManager.prototype.use = function use(fulfilled, rejected) {
    this.handlers.push({
        fulfilled: fulfilled,
        rejected: rejected
    });
        return this.handlers.length - 1;
    };

我们把添加拦截器的回调函数 保存到了 handlers 中

当我们执行 axios({ url: 'xxx' }) 调用了 Axios.prototype.request 这个方法 在这个方法当中 又调用了 InterceptorManager.js 的 forEach 方法

    InterceptorManager.prototype.forEach = function forEach(fn) {
    utils.forEach(this.handlers, function forEachHandler(h) {
        if (h !== null) {
            // 当前的 钩子函数 { fulfilled: fulfilled,  rejected: rejected }
            fn(h);
        }
    });
    };
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

// 其实 到这里我们知道了 拦截器的 添加过程 ,只不过 chain 具体干了什么还不清楚

拦截器的例子

// 我们先看一下 下边的一个例子 // 执行 this.interceptors.request.forEach 是不是得到了 下边的一个数组

    // 其实 chain 最后的结果是
    let chain = [
        // 还记得 让思考的一个问题, 为什么 在 axios.interceptors.request.use 添加拦截器的回调函数中 执行 return config 吗?
        function fulfilled (config) {
            // 这个方法 就是我们在执行  axios.interceptors.request.use 得第一个参数
            // 这里边的代码就是 use 第一个参数的代码
            config.name = 'zhang'
            return config
        },
        function rejected () {
            // 这个是 第二个参数
        },
        function fulfilled (config) {
            // 如果不执行 都 不执行 return 大家自己写一个demo 把这段代码 执行以下
            config.age = 20
            return config
        },
        function rejected () {},
        ......
    ]
    // 然后我们定义个 Promis, resolve 接收的是一个 对象, 是不是相当于 我们传入的 请求 的 data 数据
    var promise = Promise.resolve({
        name: 'wang',
        age: 27
    });

    while (chain.length) {
        // 思考 为什么 可以链式操作, 其实就是 拦截器回调函数 return config的作用
        promise = promise.then(chain.shift(), chain.shift());
    }
    // Axios.prototype.request return 的 就是 promise
    // 这里我们直接调用 来进行模拟
    promise.then( (config) => {
        console.log(config)  // {name: "zhang", age: 20}
    })

求点赞, 给点动力继续写