axios源码模拟实现

一、分析文件结构

  • /dist/ 输出目录,最后输出的axios整体文件
    • axios.js 未压缩的
    • axios.min.js 压缩后的
  • /lib/ 源码目录,所有的源代码都放在这里
    • /adapters/ 请求的适配器
      • http.js 实现http适配器(包装 http包),在 node.js向远程服务器发送请求时使用
      • xhr.js 实现 xhr适配器(包装 xhr对象),在浏览器端发送请求时使用
    • /cancel/ 定义取消功能
      • Cancel.js 构造函数,用来创建取消时的错误对象
      • CancelToken.js取消请求的构造函数
      • isCancel.js 检测参数是否为取消对象
    • /core/ 一些核心功能
      • Axios.js axios的核心主类,文件存放的是 axios的构造函数
      • buildFullPath.js 构建完整 URL 的函数文件
      • createError.js 创建指定信息的 Error 对象
      • dispatchRequest.js 发送请求的函数文件
      • enhanceError.js 更新错误对象的函数文件
      • InterceptorManager.js 拦截器的管理器
      • mergeConfig.js 合并配置的函数文件
      • settle.js 根据 http响应状态,改变Promise 的状态
      • transformData.js 数据格式转化函数
    • /helpers/ 一些功能函数
      • bind.js 返回一个新的函数,并将新函数this绑定到一个对象身上
      • buildURL.js 创建一个 URL,将参数缀到URL 后,并返回格式化后的内容
      • combineURLs.js 合并URL
      • cookies.js 处理cookie
      • deprecatedMethod.js 控制台提示不赞成使用的方法
      • isAbsoluteURL.js 检测是否为绝对路径的 URL
      • isURLSameOrigin.js 检测是否为同源的 URL
      • normalizeHeaderName.js 统一化头信息, 统一变为大写
      • parseHeaders.js 解析将头信息解析为对象
      • spread.js 用于调用函数和扩展参数数组的语法糖
        -axios.js axios入口文件
    • defaults.js axios配置文件
    • utils.js 工具函数文件
  • package.json 包信息
  • index.d.ts 配置 TypeScript的声明文件
  • index.js 整个包的入口文件

二、模拟实现 axios 对象创建过程

  模拟实现 axios 对象创建过程可以看下面代码以及代码注释。

<script>
    // 构造函数
    function Axios(config){
        // 初始化
        this.defaults = config;// 为了创建 default 默认属性
        this.interceptors = {
            request: {},
            response: {}
        }
    }
    // 原型添加相关的方法
    Axios.prototype.request = function(config){
        console.log('发送请求,请求的类型为 '+ config.method);
    }
    Axios.prototype.get = function(config){
        return this.request({method: 'GET'});
    }
    Axios.prototype.post = function(config){
        return this.request({method: 'POST'});
    }

    // 声明函数
    function createInstance(config){
        // 实例化一个对象
        // 可以 context.get() context.post() 这样使用,但是不能当做函数使用,即不能 context() 这样使用
        let context = new Axios(config);
        // 创建请求函数
        // 此时 instance 是一个函数,并且可以调用instance()然后往里面传对象,比如instance({})。 
        // 此时 instance 不能 instance.get 这样使用。
        let instance = Axios.prototype.request.bind(context);
        // 为了可以 instance.get 这样用,将 Axios.prototype 对象中的方法添加到 instance 函数对象中
        // 为了保证函数在调用时 this 一定指向实例对象 context,更加严谨些,加个bind()
        Object.keys(Axios.prototype).forEach(key => {
            instance[key] = Axios.prototype[key].bind(context);
        });
        // 为 instance 这个函数对象添加 default 和 interceptors 属性
        Object.keys(context).forEach(key => {
            instance[key] = context[key];
        });
        return instance;
    }

    let axios = createInstance();
    // 发送请求
    // axios({method:'POST'});
    axios.get({});
    axios.post({});
</script>

