axios 是如何封装 HTTP 请求的

概述
前端开发中,经常会遇到发送异步请求的场景。一个功能齐全的 HTTP 请求库可以大大降低我们的开发成本,提高开发效率。

axios 就是这样一个 HTTP 请求库,近年来非常热门。目前,它在 GitHub 上拥有超过 40,000 的 Star,许多权威人士都推荐使用它。

因此,我们有必要了解下 axios 是如何设计,以及如何实现 HTTP 请求库封装的。撰写本文时,axios 当前版本为 0.18.0,我们以该版本为例,来阅读和分析部分核心源代码。axios 的所有源文件都位于 lib 文件夹中,下文中提到的路径都是相对于 lib 来说的。

本文我们主要讨论:

怎样使用 axios。
axios 的核心模块(请求、拦截器、撤销)是如何设计和实现的?
axios 的设计优点是什么?

如何使用 axios
要理解 axios 的设计,首先需要看一下如何使用 axios。我们举一个简单的例子来说明下 axios API 的使用。

发送请求

axios({
method:‘get’,
url:‘http://bit.ly/2mTM3nY’,
responseType:‘stream’
})
.then(function(response) {
response.data.pipe(fs.createWriteStream(‘ada_lovelace.jpg’))
});
这是一个官方示例。从上面的代码中可以看到,axios 的用法与 jQuery 的 ajax 方法非常类似,两者都返回一个 Promise 对象(在这里也可以使用成功回调函数,但还是更推荐使用 Promise 或 await),然后再进行后续操作。

这个实例很简单,不需要我解释了。我们再来看看如何添加一个拦截器函数。

添加拦截器函数
axios.interceptors.request.use(function (config) {
return config;
}, function (error) {
return Promise.reject(error);
});

axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
return Promise.reject(error);
});
从上面的代码,我们可以知道:发送请求之前,我们可以对请求的配置参数(config)做处理;在请求得到响应之后,我们可以对返回数据做处理。当请求或响应失败时,我们还能指定对应的错误处理函数。

撤销 HTTP 请求
在开发与搜索相关的模块时,我们经常要频繁地发送数据查询请求。一般来说,当我们发送下一个请求时,需要撤销上个请求。因此,能撤销相关请求功能非常有用。axios 撤销请求的示例代码如下:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get(’/user/12345’, {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log(‘请求撤销了’, thrown.message);
} else {
}
});

axios.post(’/user/12345’, {
name: ‘新名字’
}, {
cancelToken: source.token
}).

source.cancel(‘用户撤销了请求’);
从上例中可以看到,在 axios 中,使用基于 CancelToken 的撤销请求方案。然而,该提案现已撤回,详情如 点这里。具体的撤销请求的实现方法,将在后面的源代码分析的中解释。

axios 核心模块的设计和实现
通过上面的例子,我相信每个人都对 axios 的使用有一个大致的了解了。下面,我们将根据模块分析 axios 的设计和实现。下面的图片,是我在本文中会介绍到的源代码文件。如果您感兴趣,最好在阅读时克隆相关的代码,这能加深你对相关模块的理解。

HTTP 请求模块
请求模块的代码放在了 core/dispatchRequest.js 文件中,这里我只展示了一些关键代码来简单说明:

module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
var adapter = config.adapter || defaults.adapter;

return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
    return response;
}, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
        throwIfCancellationRequested(config);
        return Promise.reject(reason);
    });

};
上面的代码中,我们能够知道 dispatchRequest 方法是通过 config.adapter ,获得发送请求模块的。我们还可以通过传递,符合规范的适配器函数来替代原来的模块(一般来说,我们不会这样做,但它是一个松散耦合的扩展点)。

在 defaults.js 文件中,我们可以看到相关适配器的选择逻辑——根据当前容器的一些独特属性和构造函数,来确定使用哪个适配器。

function getDefaultAdapter() {
var adapter;
if (typeof process !== ‘undefined’ && Object.prototype.toString.call(process) === ‘[object process]’) {
adapter = require(’./adapters/http’);
} else if (typeof XMLHttpRequest !== ‘undefined’) {
adapter = require(’./adapters/xhr’);
}
return adapter;
}
axios 中的 XHR 模块相对简单,它是对 XMLHTTPRequest 对象的封装,这里我就不再解释了。有兴趣的同学,可以自己阅读源源码看看,源码位于 adapters/xhr.js 文件中。

拦截器模块
现在让我们看看 axios 是如何处理,请求和响应拦截器函数的。这就涉及到了 axios 中的统一接口 ——request 函数。

