最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 手写 Axios 核心代码

    正文概述 掘金(追公交车的小仙女)   2021-03-08   572

    Axios 毋庸多说大家在前端开发中常用的一个发送 HTTP 请求的库,用过的都知道。本文用来整理项目中常用的 Axios 的封装使用。同时学习源码,手写实现 Axios 的核心代码。如果有错误或者不严谨的地方,烦请给予指正,十分感谢。如果喜欢或者有所启发,欢迎点赞,对作者也是一种鼓励 ❤️

    Axios 常用封装

    是什么

    Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。它的特性:

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

    官网地址: www.axios-js.com/zh-cn/docs/…

    Axios 使用方式有两种:一种是直接使用全局的 Axios 对象;另外一种是通过 axios.create(config) 方法创建一个实例对象,使用该对象。两种方式的区别是通过第二种方式创建的实例对象更清爽一些;全局的 Axios 对象其实也是创建的实例对象导出的,它本身上加载了很多默认属性。后面源码学习的时候会再详细说明。

    请求

    Axios 这个 HTTP 的库比较灵活,给用户多种发送请求的方式,以至于有些混乱。细心整理会发现,全局的 Axios(或者 axios.create(config)创建的对象) 既可以当作对象使用,也可以当作函数使用:

    // axios 当作对象使用
    axios.request(config)
    axios.get(url[, config])
    axios.post(url[, data[, config]])
    
    // axios() 当作函数使用。 发送 POST 请求
    axios({
      method: 'post',
      url: '/user/12345',
      data: {
        firstName: 'Fred',
        lastName: 'Flintstone'
      }
    });
    

    后面源码学习的时候会再详细说明为什么 Axios 可以实现两种方式的使用。

    取消请求

    可以使用 CancelToken.source 工厂方法创建 cancel token:

    const CancelToken = axios.CancelToken;
    const source = CancelToken.source();
    
    axios.get('/user/12345', {
      cancelToken: source.token
    }).catch(function(thrown) {
      if (axios.isCancel(thrown)) {
        console.log('Request canceled', thrown.message);
      } else {
         // 处理错误
      }
    });
    
    // 取消请求(message 参数是可选的)
    source.cancel('Operation canceled by the user.');
    

    source 有两个属性:一个是 source.token 标识请求;另一个是 source.cancel() 方法,该方法调用后,可以让 CancelToken 实例的 promise 状态变为 resolved,从而触发 xhr 对象的 abort() 方法,取消请求。

    拦截

    Axios 还有一个奇妙的功能点,可以在发送请求前对请求进行拦截,对相应结果进行拦截。结合业务场景的话,在中台系统中完成登录后,获取到后端返回的 token,可以将 token 添加到 header 中,以后所有的请求自然都会加上这个自定义 header。

    //拦截1 请求拦截
    instance.interceptors.request.use(function(config){
        //在发送请求之前做些什么
        const token = sessionStorage.getItem('token');
        if(token){
            const newConfig = {
                ...config,
                headers: {
                    token: token
                }
            }
            return newConfig;
        }else{
            return config;
        }
    }, function(error){
        //对请求错误做些什么
        return Promise.reject(error);
    });
    

    我们还可以利用请求拦截功能实现 取消重复请求,也就是在前一个请求还没有返回之前,用户重新发送了请求,需要先取消前一次请求,再发送新的请求。比如搜索框自动查询,当用户修改了内容重新发送请求的时候需要取消前一次请求,避免请求和响应混乱。再比如表单提交按钮,用户多次点击提交按钮,那么我们就需要取消掉之前的请求,保证只有一次请求的发送和响应。

    实现原理是使用一个对象记录已经发出去的请求,在请求拦截函数中先判断这个对象中是否记录了本次请求信息,如果已经存在,则取消之前的请求,将本次请求添加进去对象中;如果没有记录过本次请求,则将本次请求信息添加进对象中。最后请求完成后,在响应拦截函数中执行删除本次请求信息的逻辑。

    // 拦截2   重复请求,取消前一个请求
    const promiseArr = {};
    instance.interceptors.request.use(function(config){
        console.log(Object.keys(promiseArr).length)
        //在发送请求之前做些什么
        let source=null;
        if(config.cancelToken){
            // config 配置中带了 source 信息
            source = config.source;
        }else{
            const CancelToken = axios.CancelToken;
            source = CancelToken.source();
            config.cancelToken = source.token;
        }
        const currentKey = getRequestSymbol(config);
        if(promiseArr[currentKey]){
            const tmp = promiseArr[currentKey];
            tmp.cancel("取消前一个请求");
            delete promiseArr[currentKey];
            promiseArr[currentKey] = source;
        }else{
            promiseArr[currentKey] = source;
        }
        return config;
    
    }, function(error){
        //对请求错误做些什么
        return Promise.reject(error);
    });
    // 根据 url、method、params 生成唯一标识,大家可以自定义自己的生成规则
    function getRequestSymbol(config){
        const arr = [];
        if(config.params){
            const data = config.params;
            for(let key of Object.keys(data)){
                arr.push(key+"&"+data[key]);
            }
            arr.sort();
        }
        return config.url+config.method+arr.join("");
    }
    
    instance.interceptors.response.use(function(response){
        const currentKey = getRequestSymbol(response.config);
        delete promiseArr[currentKey];
        return response;
    }, function(error){
        //对请求错误做些什么
        return Promise.reject(error);
    });
    

    最后,我们可以在响应拦截函数中统一处理返回码的逻辑:

    // 响应拦截
    instance.interceptors.response.use(function(response){
        // 401 没有登录跳转到登录页面
        if(response.data.code===401){
            window.location.href = "http://127.0.0.1:8080/#/login";
        }else if(response.data.code===403){
            // 403 无权限跳转到无权限页面
            window.location.href = "http://127.0.0.1:8080/#/noAuth";
        }
        return response;
    }, function(error){
        //对请求错误做些什么
        return Promise.reject(error);
    })
    

    文件下载

    通常文件下载有两种方式:一种是通过文件在服务器上的对外地址直接下载;还有一种是通过接口将文件以二进制流的形式下载。

    第一种:同域名 下使用 a 标签下载:

    // httpServer.js
    const express = require("express");
    const path = require('path');
    const app = express();
    
    //静态文件地址
    app.use(express.static(path.join(__dirname, 'public')))
    app.use(express.static(path.join(__dirname, '../')));
    app.listen(8081, () => {
      console.log("服务器启动成功!")
    });
    
    // index.html
    <a href="test.txt" download="test.txt">下载</a>
    

    手写 Axios 核心代码

    第二种:二进制文件流的形式传递,我们直接访问该接口并不能下载文件,一定程度保证了数据的安全性。比较多的场景是:后端接收到查询参数,查询数据库然后通过插件动态生成 excel 文件,以文件流的方式让前端下载。

    这时候,我们可以将请求文件下载的逻辑进行封装。将二进制文件流存在 Blob 对象中,再将其转为 url 对象,最后通过 a 标签下载。

    //封装下载
    export function downLoadFetch(url, params = {}, config={}) {
        //取消
        const downSource = axios.CancelToken.source();
        document.getElementById('downAnimate').style.display = 'block';
        document.getElementById('cancelBtn').addEventListener('click', function(){
            downSource.cancel("用户取消下载");
            document.getElementById('downAnimate').style.display = 'none';
        }, false);
        //参数
        config.params = params;
        //超时时间
        config.timeout = config.timeout ? config.timeout : defaultDownConfig.timeout;
        //类型
        config.responseType = defaultDownConfig.responseType;
        //取消下载
        config.cancelToken = downSource.token;
        return instance.get(url, config).then(response=>{
            const content = response.data;
            const url = window.URL.createObjectURL(new Blob([content]));
            //创建 a 标签
            const link = document.createElement('a');
            link.style.display = 'none';
            link.href = url;
            //文件名  Content-Disposition: attachment; filename=download.txt
            const filename = response.headers['content-disposition'].split(";")[1].split("=")[1];
            link.download = filename;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            return {
                status: 200,
                success: true
            }
        })
    }
    

    手写 Axios 核心代码

    juejin.cn/post/687891…

    手写 Axios 核心代码

    写了这么多用法终于到正题了,手写 Axios 核心代码。Axios 这个库源码不难阅读,没有特别复杂的逻辑,大家可以放心阅读 ? 。

    源码入口是这样查找:在项目 node_modules 目录下,找到 axios 模块的 package.json 文件,其中 "main": "index.js", 就是文件入口。一步步我们可以看到源码是怎么串起来的。

    手写 Axios 核心代码

    模仿上面的目录结构,我们创建自己的目录结构:

    axios-js
    │  index.html
    │  
    └─lib
            adapter.js
            Axios.js
            axiosInstance.js
            CancelToken.js
            InterceptorManager.js
    

    Axios 是什么

    上面有提到我们使用的全局 Axios 对象其实也是构造出来的 axios,既可以当对象使用调用 get、post 等方法,也可以直接当作函数使用。这是因为全局的 Axios 其实是函数对象 instance 。源码位置在 axios/lib/axios.js 中。具体代码如下:

    // axios/lib/axios.js
    //创建 axios 实例
    function createInstance(defaultConfig) {
      var context = new Axios(defaultConfig);
      //instance 对象是 bind 返回的函数
      var instance = bind(Axios.prototype.request, context);
      // Copy axios.prototype to instance
      utils.extend(instance, Axios.prototype, context);
      // Copy context to instance
      utils.extend(instance, context);
      return instance;
    }
    
    // 实例一个 axios
    var axios = createInstance(defaults);
    
    // 向这个实例添加 Axios 属性
    axios.Axios = Axios;
    
    // 向这个实例添加 create 方法
    axios.create = function create(instanceConfig) {
      return createInstance(mergeConfig(axios.defaults, instanceConfig));
    };
    // 向这个实例添加 CancelToken 方法
    axios.CancelToken = require('./cancel/CancelToken');
    // 导出实例 axios
    module.exports.default = axios;
    

    根据上面的源码,我们可以简写一下自己实现 Axios.js 和 axiosInstance.js:

    // Axios.js
    //Axios 主体
    function Axios(config){
    }
    
    // 核心方法,发送请求
    Axios.prototype.request = function(config){
    }
    
    Axios.prototype.get = function(url, config={}){
        return this.request({url: url, method: 'GET', ...config});
    }
    
    Axios.prototype.post = function(url, data, config={}){
        return this.request({url: url, method: 'POST', data: data, ...config})
    }
    export default Axios;
    

    在 axiosInstance.js 文件中,实例化一个 Axios 得到 context,再将原型对象上的方法绑定到 instance 对象上,同时将 context 的属性添加到 instance 上。这样 instance 就成为了一个函数对象。既可以当作对象使用,也可以当作函数使用。

    // axiosInstance.js
    //创建实例
    function createInstance(config){
        const context = new Axios(config);
        var instance = Axios.prototype.request.bind(context);
        //将 Axios.prototype 属性扩展到 instance 上
        for(let k of Object.keys(Axios.prototype)){
            instance[k] = Axios.prototype[k].bind(context);
        }
        //将 context 属性扩展到 instance 上
        for(let k of Object.keys(context)){
            instance[k] = context[k]
        }
        return instance;
    }
    
    const axios = createInstance({});
    axios.create = function(config){
        return createInstance(config);
    }
    export default axios;
    

    也就是说 axios.js 中导出的 axios 对象并不是 new Axios() 方法返回的对象 context,而是 Axios.prototype.request.bind(context) 执行返回的 instance ,通过遍历 Axios.prototype 并改变其 this 指向到 context;遍历 context 对象让 instance 对象具有 context 的所有属性。这样 instance 对象就无敌了,? 既拥有了 Axios.prototype 上的所有方法,又具有了 context 的所有属性。

    请求实现

    我们知道 Axios 在浏览器中会创建 XMLHttpRequest 对象,在 node.js 环境中创建 http 发送请求。Axios.prototype.request() 是发送请求的核心方法,这个方法其实调用的是 dispatchRequest 方法,而 dispatchRequest 方法调用的是 config.adapter || defaults.adapter 也就是自定义的 adapter 或者默认的 defaults.adapter,默认defaults.adapter 调用的是 getDefaultAdapter 方法,源码:

    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;
    }
    

    哈哈哈,getDefaultAdapter 方法最终根据当前的环境返回不同的实现方法,这里用到了 适配器模式。我们只用实现 xhr 发送请求即可:

    //适配器 adapter.js
    function getDefaultAdapter(){
        var adapter;
        if(typeof XMLHttpRequest !== 'undefined'){
            //导入 XHR 对象请求
            adapter = (config)=>{
                return xhrAdapter(config);
            }
        }
        return adapter;
    }
    function xhrAdapter(config){
        return new Promise((resolve, reject)=>{
            var xhr = new XMLHttpRequest();
            xhr.open(config.method, config.url, true);
            xhr.send();
            xhr.onreadystatechange = ()=>{
                if(xhr.readyState===4){
                    if(xhr.status>=200&&xhr.status<300){
                        resolve({
                            data: {},
                            status: xhr.status,
                            statusText: xhr.statusText,
                            xhr: xhr
                        })
                    }else{
                        reject({
                            status: xhr.status
                        })
                    }
                }
            };
        })
    }
    export default getDefaultAdapter;
    

    这样就理顺了,getDefaultAdapter 方法每次执行会返回一个 Promise 对象,这样 Axios.prototype.request 方法可以得到执行 xhr 发送请求的 Promise 对象。

    给我们的 Axios.js 添加发送请求的方法:

    //Axios.js
    import getDefaultAdapter from './adapter.js';
    Axios.prototype.request = function(config){
        const adapter = getDefaultAdapter(config);
        var promise = Promise.resolve(config);
        var chain = [adapter, undefined];
        while(chain.length){
            promise = promise.then(chain.shift(), chain.shift());
        }
        return promise;
    }
    

    拦截器实现

    拦截器的原理在于 Axios.prototype.request 方法中的 chain 数组,把请求拦截函数添加到 chain 数组前面,把响应拦截函数添加到数组后面。这样就可以实现发送前拦截和响应后拦截的效果。 手写 Axios 核心代码 创建 InterceptorManager.js

    //InterceptorManager.js 
    //拦截器
    function InterceptorManager(){
        this.handlers = [];
    }
    InterceptorManager.prototype.use = function(fulfilled, rejected){
        this.handlers.push({
            fulfilled: fulfilled,
            rejected: rejected
        });
        return this.handlers.length -1;
    }
    
    export default InterceptorManager;
    

    在 Axios.js 文件中,构造函数有 interceptors 属性:

    //Axios.js
    function Axios(config){
        this.interceptors = {
            request: new InterceptorManager(),
            response: new InterceptorManager()
        }
    }
    

    这样我们在 Axios.prototype.request 方法中对拦截器添加处理:

    //Axios.js
    Axios.prototype.request = function(config){
        const adapter = getDefaultAdapter(config);
        var promise = Promise.resolve(config);
        var chain = [adapter, undefined];
        //请求拦截
        this.interceptors.request.handlers.forEach(item=>{
            chain.unshift(item.rejected);
            chain.unshift(item.fulfilled);
            
        });
        //响应拦截
        this.interceptors.response.handlers.forEach(item=>{
            chain.push(item.fulfilled);
            chain.push(item.rejected)
        });
        console.dir(chain);
        while(chain.length){
            promise = promise.then(chain.shift(), chain.shift());
        }
        return promise;
    }
    

    所以拦截器的执行顺序是:请求拦截2 -> 请求拦截1 -> 发送请求 -> 响应拦截1 -> 响应拦截2

    取消请求

    来到 Axios 最精彩的部分了,取消请求。我们知道 xhr 的 xhr.abort(); 函数可以取消请求。那么什么时候执行这个取消请求的操作呢?得有一个信号告诉 xhr 对象什么时候执行取消操作。取消请求就是未来某个时候要做的事情,你能想到什么呢?对,就是 Promise。Promise 的 then 方法只有 Promise 对象的状态变为 resolved 的时候才会执行。我们可以利用这个特点,在 Promise 对象的 then 方法中执行取消请求的操作。看代码:

    //CancelToken.js
    // 取消请求
    function CancelToken(executor){
        if(typeof executor !== 'function'){
            throw new TypeError('executor must be a function.')
        }
        var resolvePromise;
        this.promise = new Promise((resolve)=>{
            resolvePromise = resolve;
        });
        executor(resolvePromise)
    }
    CancelToken.source = function(){
        var cancel;
        var token = new CancelToken((c)=>{
            cancel = c;
        })
        return {
            token,
            cancel
        };
    }
    export default CancelToken;
    

    当我们执行 const source = CancelToken.source()的时候,source 对象有两个字段,一个是 token 对象,另一个是 cancel 函数。在 xhr 请求中:

    //适配器
    // adapter.js
    function xhrAdapter(config){
        return new Promise((resolve, reject)=>{
            ...
            //取消请求
            if(config.cancelToken){
                // 只有 resolved 的时候才会执行取消操作
                config.cancelToken.promise.then(function onCanceled(cancel){
                    if(!xhr){
                        return;
                    }
                    xhr.abort();
                    reject("请求已取消");
                    // clean up xhr
                    xhr = null;
                })
            }
        })
    }
    

    CancelToken 的构造函数中需要传入一个函数,而这个函数的作用其实是为了将能控制内部 Promise 的 resolve 函数暴露出去,暴露给 source 的 cancel 函数。这样内部的 Promise 状态就可以通过 source.cancel() 方法来控制啦,秒啊~ ?

    node 后端接口

    node 后端简单的接口代码:

    const express = require("express");
    const bodyParser = require('body-parser');
    const app = express();
    const router = express.Router();
    //文件下载
    const fs = require("fs");
    // get 请求
    router.get("/getCount", (req, res)=>{
      setTimeout(()=>{
        res.json({
          success: true,
          code: 200,
          data: 100
        })
      }, 1000)
    })
    
    
    // 二进制文件流
    router.get('/downFile', (req, res, next) => {
      var name = 'download.txt';
      var path = './' + name;
      var size = fs.statSync(path).size;
      var f = fs.createReadStream(path);
      res.writeHead(200, {
        'Content-Type': 'application/force-download',
        'Content-Disposition': 'attachment; filename=' + name,
        'Content-Length': size
      });
      f.pipe(res);
    })
    
    // 设置跨域访问
    app.all("*", function (request, response, next) {
      // 设置跨域的域名,* 代表允许任意域名跨域;http://localhost:8080 表示前端请求的 Origin 地址
      response.header("Access-Control-Allow-Origin", "http://127.0.0.1:5500");
      //设置请求头 header 可以加那些属性
      response.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With');
      //暴露给 axios https://blog.csdn.net/w345731923/article/details/114067074
      response.header("Access-Control-Expose-Headers", "Content-Disposition");
      // 设置跨域可以携带 Cookie 信息
      response.header('Access-Control-Allow-Credentials', "true");
      //设置请求头哪些方法是合法的
      response.header(
        "Access-Control-Allow-Methods",
        "PUT,POST,GET,DELETE,OPTIONS"
      );
      response.header("Content-Type", "application/json;charset=utf-8");
      next();
    });
    
    // 接口数据解析
    app.use(bodyParser.json())
    app.use(bodyParser.urlencoded({
      extended: false
    }))
    app.use('/api', router) // 路由注册
    
    app.listen(8081, () => {
      console.log("服务器启动成功!")
    });
    

    git 地址

    如果大家能够跟着源码敲一遍,相信一定会有很多收获。

    手写 Axios 核心代码 github 地址: github.com/YY88Xu/axio…
    Axios 封装: github.com/YY88Xu/vue2…


    起源地下载网 » 手写 Axios 核心代码

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元