三、模拟实现 axios 发送请求的过程

  axios是由Axios.prototype.request这个函数通过bind创建而来的,所以无论是 axios()还是axios.get()axios.post()等方式,请求的源头都是request

  模拟实现 axios 发送请求的过程可以看下面代码以及代码注释。

<script>
    // axios 发送请求
    //1. 声明构造函数
    function Axios(config){
        this.config = config;
    }
    Axios.prototype.request = function(config){
        //发送请求
        //源码中在创建前做了合并处理等
        //创建一个 promise 对象,可以看到这个 promise 一定是成功的
        let promise = Promise.resolve(config);
        //声明一个数组
        let chains = [dispatchRequest, undefined];// 这里的 undefined 是一个占位
        //调用 then 方法指定回调
        //这里的 then 方法执行后会执行下面的 dispatchRequest 函数,函数的返回结果是由适配器 xhrAdapter 执行结果决定的。
        //成功时候,这里 result 结果值就是下面的 response
        let result = promise.then(chains[0], chains[1]);
        //返回 promise 的结果
        //这里的 result 就是 request 函数的执行结果,也就是 axios() 的执行结果
        return result;
    }

    //2. dispatchRequest 函数
    function dispatchRequest(config){
        // 可以在这里进行对请求数据进行初始化转化、合并一切其他头信息的配置项等处理(源码中进行了处理)
        //调用适配器发送请求
        return xhrAdapter(config).then(response => {
            //可以在这里对响应的结果进行转换处理(源码中进行了处理)
            //这里的 response 就是 上面的 result 成功时候的结果值。
            return response;
        }, error => {
            throw error;
        });
    }

    //3. adapter 适配器
    function xhrAdapter(config){
        console.log('xhrAdapter 函数执行');
        return new Promise((resolve, reject) => {
            //发送 AJAX 请求
            let xhr = new XMLHttpRequest();
            //初始化
            xhr.open(config.method, config.url);
            //发送
            xhr.send();
            //绑定事件
            xhr.onreadystatechange = function(){
                if(xhr.readyState === 4){
                    //判断成功的条件
                    if(xhr.status >= 200 && xhr.status < 300){
                        //成功的状态
                        resolve({
                            //配置对象
                            config: config,
                            //响应体
                            data: xhr.response,
                            //响应头
                            headers: xhr.getAllResponseHeaders(), //是一个字符串,源码中是做了格式化,用 parseHeaders 将头信息解析成对象
                            // xhr 请求对象
                            request: xhr,
                            //响应状态码
                            status: xhr.status,
                            //响应状态字符串
                            statusText: xhr.statusText
                        });
                    }else{
                        //失败的状态
                        reject(new Error('请求失败,失败的状态码为' + xhr.status));
                    }
                }
            }
        });
    }


    //4. 创建 axios 函数
    let axios = Axios.prototype.request.bind(null);
    //这里就是最上面 request 函数的执行结果 result
    //然后执行.then 操作
    axios({
        method:'GET',
        url:'http://localhost:3000/posts'
    }).then(response => {
        console.log(response);
    });
</script>

四、模拟实现 axios 拦截器功能

  在我之前的博客一文掌握 axios 基础中提到过一个拦截器顺序的示例,当时执行结果是先执行2号请求拦截器,后执行1号请求拦截器,当时有解释原因。

  原因如下:

  promise在遍历执行时,使用的是数组的shift方法每次从中取出两个函数(成功回调,失败回调)执行。

  而在遍历执行前,我们要先将拦截器的请求回调和响应回调都压入一个数组中,之后再进行遍历运行。

  在向数组中添加 请求拦截器函数 时,根据axios请求的执行顺序,请求拦截器 应该在发送请求之前执行,所以应该添加在 发送请求函数 的前面,因此使用的是数组的unshift方法,即头部添加,故后面添加的 请求拦截器 总是放在头部。

  故 请求拦截器2 先 请求拦截器1 后。

  那么源码中 响应拦截器是使用的什么方法呢?它使用的是数组的push方法。

  在向数组中添加 请求拦截器函数 时,根据axios请求的执行顺序,响应拦截器 应该在发送请求之后执行,所以应该添加在 发送请求函数 的后面,因此使用的是数组的push方法,即尾部添加,故后面添加的 响应拦截器 总是放在尾部。

  故 响应拦截器1 先 响应拦截器2 后。

  现在来模拟实现 axios 拦截器功能,具体可以看下面代码以及代码注释。

  其中创建axios对象和发送请求可以看上面的代码,这里有些简略,只是搭了个框架。