Axios.prototype.request = function request(config) {
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);

this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
});

while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
}
return promise;

};
这个函数是 axios 发送请求的接口。因为函数实现代码相当长,这里我会简单地讨论相关设计思想:

chain 是一个执行队列。队列的初始值是一个携带配置(config)参数的 Promise 对象。

在执行队列中,初始函数 dispatchRequest 用来发送请求,为了与 dispatchRequest对应,我们添加了一个 undefined。添加 undefined 的原因是需要给 Promise 提供成功和失败的回调函数,从下面代码里的 promise = promise.then(chain.shift(), chain.shift()); 我们就能看出来。因此,函数 dispatchRequest 和 undefiend 可以看成是一对函数。

在执行队列 chain 中,发送请求的 dispatchReqeust 函数处于中间位置。它前面是请求拦截器,使用 unshift 方法插入;它后面是响应拦截器,使用 push 方法插入,在 dispatchRequest 之后。需要注意的是,这些函数都是成对的,也就是一次会插入两个。

浏览上面的 request 函数代码,我们大致知道了怎样使用拦截器。下一步,来看看怎样撤销一个 HTTP 请求。

撤销请求模块
与撤销请求相关的模块位于 Cancel/ 文件夹下,现在我们来看下相关核心代码。
首先,我们来看下基础 Cancel 类。它是一个用来记录撤销状态的类,具体代码如下:

function Cancel(message) {
this.message = message;
}

Cancel.prototype.toString = function toString() {
return ‘Cancel’ + (this.message ? ': ’ + this.message : ‘’);
};

Cancel.prototype.CANCEL = true;
使用 CancelToken 类时,需要向它传递一个 Promise 方法,用来实现 HTTP 请求的撤销,具体代码如下:

function CancelToken(executor) {
if (typeof executor !== ‘function’) {
throw new TypeError(‘executor must be a function.’);
}

var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
});

var token = this;
executor(function cancel(message) {
    if (token.reason) {
        return;
    }
    token.reason = new Cancel(message);
    resolvePromise(token.reason);
});

}

CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor© {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
adapters/xhr.js 文件中,撤销请求的地方是这样写的:

if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}

    request.abort();
    reject(cancel);
    request = null;
});

}
通过上面的撤销 HTTP请求的例子,让我们简要地讨论一下相关的实现逻辑:

在需要撤销的请求中,调用 CancelToken 类的 source 方法类进行初始化,会得到一个包含 CancelToken 类实例 A 和 cancel 方法的对象。

当 source 方法正在返回实例 A 的时候,一个处于 pending 状态的 promise 对象初始化完成。在将实例 A 传递给 axios 之后,promise 就可以作为撤销请求的触发器使用了。

当调用通过 source 方法返回的 cancel 方法后,实例 A 中 promise 状态从 pending 变成 fulfilled,然后立即触发 then 回调函数。于是 axios 的撤销方法——request.abort() 被触发了。

axios 这样设计的好处是什么?
发送请求函数的处理逻辑

如前几章所述,axios 不将用来发送请求的 dispatchRequest 函数看做一个特殊函数。实际上,dispatchRequest 会被放在队列的中间位置,以便保证队列处理的一致性和代码的可读性。

适配器的处理逻辑

在适配器的处理逻辑上,http 和 xhr 模块(一个是在 Node.js 中用来发送请求的,一个是在浏览器里用来发送请求的)并没有在 dispatchRequest 函数中使用,而是各自作为单独的模块,默认通过 defaults.js 文件中的配置方法引入的。因此,它不仅确保了两个模块之间的低耦合,而且还为将来的用户提供了定制请求发送模块的空间。

撤销 HTTP 请求的逻辑

在撤销 HTTP 请求的逻辑中,axios 设计使用 Promise 来作为触发器,将 resolve 函数暴露在外面,并在回调函数里使用。它不仅确保了内部逻辑的一致性,而且还确保了在需要撤销请求时,不需要直接更改相关类的样例数据,以避免在很大程度上入侵其他模块。

总结
本文详细介绍了 axios 的用法、设计思想和实现方法。在阅读之后,您可以了解 axios 的设计,并了解模块的封装和交互。

本文只介绍了 axios 的核心模块,如果你对其他模块代码感兴趣,可以到 GitHub 上查看。

