Axios 是如何封装 HTTP 请求的

开发 前端
本文用来整理项目中常用的 Axios 的封装使用。同时学习源码,手写实现 Axios 的核心代码。

Axios 毋庸多说大家在前端开发中常用的一个发送 HTTP 请求的库,用过的都知道。本文用来整理项目中常用的 Axios 的封装使用。同时学习源码,手写实现 Axios 的核心代码。

Axios 常用封装

是什么

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

  •  从浏览器中创建 XMLHttpRequests
  •  从 node.js 创建 http 请求
  •  支持 Promise API
  •  拦截请求和响应
  •  转换请求数据和响应数据
  •  取消请求
  •  自动转换 JSON 数据
  •  客户端支持防御 XSRF官网地址:http://www.axios-js.com/zh-cn/docs/#axios-config

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

请求

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

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

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

取消请求

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

  1. const CancelToken = axios.CancelToken;  
  2. const source = CancelToken.source();  
  3. axios.get('/user/12345', {  
  4.   cancelToken: source.token  
  5. }).catch(function(thrown) {  
  6.   if (axios.isCancel(thrown)) {  
  7.     console.log('Request canceled', thrown.message);  
  8.   } else { 
  9.       // 处理错误  
  10.   }  
  11. });  
  12. // 取消请求(message 参数是可选的)  
  13. source.cancel('Operation canceled by the user.'); 

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

