Fetch vs. Axios,选择适合自己的 HTTP 请求方式省流:Axios 在各个方面都完全碾压 Fetch,F
为了拓展我的技术栈,我开始接触到了第二款支持服务端渲染并支持 TS 全栈开发的框架 Next.js。既然是支持全栈,那么其 API 请求 的设计便是重中之重。
以往我自己开发的一些基于 Vue & React 的 SPA 项目,涉及到接口层面无一例外都是通过 二次封装 Axios 来实现。但经过一些调研,我惊讶的发现 Fetch 似乎逐渐地在替代 Axios 成为主流的请求方式。 并且,Next.js 的官方也是推荐使用 Fetch 来实现数据的请求。
那么,这两者究竟有何种具体的区别?如何为自己的项目选择合适的 HTTP 请求方式呢?为了满足我的好奇心,我决定对这两者进行一些系统性的调研。
AJAX
在开始之前,我们需要明白的是无论是 Fetch API 还是 Axios,本质上都是对 AJAX 的一种实现。而 Fetch 本身是浏览器的一个原生 API,Axios 则是一个由用户自行开发的第三方库。
AJAX 全称 Asynchronous JavaScript and Xml,直译过来是 异步的 JS 和 XML。AJAX 本身不代指任何特定的技术实现,它只是一种 技术手段,表示能够实现在不重新加载整个网页的情况下,通过与服务器进行异步通信来更新部分网页内容。随着前端技术的发展,实现 AJAX 的手段也越来越现代化,我们可以简单了解一下 AJAX 技术的发展史:
当然,随着前端水平的发展,目前出现了如 GraphQL 一类新型的互联网数据交换协议,不过目前似乎还没有在国内成为主流?
Axios
简介与概念
Axios 是目前最为主流的 HTTP 请求方式之一,其全称为 Ajax I/O System。
而 Ajax 的全称为 Asynchronous JavaScript and Xml。我们都知道,Ajax 本身不代指任何具体的技术,它只是一种 技术手段,表示能够实现在不重新加载整个网页的情况下,通过与服务器进行异步通信来更新部分网页内容。
最早实现 Ajax 的方式是通过浏览器内置的 XMLHttpRequest 来实现,而 Ajax 则是针对于 XMLHttpRequest 的 Promise 封装实现。之后随着 Node.js 的发展,它又使用了其原生 http 模块实现了服务端与客户端的 同构。借一下官网给出的定义:
Axios 是一个基于 promise 网络请求库,作用于
node.js
和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和 node.js 中)。在服务端它使用原生 node.jshttp
模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。
可以看出,Axios 的底层实现是 XMLHttpRequest,引入 Promise 对其进行了封装,让本就相对较为完善的功能变得更加强大,并真正意义上的引入了异步处理请求的概念。同时也优雅地处理了原生 XMLHttpRequest 的同步写法的逆天操作,包括但不限于:
- XMLHttpRequest 的使用通常需要处理回调函数,而回调函数容易导致“回调地狱”,使代码变得复杂且难以维护。
- 使用 XMLHttpRequest 时,错误处理通常需要在多个地方进行(例如,处理网络错误、处理请求超时、处理 HTTP 状态码错误等)。
特点
按照官网所述,Axios 总共具有如下的一些特点:
- 从浏览器创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求和响应数据
- 取消请求
- 超时处理
- 查询参数序列化支持嵌套项处理
- 自动将请求体序列化为:
- JSON (
application/json
) - Multipart / FormData (
multipart/form-data
) - URL encoded form (
application/x-www-form-urlencoded
)
- JSON (
- 将 HTML Form 转换成 JSON 进行请求
- 自动转换 JSON 数据
- 获取浏览器和 node.js 的请求进度,并提供额外的信息(速度、剩余时间)
- 为 node.js 设置带宽限制
- 兼容符合规范的 FormData 和 Blob(包括 node.js)
- 客户端支持防御XSRF
可以看到,作为一个第三方库,其功能之繁多,几乎涵盖了绝大多数使用 HTTP 进行请求的复杂场景。
基本使用
接下来我们对 axios 进行一些基本的使用。
首先安装 axios:
npm install axios
之后引入它,编写一个简单的 GET 请求:
import axios from "axios";
axios
.get("https://api.example.com/data")
.then((response) => console.log(response.data))
.catch((error) => console.error("Axios error:", error));
直接的使用 Axios 进行请求十分的简单,只需要调用其对应的请求方式的方法即可。Axios 本身封装好的方法如下所示:
export class Axios {
constructor(config?: AxiosRequestConfig);
defaults: AxiosDefaults;
interceptors: {
request: AxiosInterceptorManager<InternalAxiosRequestConfig>;
response: AxiosInterceptorManager<AxiosResponse>;
};
getUri(config?: AxiosRequestConfig): string;
request<T = any, R = AxiosResponse<T>, D = any>(
config: AxiosRequestConfig<D>
): Promise<R>;
get<T = any, R = AxiosResponse<T>, D = any>(
url: string,
config?: AxiosRequestConfig<D>
): Promise<R>;
delete<T = any, R = AxiosResponse<T>, D = any>(
url: string,
config?: AxiosRequestConfig<D>
): Promise<R>;
head<T = any, R = AxiosResponse<T>, D = any>(
url: string,
config?: AxiosRequestConfig<D>
): Promise<R>;
options<T = any, R = AxiosResponse<T>, D = any>(
url: string,
config?: AxiosRequestConfig<D>
): Promise<R>;
post<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
): Promise<R>;
put<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
): Promise<R>;
patch<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
): Promise<R>;
postForm<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
): Promise<R>;
putForm<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
): Promise<R>;
patchForm<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
): Promise<R>;
}
虽然直接调用封装好的方法很方便,但是为了统一同一项目中的请求配置,主流的做法是将请求配置进行全局定义后,对 axios 进行二次封装,直接通过配置项进行请求。
axios({
method: "post",
url: "http://example.com/",
data: {
firstName: "David",
lastName: "Pollock",
},
})
.then((response) => {})
.catch((error) => {});
底层处理
当我们调用上述 .get
方法后,axios 底层对请求的处理流程主要为:
axios.get() 调用
↓
合并请求配置
↓
请求拦截器(如果有)
↓
选择适配器(浏览器环境中选择 XMLHttpRequest)
↓
发送请求(使用 XMLHttpRequest 发送)
↓
处理响应
↓
响应拦截器(如果有)
↓
返回 Promise(resolve 或 reject)
-
请求配置的合并
首先,Axios 会将你传入的请求配置(在这个例子中就是 URL 和默认的 GET 方法)与全局配置(如果有设置的话)进行合并。这个合并操作包括 URL、方法、头部信息、参数、数据、响应类型等。往往在对 axios 进行二次封装时需要手动重写 axios 配置。
-
拦截器的应用
Axios 允许在请求发送之前和响应到达之后分别设置请求拦截器和响应拦截器。拦截器在请求发出前可以对请求配置进行修改,比如添加认证 token,或者对请求数据进行处理;在响应返回后也可以对响应数据进行处理,或统一处理错误。
-
适配器选择
Axios 内部使用了一个名为
adapter
的模块,决定了使用哪种底层方法来发送请求。通常在浏览器环境中,Axios 会选择XMLHttpRequest
作为适配器,而在 Node.js 环境中,它会选择http
模块。 -
请求的发送
选择适配器后,Axios 调用适配器对应的方法来发送 HTTP 请求。以浏览器环境为例,Axios 使用
XMLHttpRequest
来发送请求。具体的流程如下:
-
创建
XMLHttpRequest
对象。 -
调用
xhr.open
方法设置请求的 HTTP 方法和 URL。 -
设置请求头部信息。
-
如果有请求数据,则通过
xhr.send(data)
发送数据;如果是 GET 请求且没有数据,则直接调用xhr.send()
。
-
-
处理响应
请求发送后,Axios 会等待请求的完成,并在完成后处理响应。
-
状态检查:首先,Axios 会检查 HTTP 状态码是否在 2xx 范围内。如果是,则认为请求成功。
-
解析响应:如果请求成功,Axios 会将
XMLHttpRequest
返回的响应数据解析为 JSON(如果响应头部的Content-Type
是application/json
),并将解析后的数据封装在一个response
对象中,传递给.then
回调。 -
错误处理:如果 HTTP 状态码不在 2xx 范围内,或者请求本身失败(如网络错误、超时等),则会触发
.catch
回调,将错误信息传递给该回调。
-
-
响应拦截器
在响应数据返回并处理完毕后,如果设置了响应拦截器,Axios 会调用这些拦截器对响应数据进行进一步处理,比如对响应状态码进行拦截后结合组件库显示对应的提示信息或者手动抛异常。
-
返回 Promise
Axios 的所有方法都返回一个 Promise 对象。在请求完成后,Axios 通过
resolve
或reject
来处理请求的结果,将响应数据或错误传递给.then
或.catch
以实现链式调用,或者结合 async/await 来实现同步代码编写异步任务。
主流功能的实现
如今前端主流的请求功能实现有:
- 响应超时
- 拦截器
- Cookie 携带
- 数据转化
- 跟踪请求进度
- 并发请求
响应超时
AxiosRequestConfig 中提供了 timeout
属性,直接在配置中设置即可,单位为 ms。实际上是直接使用了xhr 的 timeout 属性来设置的,并通过 xhr 的 ontimeout 方法对超时事件进行捕获。当请求超过指定的超时时间且未收到响应时,该事件会被触发,执行如下操作:
- 中止请求:调用
xhr.abort()
方法中止请求,防止其继续执行。虽然请求超时了,但服务器可能仍然在处理请求,为了节省带宽和资源,通常会中止请求。 - 返回错误:Axios 会调用
reject
来触发 Promise 的catch
回调,返回一个带有超时错误信息的错误对象。错误对象通常包括错误消息、请求配置、错误代码(如'ECONNABORTED'
),以及XMLHttpRequest
对象本身。
axios({
method: "post",
url: "http://example.com/",
timeout: 4000, // 请求4秒无响应则会超时
data: {
firstName: "David",
lastName: "Pollock",
},
})
.then((response) => {
/* 处理响应 */
})
.catch((error) => console.error("请求超时"));
拦截器
拦截器是 Axios 其能够广泛普及的原因之一。Axios 本身就提供了手动配置请求拦截器、响应拦截器的方法,通常用于二次封装中:
axios.interceptors.request.use(
(config) => {
// 对配置进行一些处理,比如解析 token,放到 Header里面
return config;
},
(err) => Promise.reject(err)
);
axios.interceptors.response.use(
(response) => response.data,
(err) => {
// 对错误进行一些处理,比如一些自定义的响应状态码的捕捉处理
Promise.reject(err);
}
);
Cookie 携带
Axios 默认会发送与请求同源的 Cookie,且可以通过 withCredentials
选项进行控制。withCredentials
选项的可选值:
true
: 发送跨域请求时携带 Cookiefalse
: 不发送跨域请求时携带 Cookie(默认值)
数据转化
Axios 的特性之一就是能够对请求数据进行自动转化。
- 将 JavaScript 对象自动转换为 JSON 字符串并设置适当的
Content-Type
。 - 自动识别和处理
FormData
、URLSearchParams
等格式的数据。 - 自动解析
application/json
类型的响应数据为 JavaScript 对象。
跟踪请求进度
Axios 的配置项中,天生提供了 onDownloadProgress
和 onUploadProgress
回调函数配置选项,分别用于跟踪下载进度和上传进度。
axios({
method: "get",
url: "https://example.com/large-file",
onDownloadProgress: function (progressEvent) {
const { loaded, total, timeStamp } = progressEvent;
const percentage = Math.round((loaded * 100) / total);
console.log(
`Downloaded: ${loaded} bytes of ${total} bytes (${percentage}%)`
);
// 计算下载速度 (bytes per second)
const speed = loaded / (timeStamp / 1000);
console.log(`Download speed: ${speed} bytes/second`);
// 计算剩余时间 (秒)
const remainingTime = (total - loaded) / speed;
console.log(`Estimated remaining time: ${remainingTime} seconds`);
},
})
.then((response) => {
console.log("Download complete");
})
.catch((error) => {
console.error("Error downloading the file:", error);
});
const data = new FormData();
data.append("file", file);
axios({
method: "post",
url: "https://example.com/upload",
data: data,
onUploadProgress: function (progressEvent) {
const { loaded, total, timeStamp } = progressEvent;
const percentage = Math.round((loaded * 100) / total);
console.log(`Uploaded: ${loaded} bytes of ${total} bytes (${percentage}%)`);
// 计算上传速度 (bytes per second)
const speed = loaded / (timeStamp / 1000);
console.log(`Upload speed: ${speed} bytes/second`);
// 计算剩余时间 (秒)
const remainingTime = (total - loaded) / speed;
console.log(`Estimated remaining time: ${remainingTime} seconds`);
},
})
.then((response) => {
console.log("Upload complete");
})
.catch((error) => {
console.error("Error uploading the file:", error);
});
并发请求
Axios 既然基于 Promise,当然支持以 Promise.all()
为核心的并发异步任务。Axios 提供了一个单独的 all 方法来实现:
axios
.all([
axios.get("https://api.github.com/users/iliakan"),
axios.get("https://api.github.com/users/taylorotwell"),
])
.then(
axios.spread((obj1, obj2) => {
...
})
);
Fetch API
简介与概念
Fetch 本身和 Axios 有着本质的不同,它本身是一个浏览器的原生自带的 API,可参见MDN 官方文档。
Fetch 提供了对 Request
和 Response
(以及其他与网络请求有关的)对象的通用定义。这将在未来更多需要它们的地方使用它们,无论是 service worker、Cache API,又或者是其他处理请求和响应的方式,甚至是任何一种需要你自己在程序中生成响应的方式(即使用计算机程序或者个人编程指令)。
它同时还为有关联性的概念,例如 CORS 和 HTTP Origin 标头信息,提供一种新的定义,取代它们原来那种分离的定义。
fetch()
强制接受一个参数,即要获取的资源的路径。它返回一个 Promise
,该 Promise 会在服务器使用标头响应后,兑现为该请求的 Response
——即使服务器的响应是 HTTP 错误状态。你也可以传一个可选的第二个参数 init
(参见 Request
)。
一旦 Response
被返回,有许多方法可以获取主体定义的内容以及如何处理它。
你也可以通过 Request()
和 Response()
构造函数直接创建请求和响应。但是我们不建议这么做,它们更可能被创建为其他的 API 操作的结果(比如,service worker 中的 FetchEvent.respondWith
)。
Fetch 本身也使用了 Promise 来处理异步的请求,但是其作为一个浏览器原生的 API,在通用性和一些开箱即用的功能上面是无法和 Axios 相提并论的。很多的功能我们都需要自己手动去封装一遍方可实现和 Axios 类似的效果。
特点
思来想去,这个 API 说实话没有 Axios 那样显著的特点。
- 浏览器原生支持,这个是我觉得最大的优点
- 简洁的请求语法,和 Axios 类似。
- 原生 Promise,可以和异步编程完美结合
- 更加接近 HTTP 请求本质的配置(这个算吗)
基本使用
因同样是基于 Promise,Fetch 的使用方式和 Axios 大差不差。
const url = "http://example.com/";
const options = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
},
body: JSON.stringify({
a: 10,
b: 20,
}),
};
fetch(url, options).then((response) => {
console.log(response.status);
});
其中最大的不同之处在于 传递数据的方式不同,Axios 是放到 data
属性里,以对象的方式进行传递即可,因为 Axios 本身就具有对数据自动转化的能力;而 Fetch 则是需要放在 body 属性中,先用 JSON.stringfy()
方法转为字符串后方可进行传递。
简单来说,Fetch 请求更接近原生 HTTP 的请求方式,一个一个都需要你手动配置;Axios 开箱即用,调用方法、传递 URL 和数据即可。
主流功能的实现
Fetch 如果想实现和 Axios 一样的开箱即用功能,需要自己手动封装方法。
响应超时
Fetch 需要手动结合 AbortController 来对一个或者多个 Web 请求进行手动控制。
AbortController 本身是一个用于控制 DOM 请求(如 Fetch 请求)的对象,允许你在发起请求之后根据需要取消该请求。
AbortController 对象有一个 signal
属性,这是一个 AbortSignal
对象,它与特定的请求关联。当调用 AbortController 的 abort()
方法时,signal
会发出中止信号,相关联的 fetch 请求就会被取消,并抛出一个 DOMException
,其 name
属性值为 "AbortError"
。
我们如果需要设置超时功能,那么需要手动的创建一个 setTimeout,来对 abort()
方法进行定时调用(感觉好蠢啊):
const controller = new AbortController();
const options = {
method: "POST",
signal: controller.signal,
body: JSON.stringify({
firstName: "David",
lastName: "Pollock",
}),
};
const promise = fetch("http://example.com/", options);
// 如果4秒钟没有响应则超时
const timeoutId = setTimeout(() => controller.abort(), 4000);
promise
.then((response) => {
/* 处理响应 */
})
.catch((error) => console.error("请求超时"));
拦截器
同样地,Fetch 想实现拦截器也得自己手动封装并添加方法。可以将 Fetch API 进行 Class 二次封装并添加拦截器方法,如下所示:
type RequestInterceptor = (input: RequestInfo, init?: RequestInit) => Promise<[RequestInfo, RequestInit?]>;
type ResponseInterceptor = (response: Response) => Promise<Response>;
class HttpClient {
private requestInterceptors: RequestInterceptor[] = [];
private responseInterceptors: ResponseInterceptor[] = [];
// 添加请求拦截器
addRequestInterceptor(interceptor: RequestInterceptor) {
this.requestInterceptors.push(interceptor);
}
// 添加响应拦截器
addResponseInterceptor(interceptor: ResponseInterceptor) {
this.responseInterceptors.push(interceptor);
}
// 封装的 fetch 方法
async fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
// 执行所有的请求拦截器
for (const interceptor of this.requestInterceptors) {
[input, init] = await interceptor(input, init);
}
// 发起请求
let response = await fetch(input, init);
// 执行所有的响应拦截器
for (const interceptor of this.responseInterceptors) {
response = await interceptor(response);
}
return response;
}
}
// 使用示例
const httpClient = new HttpClient();
// 添加一个请求拦截器
httpClient.addRequestInterceptor(async (input, init) => {
const modifiedInit = {
...init,
headers: {
...init?.headers,
Authorization: 'Bearer token',
},
};
return [input, modifiedInit];
});
// 添加一个响应拦截器
httpClient.addResponseInterceptor(async (response) => {
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Request failed: ${errorData.message}`);
return response;
});
// 使用封装的 fetch 发送请求
httpClient.fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log('Data:', data))
.catch(error => console.error('Error:', error));
Cookie 携带
Fetch API 在默认情况下不会自动发送 Cookie,需要手动设置 credentials
选项。
credentials
选项的可选值:
'omit'
: 不发送 Cookie(默认值)'same-origin'
: 仅在同源请求中发送 Cookie'include'
: 无论同源或跨域请求均发送 Cookie
// fetchClient.ts
const fetchClient = async (url: string, options: RequestInit = {}) => {
const response = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
credentials: "include", // 发送请求时携带 Cookie
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
};
export default fetchClient;
数据转化
很遗憾,Fetch API 几乎不提供任何自动的数据转化选项,需要用户清楚地知道请求数据&返回数据是什么格式并对每个请求进行手动转化。就像上面的那个例子,必须将 JS 对象转成字符串之后才能发送数据。
它提供的手动转化 API 有下面几种:
- arrayBuffer()
- blob()
- json()
- text()
- formData()
当然,你也可以手动封装类似的自动转化的方法对数据进行自动配置。
跟踪请求进度
这个就相对比较复杂了。Axios 自带的那两个函数的底层实现是基于浏览器提供的 XMLHttpRequest
对象的 progress
事件。在这些事件中,浏览器会不断更新上传或下载的进度信息,Axios 通过绑定用户提供的回调函数,实时地将这些进度信息传递给开发者,从而实现对上传和下载进度的监控。
而 Fetch API,本身不提供对进度的监控信息,倒不如说这个监控特性就是 xhr 的特性之一。而 Fetch 和 XHR 是浏览器自带的两个不同 API,对应的职责也各不相同,因此 Fetch 本身是不支持请求进度监控的,如果想要实现,要么就对 Fetch API 进行手动分块请求(过于复杂),要么使用 xhr 来手动自己封装。在这里就暂时不赘述了,有兴趣的同学可以自己试试看?
并发请求
Fetch API 本身是基于 Promise,那么直接使用 Promise.all() 方法就可以实现对多个异步任务的同时发起,待所有任务结束后才结束。
Promise.all([
fetch("https://api.github.com/users/iliakan"),
fetch("https://api.github.com/users/taylorotwell"),
])
.then(async ([res1, res2]) => {
const a = await res1.json();
const b = await res2.json();
})
.catch((error) => {
console.log(error);
});
总结一下?
经过一系列的调研后,我觉得可以得出结论:
Axios 在各个方面都完全碾压 Fetch,Fetch 唯一的优点就是它是原生 API。
不过换个思路,我们日常的开发当中真的需要手动实现这么多的请求功能吗?我觉得也未必,重要的是需求。Axios 虽然开箱即用,但是作为一个第三方库,可能有的时候使用还是过于重量级。
另外,Fetch API 原生浏览器就支持,这也就意味着 **我们可以直接手动的在开发者工具发请求并查看效果。**这对于一些 GET 接口的测试还是比较方便的,毕竟只用调一下函数传个 URL 就可以测。
因此,我个人认为在相对比较大型、全栈的项目中可以使用 Axios 来对请求进行统一处理,毕竟它是 同构 的。在一些快速开发、轻量级的 Web 应用可以直接使用 Fetch。说实话,现在市面上主流的 Fetch 二次封装方案也比较成熟了,我觉得大家可以直接去借鉴一下,拿来直接用就行了。
顺便贴一下我自己一直在用的Axios 的二次封装方案,我觉得还是写的比较不错的,能够适配绝大多数的场景。Fetch API 的二次封装方案可能还得调研一下,弄个最佳实践。
转载自:https://juejin.cn/post/7400671870872616994