<think>好的,我现在需要帮助用户理解为什么需要封装axiosHTTP请求。用户的问题看起来是关于前端开发的,特别是使用axios库进行网络请求的情况。首先,我得回忆一下axios的基本用法,以及在实际项目中为什么要进行封装。 用户可能已经知道axios是一个基于Promise的HTTP客户端,用于浏览器和Node.js。但直接使用axios虽然方便,但在大型项目中可能会有重复代码,比如设置baseURL、处理错误、添加请求头等。这时候,封装axios可以带来很多好处,比如统一管理配置,减少重复代码,提高可维护性。 接下来,我需要考虑用户可能的背景。他们可能正在学习前端开发,或者正在准备面试,复试中提到过移动端适配的问题,可能现在转向了网络请求方面的技术。用户可能想知道封装axios的具体原因和实际应用场景,而不仅仅是概念上的解释。 用户的问题“axios封装http请求干嘛”其实是在问封装的目的和好处。我需要列举出几个关键点,比如统一错误处理、配置管理、拦截器的使用、简化调用方式等。同时,要结合实际例子,说明如何通过封装提高开发效率和代码的可维护性。 还需要预测用户可能的后续问题,比如如何具体实现封装,或者封装后的请求和直接使用axios有什么区别。这时候,给出一个简单的代码示例会有帮助,但用户之前的回复中有代码块,可能需要用Markdown格式展示,但根据当前指示,不需要使用代码块,但可以提到示例的结构。 另外,用户之前的问题涉及到移动端适配,可能他们正在准备一个综合性的项目,需要整合多个方面的知识,比如UI适配、网络请求等。因此,在回答时,可以适当提到封装axios如何与项目其他部分结合,比如配合Vue或React框架使用,如何处理鉴权令牌等。 还要注意避免技术术语过于复杂,用简洁明了的中文解释概念。比如,解释拦截器时,可以说明它们在请求发出前或响应返回后的处理作用,比如添加loading动画,统一处理错误信息等。 总结下来,回答的结构应该包括:封装的目的、主要优点、具体实现方式(如拦截器、默认配置)、实际应用场景,以及可能的示例。同时,结合用户之前的复试准备内容,强调封装带来的协作效率和维护性提升,可能让回答更贴近他们的需求。</think>--- **一、封装axios的核心价值(必答要点)** 通过封装axios实现以下开发优化目标: 1. **统一错误处理** - 自动拦截401/403/500等异常状态码 - 全局错误提示(如:`Toast轻提示`代替`console.log`) - 网络超时自动重试机制 2. **标准化请求流程** ```javascript // 原生写法 axios.get('/api/data').then(...).catch(...) // 封装http.get('/data').then(...) // 自动处理异常 ``` 3. **安全加固** - 自动携带JWT令牌 - CSRF Token动态注入 - 敏感参数加密处理 --- **二、典型封装结构(技术亮点)** ```javascript const service = axios.create({ baseURL: 'https://api.example.com', timeout: 15000 // 15秒超时 }) // 请求拦截器(发送前处理) service.interceptors.request.use(config => { config.headers.Authorization = localStorage.getItem('token') return config }) // 响应拦截器(接收后处理) service.interceptors.response.use( response => { if (response.data.code !== 200) { showErrorToast(response.data.msg) return Promise.reject() } return response.data }, error => { if (error.response.status === 401) { router.push('/login') } return Promise.reject(error) } ) ``` --- **三、高频追问应对策略** 1. **为什么不用fetch?** "axios具备自动转换JSON数据、取消请求、拦截器等生产级特性,而fetch需要手动封装,且默认不带cookie" 2. **如何实现取消重复请求?** "通过axios.CancelToken生成唯一标识,在拦截器中检查重复URL+参数,自动取消前序未完成请求" 3. **文件上传如何特殊处理?** "单独封装upload方法,设置`headers: {'Content-Type': 'multipart/form-data'}`,并暴露上传进度回调" --- **四、项目实践示例** ```javascript // 封装后的调用示例 import http from '@/utils/request' // 获取问诊记录 http.post('/consult', { symptom: '头痛', duration: '3天' }).then(data => { // 直接获取业务数据,无需处理状态码 showConsultList(data) }) // 错误场景自动处理 → 401状态码:跳转登录页 → 500错误:显示"服务器开小差,请稍后重试" ``` --- **五、性能优化方向(加分项)** 1. **请求缓存**:对GET请求结果进行内存缓存(LRU策略) 2. **并发控制**:限制并行请求数量(特别是低端安卓设备) 3. **智能重试**:对网络波动导致的失败自动重试2次 4. **流量优化**:响应数据压缩(gzip)、图片格式自动转换(WebP) 建议结合具体业务场景说明,例如:"在在线问诊的IM模块中,通过封装实现了消息重发机制:发送失败的消息会自动进入待发送队列,网络恢复后优先重传"
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值