作者 | 追公交车的小仙女       责编 | 欧阳姝黎

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)创建的对象) 既可以当作对象使用,也可以当作函数使用:

// 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>

第二种:二进制文件流的形式传递,我们直接访问该接口并不能下载文件,一定程度保证了数据的安全性。比较多的场景是:后端接收到查询参数,查询数据库然后通过插件动态生成 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.txtconst 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}})
}

https://juejin.cn/post/6878912072780873742

手写 Axios 核心代码

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

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

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

axios-js
│  index.html
│
└─libadapter.jsAxios.jsaxiosInstance.jsCancelToken.jsInterceptorManager.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 instanceutils.extend(instance, Axios.prototype, context);// Copy context to instanceutils.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 adapteradapter = require('./adapters/xhr');} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {// For node use HTTP adapteradapter = 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 数组前面,把响应拦截函数添加到数组后面。这样就可以实现发送前拦截和响应后拦截的效果。

创建 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 xhrxhr = 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/114067074response.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 地址:https://github.com/YY88Xu/axios-js
Axios 封装:https://github.com/YY88Xu/vue2-component

生于2001年的《程序员》曾陪伴了无数开发者成长,影响了一代又一代的中国技术人。时隔20年,《新程序员》带着全球技术大师深邃思考、优秀开发者技术创造等深度内容回来了!同时将全方位为所有开发者呈现国内外核心技术生态体系全景图。扫描下方小程序码即可立即订阅!

axios 是如何封装 HTTP 请求的相关推荐

  1. Vue3(撩课学院)笔记09-axios简介,发起get请求的两种方式,发起带参的get及post请求,发起并发请求,并发请求结果将数组展开,axios全局配置,axios配置及封装,请求和响应拦截

    1.axios简介 axios是基于promise可以用于浏览器和node.js的网络请求库,在服务器端使用原生node.js,在浏览气短使用ajax(即XMLHttpRequests) 2.axio ...

  2. axios基础和封装

    一.简介 axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,它本身具有以下特征: 从浏览器中创建 XMLHttpRequest 从 node.js 发出 http ...

  3. axios 配置loading_用Axios Element 实现全局的请求 loading

    Kapture 2018-06-07 at 14.57.40.gif 背景 业务需求是这样子的,每当发请求到后端时就触发一个全屏的 loading,多个请求合并为一次 loading. 现在项目中用的 ...

  4. ios 请求失败封装_vue_axios请求封装、异常拦截统一处理

    1.前端网络请求封装.异常统一处理 vue中采用axios处理网络请求,避免请求接口重复代码,以及各种网络情况造成的异常情况的判断,采用axios请求封装和异常拦截操作: axios 请求封装 // ...

  5. 原生 Ajax 封装 和 Axios 二次 封装

    AJAX 异步的JavaScript与XML技术( Asynchronous JavaScript and XML ) Ajax 不需要任何浏览器插件,能在不更新整个页面的前提下维护数据,但需要用户允 ...

  6. vue:axios二次封装,接口统一存放

    https://www.jianshu.com/p/9077baa9d543 一.基于框架:vue 二.基于http库:axios 三.基本用法:     1.通过node安装: npm instal ...

  7. 实现uniapp的app和小程序开发中能使用axios进行跨域网络请求,并支持携带cookie

    实现uniapp的app和小程序开发中能使用axios进行跨域网络请求,并支持携带cookie 1-使用npm install axios;命令安装axios 2-新建一个文件夹再建一个.js后缀文件 ...

  8. axios的简单封装

    前言 在每次使用原装的axios发送 http请求时,如果需要token验证,则都需要创建拦截器,添加'token'请求头,或者在config中具体的请求体中添加,是一个非常麻烦的事情. 遇到401, ...

  9. axios二次封装以及API接口统一管理

    前端向服务器发送请求,使用的方法有很多: XMLHttpRequest.fetch.JQ.axios 1.axios二次封装 二次封装axios是为了请求拦截器.响应拦截器. 请求拦截器:可以在发送请 ...

最新文章

  1. 常见的web漏洞及其防范(转)
  2. 一周小程序学习 第1天
  3. C++ 多态实现机制
  4. scikit-learn学习笔记(四)Ridge Regression ( 岭回归 )
  5. 使用BAT批处理执行sql
  6. jdbc 批量insert_JDBC相关知识解答
  7. React 第十二章 React思想
  8. Linux 查看命令
  9. python类的构造方法和assert的使用,用MethodType动态绑定类方法
  10. oracle 同步索引,oracle全文索引之同步和优化索引做了什么
  11. RGB色彩模式划分为0~255,是按照什么标准进行划分的?
  12. java 拖拉机_Java——io流
  13. Machine Learning Regression-Case Study
  14. 上升了百分之几怎么算_增长百分比怎么算
  15. 如何删去微信小程序服务器,微信小程序怎么注销
  16. 图片 bmp 格式详解
  17. 四款最热门的云计算产品
  18. pcs for linux7下载,BaiduPCS-Go
  19. hiho #1474 拆字游戏(dfs,记录状态)
  20. java zip 创建目录_Java实现Zip压缩目录中的所有文件

热门文章

  1. 分享一个stm32的OLED驱动,实现图像平移滚动滑动,esp8266-wifi物联网远程开关天气,开源代码电路原理图
  2. 【Spring Cloud】负载均衡-Ribbon
  3. 面试—每日一题(5)
  4. 20个有趣的Linux命令
  5. Entity Framework Codefirst的配置步骤
  6. Centos7下Yum安装PHP5.5,5.6,7.0
  7. 乐观锁-version的使用
  8. 剑指Offer_07_斐波那契数列
  9. ARP攻击原理简析及防御措施
  10. Objective-c方法调用流程