拦截

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

  1. //拦截1 请求拦截  
  2. instance.interceptors.request.use(function(config){  
  3.     //在发送请求之前做些什么  
  4.     const token = sessionStorage.getItem('token');  
  5.     if(token){  
  6.         const newConfig = {  
  7.             ...config,  
  8.             headers: {  
  9.                 token: token  
  10.             }  
  11.         }  
  12.         return newConfig;  
  13.     }else{  
  14.         return config;  
  15.     }  
  16. }, function(error){  
  17.     //对请求错误做些什么  
  18.     return Promise.reject(error);  
  19. }); 

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

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

  1. // 拦截2   重复请求,取消前一个请求  
  2. const promiseArr = {};  
  3. instance.interceptors.request.use(function(config){  
  4.     console.log(Object.keys(promiseArr).length)  
  5.     //在发送请求之前做些什么  
  6.     let source=null 
  7.     if(config.cancelToken){  
  8.         // config 配置中带了 source 信息  
  9.         source = config.source;  
  10.     }else{  
  11.         const CancelToken = axios.CancelToken;  
  12.         source = CancelToken.source();  
  13.         config.cancelToken = source.token;  
  14.     }  
  15.     const currentKey = getRequestSymbol(config);  
  16.     if(promiseArr[currentKey]){  
  17.         const tmp = promiseArr[currentKey];  
  18.         tmp.cancel("取消前一个请求");  
  19.         delete promiseArr[currentKey];  
  20.         promiseArr[currentKey] = source;  
  21.     }else{  
  22.         promiseArr[currentKey] = source;  
  23.     }  
  24.     return config;  
  25. }, function(error){  
  26.     //对请求错误做些什么  
  27.     return Promise.reject(error);  
  28. });  
  29. // 根据 url、method、params 生成唯一标识,大家可以自定义自己的生成规则  
  30. function getRequestSymbol(config){  
  31.     const arr = [];  
  32.     if(config.params){  
  33.         const data = config.params;  
  34.         for(let key of Object.keys(data)){  
  35.             arr.push(key+"&"+data[key]);  
  36.         }  
  37.         arr.sort();  
  38.     }  
  39.     return config.url+config.method+arr.join("");  
  40.  
  41. instance.interceptors.response.use(function(response){  
  42.     const currentKey = getRequestSymbol(response.config);  
  43.     delete promiseArr[currentKey];  
  44.     return response;  
  45. }, function(error){  
  46.     //对请求错误做些什么  
  47.     return Promise.reject(error);  
  48. }); 

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

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

文件下载

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

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

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

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

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

  1. //封装下载  
  2. export function downLoadFetch(url, params = {}, config={}) {  
  3.     //取消  
  4.     const downSource = axios.CancelToken.source();  
  5.     document.getElementById('downAnimate').style.display = 'block' 
  6.     document.getElementById('cancelBtn').addEventListener('click', function(){  
  7.         downSource.cancel("用户取消下载");  
  8.         document.getElementById('downAnimate').style.display = 'none' 
  9.     }, false);  
  10.     //参数  
  11.     config.params = params;  
  12.     //超时时间  
  13.     configconfig.timeout = config.timeout ? config.timeout : defaultDownConfig.timeout;  
  14.     //类型  
  15.     config.responseType = defaultDownConfig.responseType;  
  16.     //取消下载  
  17.     config.cancelToken = downSource.token;  
  18.     return instance.get(url, config).then(response=> 
  19.         const content = response.data;  
  20.         const url = window.URL.createObjectURL(new Blob([content]));  
  21.         //创建 a 标签  
  22.         const link = document.createElement('a');  
  23.         link.style.display = 'none' 
  24.         link.href = url 
  25.         //文件名  Content-Disposition: attachment; filename=download.txt  
  26.         const filename = response.headers['content-disposition'].split(";")[1].split("=")[1];  
  27.         link.download = filename 
  28.         document.body.appendChild(link);  
  29.         link.click();  
  30.         document.body.removeChild(link);  
  31.         return {  
  32.             status: 200,  
  33.             success: true  
  34.         }  
  35.     })  

手写 Axios 核心代码

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

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

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

  1. axios-js  
  2. │  index.html  
  3. │    
  4. └─lib  
  5.         adapter.js  
  6.         Axios.js  
  7.         axiosInstance.js  
  8.         CancelToken.js  
  9.         InterceptorManager.js 

Axios 是什么

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

  1. // axios/lib/axios.js  
  2. //创建 axios 实例  
  3. function createInstance(defaultConfig) {  
  4.   var context = new Axios(defaultConfig);  
  5.   //instance 对象是 bind 返回的函数  
  6.   var instance = bind(Axios.prototype.request, context);  
  7.   // Copy axios.prototype to instance  
  8.   utils.extend(instance, Axios.prototype, context);  
  9.   // Copy context to instance  
  10.   utils.extend(instance, context);  
  11.   return instance;  
  12.  
  13. // 实例一个 axios  
  14. var axios = createInstance(defaults);  
  15. // 向这个实例添加 Axios 属性  
  16. axios.Axios = Axios;  
  17. // 向这个实例添加 create 方法  
  18. axios.create = function create(instanceConfig) {  
  19.   return createInstance(mergeConfig(axios.defaults, instanceConfig));  
  20. };  
  21. // 向这个实例添加 CancelToken 方法  
  22. axios.CancelToken = require('./cancel/CancelToken'); 
  23. // 导出实例 axios  
  24. module.exports.default = axios

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

  1. // Axios.js  
  2. //Axios 主体  
  3. function Axios(config){ 
  4.  
  5. // 核心方法,发送请求  
  6. Axios.prototype.request = function(config){  
  7.  
  8. Axios.prototype.get = function(url, config={}){  
  9.     return this.request({url: url, method: 'GET', ...config});  
  10.  
  11. Axios.prototype.post = function(url, data, config={}){  
  12.     return this.request({url: url, method: 'POST', data: data, ...config})  
  13.  
  14. export default Axios; 

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

  1. // axiosInstance.js  
  2. //创建实例  
  3. function createInstance(config){  
  4.     const context = new Axios(config);  
  5.     var instance = Axios.prototype.request.bind(context);  
  6.     //将 Axios.prototype 属性扩展到 instance 上  
  7.     for(let k of Object.keys(Axios.prototype)){  
  8.         instance[k] = Axios.prototype[k].bind(context);  
  9.     }  
  10.     //将 context 属性扩展到 instance 上  
  11.     for(let k of Object.keys(context)){  
  12.         instance[k] = context[k]  
  13.     }  
  14.     return instance;  
  15.  
  16. const axios = createInstance({});  
  17. axios.create = function(config){  
  18.     return createInstance(config);  
  19.  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 方法,源码: 

  1. function getDefaultAdapter() {  
  2.   var adapter;  
  3.   if (typeof XMLHttpRequest !== 'undefined') {  
  4.     // For browsers use XHR adapter  
  5.     adapter = require('./adapters/xhr');  
  6.   } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {  
  7.     // For node use HTTP adapter  
  8.     adapter = require('./adapters/http');  
  9.   }  
  10.   return adapter;  

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

  1. //适配器 adapter.js  
  2. function getDefaultAdapter(){  
  3.     var adapter;  
  4.     if(typeof XMLHttpRequest !== 'undefined'){  
  5.         //导入 XHR 对象请求  
  6.         adapter = (config)=> 
  7.             return xhrAdapter(config);  
  8.         }  
  9.     }  
  10.     return adapter;  
  11.  
  12. function xhrAdapter(config){  
  13.     return new Promise((resolve, reject)=> 
  14.         var xhr = new XMLHttpRequest();  
  15.         xhr.open(config.method, config.url, true);  
  16.         xhr.send();  
  17.         xhr.onreadystatechange = ()=> 
  18.             if(xhr.readyState===4){  
  19.                 if(xhr.status>=200&&xhr.status<300){  
  20.                     resolve({  
  21.                         data: {},  
  22.                         status: xhr.status,  
  23.                         statusText: xhr.statusText,  
  24.                         xhr: xhr 
  25.                     })  
  26.                 }else{  
  27.                     reject({ 
  28.                          status: xhr.status  
  29.                     })  
  30.                 }  
  31.             }  
  32.         };  
  33.     })  
  34.  export default getDefaultAdapter; 

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

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

  1. //Axios.js  
  2. import getDefaultAdapter from './adapter.js';  
  3. Axios.prototype.request = function(config){  
  4.     const adapter = getDefaultAdapter(config);  
  5.     var promise = Promise.resolve(config);  
  6.     var chain = [adapter, undefined];  
  7.     while(chain.length){  
  8.         promisepromise = promise.then(chain.shift(), chain.shift());  
  9.     }  
  10.     return promise;  

拦截器实现

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

创建 InterceptorManager.js 

  1. //InterceptorManager.js   
  2. //拦截器  
  3. function InterceptorManager(){  
  4.     this.handlers = [];  
  5.  
  6. InterceptorManager.prototype.use = function(fulfilled, rejected){  
  7.     this.handlers.push({  
  8.         fulfilled: fulfilled,  
  9.         rejected: rejected  
  10.     });  
  11.     return this.handlers.length -1;  
  12.  
  13. export default InterceptorManager; 

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

  1. //Axios.js  
  2. function Axios(config){  
  3.     this.interceptors = {  
  4.         request: new InterceptorManager(),  
  5.         response: new InterceptorManager()  
  6.     }  

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

  1. //Axios.js  
  2. Axios.prototype.request = function(config){  
  3.     const adapter = getDefaultAdapter(config);  
  4.     var promise = Promise.resolve(config);  
  5.     var chain = [adapter, undefined];  
  6.     //请求拦截  
  7.     this.interceptors.request.handlers.forEach(item=> 
  8.         chain.unshift(item.rejected);  
  9.         chain.unshift(item.fulfilled);      
  10.      });  
  11.     //响应拦截  
  12.     this.interceptors.response.handlers.forEach(item=> 
  13.         chain.push(item.fulfilled);  
  14.         chain.push(item.rejected)  
  15.     });  
  16.     console.dir(chain);  
  17.     while(chain.length){  
  18.         promisepromise = promise.then(chain.shift(), chain.shift());  
  19.     }  
  20.     return promise;  

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

取消请求

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

  1. //CancelToken.js  
  2. // 取消请求  
  3. function CancelToken(executor){  
  4.     if(typeof executor !== 'function'){  
  5.         throw new TypeError('executor must be a function.')  
  6.     }  
  7.     var resolvePromise;  
  8.     this.promise = new Promise((resolve)=> 
  9.         resolveresolvePromise = resolve;  
  10.     });  
  11.     executor(resolvePromise)  
  12.  
  13. CancelToken.source = function(){  
  14.     var cancel;  
  15.     var token = new CancelToken((c)=> 
  16.         ccancel = c;  
  17.     })  
  18.     return { 
  19.          token,  
  20.         cancel  
  21.     };  
  22.  
  23. export default CancelToken; 

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

  1. //适配器  
  2. // adapter.js  
  3. function xhrAdapter(config){  
  4.     return new Promise((resolve, reject)=> 
  5.         ...  
  6.         //取消请求  
  7.         if(config.cancelToken){  
  8.             // 只有 resolved 的时候才会执行取消操作  
  9.             config.cancelToken.promise.then(function onCanceled(cancel){  
  10.                 if(!xhr){  
  11.                     return;  
  12.                 }  
  13.                 xhr.abort();  
  14.                 reject("请求已取消"); 
  15.                  // clean up xhr  
  16.                 xhr = null 
  17.             })  
  18.         }  
  19.     })  

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

node 后端接口

node 后端简单的接口代码: 

  1. const express = require("express");  
  2. const bodyParser = require('body-parser');  
  3. const app = express();  
  4. const router = express.Router();  
  5. //文件下载  
  6. const fs = require("fs");  
  7. // get 请求  
  8. router.get("/getCount", (req, res)=> 
  9.   setTimeout(()=> 
  10.     res.json({  
  11.       success: true,  
  12.       code: 200,  
  13.       data: 100  
  14.     })  
  15.   }, 1000)  
  16. })  
  17. // 二进制文件流 
  18. router.get('/downFile', (req, res, next) => {  
  19.   var name = 'download.txt' 
  20.   var path = './' + name;  
  21.   var size = fs.statSync(path).size;  
  22.   var f = fs.createReadStream(path);  
  23.   res.writeHead(200, {  
  24.     'Content-Type': 'application/force-download',  
  25.     'Content-Disposition': 'attachment; filename=' + name,  
  26.     'Content-Length': size  
  27.   });  
  28.   f.pipe(res);  
  29. })  
  30. // 设置跨域访问  
  31. app.all("*", function (request, response, next) { 
  32.   // 设置跨域的域名,* 代表允许任意域名跨域;http://localhost:8080 表示前端请求的 Origin 地址  
  33.   response.header("Access-Control-Allow-Origin", "http://127.0.0.1:5500");  
  34.   //设置请求头 header 可以加那些属性  
  35.   response.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With');  
  36.   //暴露给 axios https://blog.csdn.net/w345731923/article/details/114067074  
  37.   response.header("Access-Control-Expose-Headers", "Content-Disposition");  
  38.   // 设置跨域可以携带 Cookie 信息  
  39.   response.header('Access-Control-Allow-Credentials', "true");  
  40.   //设置请求头哪些方法是合法的  
  41.   response.header(  
  42.     "Access-Control-Allow-Methods",  
  43.     "PUT,POST,GET,DELETE,OPTIONS"  
  44.   );  
  45.   response.header("Content-Type", "application/json;charset=utf-8");  
  46.   next();  
  47. });  
  48. // 接口数据解析  
  49. app.use(bodyParser.json())  
  50. app.use(bodyParser.urlencoded({  
  51.   extended: false  
  52. }))  
  53. app.use('/api', router) // 路由注册  
  54. app.listen(8081, () => {  
  55.   console.log("服务器启动成功!")  
  56. }); 

git 地址

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

 

责任编辑:庞桂玉 来源: 前端大全
相关推荐

2023-09-19 22:41:30

控制器HTTP

2021-04-22 05:37:14

Axios 开源项目HTTP 拦截器

2021-01-18 05:13:04

TomcatHttp

2018-07-30 16:31:00

javascriptaxioshttp

2021-04-12 05:55:29

缓存数据Axios

2021-04-06 06:01:11

AxiosWeb 项目开发

2024-03-19 08:36:19

2021-06-02 05:41:48

项目实践Axiosaxios二次封装

2021-07-23 15:55:31

HTTPETag前端

2021-05-26 05:18:51

HTTP ETag Entity Tag

2018-10-18 10:05:43

HTTP网络协议TCP

2019-12-13 09:14:35

HTTP2协议

2019-07-02 08:24:07

HTTPHTTPSTCP

2018-07-24 13:01:52

前端优化前端性能浏览器

2019-04-08 15:11:12

HTTP协议Web

2024-04-15 16:11:33

C#HTTP请求.NET

2021-08-26 06:58:14

Http请求url

2022-06-07 08:59:58

hookuseRequestReact 项目

2020-10-19 19:05:20

VueAxiosAPI

2023-03-06 07:25:09

http响应头ETag
点赞
收藏

51CTO技术栈公众号