fetch 在实际项目中的思考
随着 fetch 的出现,可以让我们不在需要在项目中导入一些三方库来发送 http 请求。虽然浏览器中早已存在 XMLHttpRequest,但是由于它的写法比较繁琐,我们在使用的通常都是导入一些依赖库来更方便的使用它,比如 jquery 的 $.ajax()、angular 的 $http,以及最常用的 axios。但是 fetch 在实际项目中真的好用吗?
虽然我们确实可以通过 fetch 更容易的操作一些底层的操作,但是在实际项目中使用抽象的、简便的方法其实真的会比直接操作底层 API 会更省力。
错误处理
我们在学习 fetch 的基础写法会发现,它看起来非常像我们常用的依赖库的写法:
js
// 比如 axios 的写法
import axios from "axios";
axios
.get("http://localhost:3000/api")
.then((result) => console.log("success:", result))
.catch((error) => console.log("error:", error));
通过 fetch 改写可以写成如下:
js
fetch("http://localhost:3000/api")
.then((response) => response.json())
.then((result) => console.log("success:", result))
.catch((error) => console.log("error:", error));
看起来 fetch 只是多了一行将响应流解析成 JSON 格式的代码,看起来问题不是很大。
但是我们在认真看一下,或者实际上调用一下就会发现,它们是不一样的,前面提到的多种三方库在处理错误状态码(比如 404、500 这些)的时候都会当成是一个错误。但是 fetch 只会在网络异常(ip 解析异常、服务无法访问或者 cors)的时候才会拒绝这个 Promise,之后在 catch 中捕获。
这也就意味在上面的代码中,如果我们的响应状态码是 404,fetch 代码段还是会打印 success。如果我们想在服务端响应异常的时候返回一个拒绝状态,就需要额外添加一些代码:
js
fetch("http://localhost:3000/api")
.then((response) => {
return response.json().then((data) => {
if (response.ok) {
return data;
} else {
return Promise.reject({ status: response.status, data });
}
});
})
.then((result) => console.log("success:", result))
.catch((error) => console.log("error:", error));
ok 的值只在响应码处于 200 - 299 的范围内才返回 true
可能很多人会觉得这没什么,即便从服务端返回的是 404 这样的状态码,也是从服务端获取的数据,说明服务端确实成功响应了。这其实是一种观点,没有什么对错之分,只不过在我看来服务端返回的错误响应还是应该认为是一种异常,但是我们没办法修改 fetch 的规范,我们只是应该存在一种更好的抽象去表示。
POST 请求
另一种常用的场景就是发送 POST 请求,当我们使用 axios 去发送,只需要简单的一行:
js
axios.post('http://localhost:3000/user', {
name: 'leo'
})
但是在 fetch 中,我们却需要使用定义一堆东西,简单的设置 method、body 并不能成功请求,比如:
js
fetch("http://localhost:3000/user", {
method: "POST",
body: {
name: "leo",
},
})
这种简单写法发送给服务端的数据是异常的([object Object]
)。fetch 的 API 是非常显式的,对于发送的是 JSON 数据,我们必须把这个数据转成字符串,并添加 Content-Type: application/json
头来告诉服务端这个 payload 是 JSON,否则服务端会认为它是一个字符串。
改写如下可以正常使用:
js
fetch("http://localhost:3000/user", {
method: "POST",
headers: {
'Content-Type': 'application/json'
}
body: JSON.stringify({
name: "leo",
}),
})
假设我们有非常多类似的请求,那就要重复写这些配置。
fetch 默认设置
如前面所说,fetch 是一种非常显式的 API,如果我们不主动设置,就不能拿到任何东西。在实际项目中,可能存在以下问题:
- fetch 默认是不会发送 cookie 的,如果我们的项目中依赖 cookie 来做验证就需要手动进行配置。
- 需要手动设置 'Accept: application/json' 表明客户端可以处理 JSON 数据。
- fetch 默认是不支持 cors 的
所以我们在 fetch 请求中都需要设置如下:
js
fetch(url, {
credentials: "include",
mode: "cors",
headers: {
Accept: "application/json",
},
});
看起来多加点配置也没什么,但是如果我们的项目中每个请求都需要添加这些呢?fetch 并没有提供可以修改默认的方法。
axios 提供了这种修改默认配置的方法,非常简便:
js
axios.defaults.baseURL = 'http://localhost:3000';
axios.defaults.headers.common['Accept'] = 'application/json';
axios.defaults.headers.post['Content-Type'] = 'application/json';
axios 的目标是为了能简单的调用 api 发送请求。而 fetch 的目标更远大,并不是很适合我们目前的项目。
总结
如果我们不想要导入三方库来发送请求,意味着我们就不能通过下面这一行代码实现:
js
function addUser(details) {
return axios.post('http://localhost:3000/user', details);
}
而是需要封装一个这样的方法:
js
function addUser(details) {
return fetch('http://localhost:3000/user', {
mode: 'cors',
method: 'POST',
credentials: 'include',
body: JSON.stringify(details),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
}).then(response => {
return response.json().then(data => {
if (response.ok) {
return data;
} else {
return Promise.reject({status: response.status, data});
}
});
});
}
这个一看就知道在项目中是不应该存在大量这样的重复代码,我们可能需要将更多的场景考虑进来,然后进行一个封装。
那么当我们有另一个项目也需要这个封装函数的时候,是不是又要去修改、优化,来让这些 api 变得更简便、
适用。但是在这个过程中我们是不是又创建了一个发送请求的库,而不是直接使用 fetch 这个 api 呢?
当然,本文并不是在否认 fetch 是一个无用的设计,fetch 的底层设计非常好,比起 XMLHttpRequest,它能让我们对请求进行更细的控制。只不过在日常项目中这种底层 api 不是非常适用,直接使用封装好的工具可能是更好的选择。