<script>
    //1.构造函数
    function Axios(config){
        this.config = config;
        this.interceptors = {
            request: new InterceptorManager(),
            response: new InterceptorManager(),
        }
    }
    //3.发送请求  难点与重点
    Axios.prototype.request = function(config){
        //源码中在创建前做了合并处理等
        //创建一个 promise 对象
        let promise = Promise.resolve(config);
        //创建一个数组
        const chains = [dispatchRequest, undefined];// 这里的 undefined 是一个占位
        //处理拦截器
        //请求拦截器 
        //将请求拦截器的回调 压入到 chains 的前面
        this.interceptors.request.handlers.forEach(item => {
            chains.unshift(item.fulfilled, item.rejected);
        });
        //响应拦截器
        this.interceptors.response.handlers.forEach(item => {
            chains.push(item.fulfilled, item.rejected);
        });

        // console.log(chains);
        //promise 遍历执行,使用数组的 shift 方法每次从中取出两个函数(成功回调,失败回调)执行
        while(chains.length > 0){
            promise = promise.then(chains.shift(), chains.shift());
        }

        return promise;
    }

    //发送请求
    function dispatchRequest(config){
        //返回一个promise
        return new Promise((resolve, reject) => {
            resolve({
                status: 200,
                statusText: 'OK'
            });
        });
    }

    //2.拦截器管理器构造函数
    function InterceptorManager(){
        this.handlers = [];
    }
    //一旦调用use,就把成功和失败的回调函数做成一个对象压入到 handlers 中
    InterceptorManager.prototype.use = function(fulfilled, rejected){
        this.handlers.push({
            fulfilled,
            rejected
        })
    }

    //4.创建实例
    let context = new Axios({});
    //创建axios函数
    let axios = Axios.prototype.request.bind(context);
    //为 axios 这个函数对象添加 context 的 default 与 interceptors 属性
    Object.keys(context).forEach(key => {
        axios[key] = context[key];
    });


    //5.以下为功能测试代码
    // 设置请求拦截器  config 配置对象
    axios.interceptors.request.use(function one(config) {
        console.log('请求拦截器 成功 - 1号');
        return config;
    }, function one(error) {
        console.log('请求拦截器 失败 - 1号');
        return Promise.reject(error);
    });

    axios.interceptors.request.use(function two(config) {
        console.log('请求拦截器 成功 - 2号');
        return config;
    }, function two(error) {
        console.log('请求拦截器 失败 - 2号');
        return Promise.reject(error);
    });

    // 设置响应拦截器
    axios.interceptors.response.use(function (response) {
        console.log('响应拦截器 成功 1号');
        return response;
    }, function (error) {
        console.log('响应拦截器 失败 1号')
        return Promise.reject(error);
    });

    axios.interceptors.response.use(function (response) {
        console.log('响应拦截器 成功 2号')
        return response;
    }, function (error) {
        console.log('响应拦截器 失败 2号')
        return Promise.reject(error);
    });


    //6.发送请求
    axios({
        method: 'GET',
        url: 'http://localhost:3000/posts'
    }).then(response => {
        console.log(response);
    });
</script>

五、模拟实现 axios 取消请求功能

  现在来模拟实现 axios 取消请求功能,具体可以看下面代码以及代码注释。

  其中创建axios对象和发送请求可以看上面的代码,这里有些简略,只是搭了个框架。

