1. 引言
Fetch API 是现代浏览器内置的网络请求接口,专为取代传统的 XMLHttpRequest
而设计。它基于 Promise
设计,大幅简化了异步 HTTP 请求写法,支持 async/await
语法,使得异步代码更易读和维护。
XMLHttpRequest
:传统 AJAX 技术的底层 APIPromise
:异步操作结果的占位符对象,通过.then()
链式调用,将嵌套结构扁平化async/await
:Promise 的语法糖,将异步代码写出同步代码的形式,极大提升可读性与维护性
2. Fetch API vs XHR
使用 Fetch API 向 JSONPlaceholder POST 数据:
JavaScript
1function postWithFetch() {
2 const url = 'https://jsonplaceholder.typicode.com/posts';
3
4 const params = {
5 title: '现代 API 测试',
6 body: '用 Fetch API 发送 POST 请求',
7 userId: 10
8 };
9
10 fetch(url, {
11 method: 'POST',
12 headers: {
13 // 设置请求头:声明本次请求体的数据格式为 JSON
14 'Content-Type': 'application/json'
15 },
16 // 发送数据:对象需序列化为 JSON 字符串
17 body: JSON.stringify(params)
18 })
19 .then(res => {
20 // 检查响应状态码
21 if (!res.ok) {
22 // HTTP Response.ok:响应的状态是否在 200-299 之间
23 // 创建错误对象并抛出,便于下一个 catch 捕获
24 throw new Error(`状态码: ${res.status}`);
25 }
26
27 // 返回解析后的 JSON 数据
28 return res.json();
29 })
30 .then(data => {
31 // 处理解析后的 JSON 数据
32 console.log('Fetch API 返回: ', data);
33 })
34 .catch(err => {
35 // 仅捕获主动 throw 或网络级异常
36 console.error('Fetch API 请求失败: ', err.message);
37 });
38}
39
40// 调用
41postWithFetch();
使用 async/await
语法改写 Fetch API 请求,使代码更简洁易读:
JavaScript
1async function postWithFetchAsync() {
2 const url = 'https://jsonplaceholder.typicode.com/posts';
3
4 const params = {
5 title: '现代 API 测试',
6 body: '用 Fetch API(async/await)发送 POST 请求',
7 userId: 10
8 };
9
10 try {
11 const res = await fetch(url, {
12 method: 'POST',
13 headers: {
14 // 设置请求头:声明本次请求体的数据格式为 JSON
15 'Content-Type': 'application/json'
16 },
17 // 发送数据:对象需序列化为 JSON 字符串
18 body: JSON.stringify(params)
19 });
20
21 // 检查响应状态码
22 if (!res.ok) {
23 // HTTP Response.ok:响应的状态是否在 200-299 之间
24 // 创建错误对象并抛出,便于下一个 catch 捕获
25 throw new Error(`状态码: ${res.status}`);
26 }
27
28 // 解析 JSON 数据
29 const data = await res.json();
30 console.log('Fetch API 返回: ', data);
31 } catch (err) {
32 // 仅捕获主动 throw 或网络级异常
33 console.error('Fetch API 请求失败: ', err.message);
34 }
35}
36
37// 调用
38postWithFetchAsync();
使用 XMLHttpRequest
(XHR)传统方法 POST 数据:
JavaScript
1function postWithXhr() {
2 const url = 'https://jsonplaceholder.typicode.com/posts';
3
4 const xhr = new XMLHttpRequest();
5 xhr.open('POST', url);
6
7 // 可设置跨域时携带 Cookie
8 // 🚩 注意:同源请求下可自动携带 Cookie,跨域则依赖 `withCredentials`
9 // 🚩 只有在 `open()` 后、`send()` 前设置才有效
10 // xhr.withCredentials = true;
11
12 // 设置请求头:声明本次请求体的数据格式为 JSON
13 xhr.setRequestHeader('Content-Type', 'application/json');
14
15 // 响应处理
16 // onload:无论状态码是不是 2xx 都会触发
17 xhr.onload = function () {
18 // 🚩 检查 HTTP 状态码是否为成功(2xx)
19 if (xhr.status >= 200 && xhr.status < 300) {
20 // 解析返回 JSON 数据
21 const data = JSON.parse(xhr.responseText);
22 console.log('XHR 返回: ', data);
23 } else {
24 // ⚠️ 非 2xx 状态时也会进来 onload
25 console.error('XHR 请求失败, 状态码: ', xhr.status);
26 }
27 };
28
29 // onerror:只在网络层出错才会触发(如断网/服务不可达/跨域失败等)
30 xhr.onerror = function () {
31 // ⚠️ 只有网络错误才会触发本回调
32 console.error('XHR 网络异常或被中断');
33 };
34
35 const params = {
36 title: '传统 XHR 测试',
37 body: '用 XMLHttpRequest 发送 POST 请求',
38 userId: 20
39 };
40
41 // 发送数据:对象需序列化为 JSON 字符串
42 xhr.send(JSON.stringify(params));
43}
44
45// 调用
46postWithXhr();
3. Fetch API 语法
Fetch API 完整语法示例(包含常用 options 和返回值处理):
JavaScript
1fetch('https://api.wuxianjie.net/v1/no', {
2 // ===== 常用 options 配置 =====
3 method: 'POST', // 请求方法:'GET'(默认)、'POST'、'PUT'、'DELETE' 等
4 headers: {
5 'Content-Type': 'application/json', // 请求头,指定数据格式为 JSON
6 'Authorization': 'Bearer YOUR_TOKEN' // 可选,添加鉴权信息
7 },
8 // 请求体,通常为字符串化的 JSON
9 body: JSON.stringify({name: 'wxj', age: 18}),
10 // 可设置跨域时携带 Cookie,默认仅在同源请求(same-origin)下自动携带
11 credentials: 'include'
12})
13.then(response => {
14 // ===== 返回值处理(Response 对象)=====
15 if (!response.ok) {
16 // HTTP Response.ok:响应的状态是否在 200-299 之间
17 // ⚠️️ HTTP 状态非 2xx 时不会进入 catch,需要手动抛错
18 throw new Error(`HTTP 错误, 状态码: ${response.status}`);
19 }
20
21 // 解析为不同数据格式
22 // ⚠️️ 无需 `return await response.json()`,因为后续的 `.then` 会自动展开(flatten)Promise 并继续链式传递
23 return response.json(); // 常用:解析为 JSON 对象
24 // return response.text(); // 可选:解析为纯文本
25 // return response.blob(); // 可选:解析为二进制数据
26})
27.then(data => {
28 // data 是解析后的响应数据
29 // ⚠️️ Promise 链的 `.then` 可以自动把返回的 Promise「展平」,把最终的结果传下去(即 promise resolution)
30 console.log('响应数据: ', data);
31})
32.catch(error => {
33 // ⚠️️ 仅捕获网络错误或上面 throw 抛出的异常
34 console.error('请求异常: ', error);
35})
36.finally(() => {
37 // 无论成功或失败都会执行
38 console.log('请求结束');
39});
- 在 Promise 链(即
.then
链)中,每个.then
接收到上一个.then
返回的值 - 如果上一个
.then
返回的是 Promise(比如response.json()
),则下一个.then
会在该 Promiseresolve
后执行,并接收到 resolve 的结果
🌟 使用 async/await
可以让异步 fetch 代码更简洁、易读、便于异常处理,逻辑更清晰!
JavaScript
1// 定义异步函数,便于调用 await
2async function postData() {
3 try {
4 const response = await fetch('https://api.wuxianjie.net/v1/no', {
5 // ===== 常用 options 配置 =====
6 method: 'POST', // 请求方法:'GET'(默认)、'POST'、'PUT'、'DELETE' 等
7 headers: {
8 'Content-Type': 'application/json', // 请求头,指定数据格式为 JSON
9 'Authorization': 'Bearer YOUR_TOKEN' // 可选,添加鉴权信息
10 },
11 // 请求体,通常为字符串化的 JSON
12 body: JSON.stringify({name: 'wxj', age: 18}),
13 // 可设置跨域时携带 Cookie,默认仅在同源请求(same-origin)下自动携带
14 credentials: 'include'
15 });
16
17 // ===== 返回值处理(Response 对象)=====
18 if (!response.ok) {
19 // HTTP Response.ok:响应的状态是否在 200-299 之间
20 // ⚠️️ HTTP 状态非 2xx 时不会进入 catch,需要手动抛错
21 throw new Error(`HTTP 错误, 状态码: ${response.status}`);
22 }
23
24 // 解析为不同数据格式
25 // ⚠️️ 必需 `await response.json()`,因为需要等待解析完成
26 const data = await response.json(); // 常用:解析为 JSON 对象
27 // const data = await response.text(); // 可选:解析为纯文本
28 // const data = await response.blob(); // 可选:解析为二进制数据
29
30 // data 是解析后的响应数据
31 console.log('响应数据: ', data);
32 } catch (error) {
33 // ⚠️️ 仅捕获网络错误或上面 throw 抛出的异常
34 console.error('请求异常: ', error);
35 } finally {
36 // 无论成功或失败都会执行
37 console.log('请求结束');
38 }
39}
40
41// 调用异步函数
42postData();
上面两种写法功能上效果一致,await
只是实现 .then
的「语法糖」。本质区别:
.then
是以回调的形式对 Promise 结果做处理,支持链式多步操作await
是暂停(阻塞)当前async
函数后续代码、等 Promise 结果出来再继续运行,书写更清晰,异常更好捕捉
4. 注意事项
4.1 Why if (!res.ok)
Fetch API 与传统 XHR 错误处理方式基本一致
- 只有在「网络异常」时,Fetch 的 Promise 才会被
reject
(拒绝),catch
/onerror
才会触发- 🚫 网络连接失败(如断网、服务器不可达、DNS 解析失败等)
- 🚫 CORS(Cross-Origin Resource Sharing,跨域资源共享)策略拦截
- 🚫 主动终止请求(如调用
AbortController.abort
) - 此时抛出的通常是
TypeError
(类型错误)
- 但如果只是 HTTP 错误(如 404、500),Promise 依然会
resolve
(解决),只是response.ok === false
,需要手动判断状态码做相应处理
Fetch/XHR 与 Axios 的最大不同
- Axios 会自动将 HTTP 状态码非 2xx 的响应作为错误(reject promise,自动进入 catch),无需手动判断
- Axios 的 error 对象内部还包含了丰富的错误信息(如
error.response
、error.request
、error.message
),便于精细化处理
Fetch API 错误处理写法:
JavaScript
1async function fetchData() {
2 try {
3 const res = await fetch('https://api.wuxianjie.net/v1/no');
4 // 🌟 必须手动检查 HTTP 状态码
5 if (!res.ok) {
6 throw new Error(`HTTP 错误! 状态码: ${res.status}`); // 可自定义错误信息
7 }
8 const data = await res.json();
9 console.log('数据: ', data);
10 } catch (error) {
11 // 只有网络层异常才会走这里,如断网/CORS
12 console.error('Fetch 异常: ', error);
13 }
14}
15
16// 调用函数
17fetchData();
Axios 错误处理(以请求 DummyJSON API 为例)写法:
HTML
1<html>
2 <body>
3 <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
4 <script>
5 async function fetchDataAxios() {
6 try {
7 // 不存在的 URL
8 // const res = await axios.get('https://api.wuxianjie.net/v1/no');
9
10 // 模拟响应 400 错误
11 const res = await axios.get('https://dummyjson.com/http/400');
12 console.log('数据: ', res.data);
13 } catch (err) {
14 // HTTP 错误(如 404,500)也会自动走这里
15 if (err.response) {
16 // 服务器响应了,且状态码非 2xx
17 console.error(
18 'Axios HTTP 错误: ',
19 err.response.status,
20 err.response.data
21 );
22 } else if (err.request) {
23 // 请求已发出但没有收到响应,当访问不存在的 URL 时会触发
24 console.error('未收到响应: ', err.request);
25 } else {
26 // 配置等其它错误
27 console.error('Axios 异常: ', err.message);
28 }
29 }
30 }
31
32 // 调用函数
33 fetchDataAxios();
34 </script>
35 </body>
36</html>
4.2 Why await response.json()
fetch(url)
返回一个 Promise,解析后得到 Response 对象response.json()
等「读取响应体内容(如.json()
、.text()
、.blob()
)」也是异步操作,同样返回 Promise- 只有「先
await fetch
,再await response.json
」才能确保最终拿到的是真正的 JSON 数据,而不是 Promise 对象
常见困惑
- 误以为 fetch 后的 response 已经拿到了内容,其实只是响应头和状态,body(主体)还未解析
- 若省略
await
,则在执行console.log(response.json())
,看到的永远是Promise{<pending>}
JavaScript
1// 封装 API 调用
2async function fetchVersion() {
3 const res = await fetch('http://localhost:8890/wxj/api/v1/public/version', {
4 method: 'GET',
5 headers: {
6 'Accept-Language': 'zh'
7 }
8 });
9
10 if (!res.ok) {
11 const errData = await res.json();
12 throw new Error(`${errData.error || JSON.stringify(errData)}`);
13 }
14
15 // ‼️ 这里未使用 `await`,而是直接获取 `res.json()` 的结果
16 const data = res.json();
17
18 // 输出的是 `Promise{<pending>}`,因为此时 `data` 只是还未完成的 `Promise`
19 console.log(data);
20
21 return data;
22}
23
24try {
25 // ❓ 为何最终可以获取 API 结果
26 // 此处 await 已是真正等待 Promise 完成,故拿到实际数据
27 const versionData = await fetchVersion();
28 console.log('API 调用成功: ', versionData);
29} catch (err) {
30 console.error('API 调用失败: ', err.message);
31}
5. TS 最佳实践
TypeScript
1// HTTP JSON API 工具函数:封装 fetch 请求,自动处理 JSON 响应和错误
2async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
3 let res: Response;
4 try {
5 res = await fetch(url, options);
6 } catch (err) {
7 // 捕获网络异常(如 DNS、连接超时等)
8 throw new Error(`[网络错误] 请求失败: ${url}, 原因为: ${(err as Error).message}`);
9 }
10
11 let data: any;
12 try {
13 data = await res.json();
14 } catch {
15 // 若解析 JSON 失败,则尝试读取 text
16 const text = await res.text();
17 throw new Error(`[响应解析异常] 非标准 JSON 响应: ${text}`);
18 }
19
20 if (!res.ok) {
21 throw new Error(`[服务端错误] ${res.status} ${res.statusText} => ${(data && (data.error || JSON.stringify(data)))}, 请求路径: ${url}`);
22 }
23
24 return data as T;
25}
26
27// ===== 用法演示 =====
28type VersionApi = {
29 version: string;
30 description: string;
31 author: string;
32};
33
34async function demo() {
35 // 不带请求头
36 fetchJson<VersionApi>('http://localhost:8890/wxj/api/v1/public/version')
37 .then(data => {
38 console.log('[成功] 不指定请求头获取数据:', data);
39 })
40 .catch(err => {
41 console.error('[失败] 不指定请求头获取数据:', err.message);
42 });
43
44 // 带 Accept-Language 头
45 fetchJson<VersionApi>('http://localhost:8890/wxj/api/v1/public/version', {
46 headers: {'Accept-Language': 'zh'}
47 })
48 .then(data => {
49 console.log('[成功] 指定请求头获取数据:', data);
50 })
51 .catch(err => {
52 console.error('[失败] 指定请求头获取数据:', err.message);
53 });
54}
55
56demo();