<body>
    <div class="container">
        <h2 class="page-header">axios取消请求</h2>
        <button class="btn btn-primary"> 发送请求 </button>
    </div>
    <script>
        //构造函数
        function Axios(config){
            this.config = config;
        }
        //原型 request 方法
        Axios.prototype.request = function(config){
            return dispatchRequest(config);
        }
        //dispatchRequest 函数
        function dispatchRequest(config){
            return xhrAdapter(config); 
        }
        //xhrAdapter
        function xhrAdapter(config){
            //发送 AJAX 请求
            return new Promise((resolve, reject) => {
                //实例化对象
                const xhr = new XMLHttpRequest();
                //初始化
                xhr.open(config.method, config.url);
                //发送
                xhr.send();
                //处理结果
                xhr.onreadystatechange = function(){
                    if(xhr.readyState === 4){
                        //判断结果
                        if(xhr.status >= 200 && xhr.status < 300){
                            //设置为成功的状态
                            resolve({
                                status: xhr.status,
                                statusText: xhr.statusText
                            });
                        }else{
                            reject(new Error('请求失败'));
                        }
                    }
                }
                //关于取消请求的处理
                if(config.cancelToken){
                    //对 cancelToken 对象身上的 promise 对象指定成功的回调
                    config.cancelToken.promise.then(value => {
                        // 取消请求
                        xhr.abort();
                        //将整体结果设置为失败
                        reject(new Error('请求已经被取消'))
                    });
                }
            })
        }

        // 创建 axios 函数
        const context = new Axios({});
        const axios = Axios.prototype.request.bind(context);

        // CancelToken 构造函数
        function CancelToken(executor){
            // 声明一个变量
            var resolvePromise;
            // 为实例对象添加属性,这个 promise 属性的值是一个 Promise 对象,并且初始是 pedding 状态
            this.promise = new Promise((resolve) => {
                // 将 resolve 赋值给 resolvePromise
                // 这样执行 resolvePromise 函数时,就能更改 promise 原状态
                //这是因为,resolve 和 resolvePromise 都是引用类型,指向的是同一个内存地址,resolvePromise执行,则 resolve 也执行
                resolvePromise = resolve
            });
            // 调用 executor 函数
            executor(function(){
                // 执行 resolvePromise 函数
                resolvePromise();
            });
        }

        // 获取按钮 以上为模拟实现的代码
        const btns = document.querySelectorAll('button');
        //声明全局变量
        let cancel = null;
        //发送请求
        btns[0].onclick = function(){
            //检测上一次的请求是否已经完成
            if(cancel !== null){
                //取消上一次的请求
                cancel();
            }

            //创建 cancelToken 的值
            let cancelToken = new CancelToken(function(c){
                // 此时的 function(c){} 就是上面的CancelToken构造函数里的 executor,
                // 所以此时的 c 就是上面的CancelToken构造函数里的 function(){resolvePromise()}
                // 所以这里的 cancel 其实就是 function(){resolvePromise()}
                // cancel 一旦执行,resolvePromise() 就会执行,CancelToken构造函数里的 promise 属性的状态就会改变
                cancel = c;
            });

            axios({
                method: 'GET',
                url: 'http://localhost:3000/posts',
                //添加配置对象的属性
                cancelToken: cancelToken
            }).then(response => {
                console.log(response);
                //将 cancel 的值初始化
                cancel = null;
            })
        }
    </script>   
</body>

六、axios运行的整体流程

  整体流程:request(config)=> dispatchRequest(config) => xhrAdapter(config)

  • request(config):将请求拦截器 / dispatchRequest()/ 响应拦截器 通过 promise链串连起来,返回 promise
  • dispatchRequest(config):转换请求数据 => 调用xhrAdapter()发请求 => 请求返回后转换响应数据,返回promise
  1. xhrAdapter(config):创建 XHR对象,根据 config进行相应设置,发送特定请求,并接收响应数据,返回 promise

axios源码模拟实现_第1张图片


  至此,axios源码模拟实现就结束了,我是看了尚硅谷的视频进行学习的,感觉模拟实现的代码中核心思想还是有的,不过想要更深入了解最好还是从头认认真真的研究一下源码。

你可能感兴趣的:(前端小白,前端,axios)