最近在深入研究 TanStack Query
在项目中的各种高阶用法,发现结合 Fetch API
封装接口请求方法也还挺好用的,它不仅是原生的高性能 API
,还能避免引入一个库(通常是 Axios
)。而且好多主流的网站实际上都使用了 Fetch API
:
然后就想着深入学习一下前端中常见的一些网络资源请求相关API和工具之间的演进和区别。
先抛出一个问题,浏览器中的 http请求
有哪些方式?有这些:
URL
Links
JavaScript(window.location.href = 'http://www.google.com'
)XMLHttpRequest (XHR)
Fetch API
Axios
WebSocket
在实际开发中,XHR
和 Fetch API
是发送异步 HTTP
请求的两种主要方式,在使用它们时需要注意跨域问题和安全问题。Axios
提供了更多易于使用的功能和选项,使得发送 HTTP
请求更加方便和灵活。WebSocket
则适用于需要实现实时双向通信的应用场景。
日常开发更多可能接触到的是Ajax
、Fetch API
、Axios
三种:
Ajax
:是一种早期的用于在浏览器中发送异步HTTP
请求的技术。Ajax
通过XMLHttpRequest
对象来发送请求,并通过回调函数处理响应数据。Ajax
的优点是简单易用,缺点是需要手动编写大量的回调函数来处理请求和响应,代码可读性较差。Fetch API
:是一个新的JavaScript API
,用于在浏览器中发送异步HTTP
请求。Fetch
使用Promise
对象来处理请求和响应,支持链式调用和异步处理,代码可读性较好。Fetch
的优点是支持跨域请求、使用标准的Promise API
、返回的是Response
对象,可以使用各种方法处理响应数据。缺点是不兼容低版本的浏览器,需要使用polyfill
来解决。Axios
:是一个流行的JavaScript HTTP
客户端库,用于在浏览器和Node.js
环境中发送HTTP
请求。Axios
使用Promise
对象来处理请求和响应,支持链式调用和异步处理,代码可读性较好。Axios
的优点是具有丰富的功能和选项,如请求取消、拦截请求和响应、转换请求和响应数据等。缺点是需要手动引入库文件,增加了代码量和体积。
接下来,我们就详细总结主要的三种方式,一次性彻底搞懂它们之间的区别。
1、Ajax
1.1 什么是 Ajax
Asynchronous JavaScript + XML
(异步 JavaScript 和 XML), 其本身不是一种新技术,而是一个在 2005 年被Jesse James Garrett
提出的新术语,用来描述一种使用现有技术集合的‘新’方法,包括:HTML 或 XHTML, CSS, JavaScript, DOM, XML (en-US), XSLT, 以及最重要的XMLHttpRequest
。当使用结合了这些技术的 AJAX 模型以后,网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面。这使得程序能够更快地回应用户的操作。
尽管 X 在 Ajax 中代表 XML,但由于JSON的许多优势,比如更加轻量以及作为 Javascript 的一部分,目前 JSON 的使用比 XML 更加普遍。JSON 和 XML 都被用于在 Ajax 模型中打包信息。
1.2 Ajax 基本使用
1.2.1 发出 HTTP 请求
要使用 JavaScript 向服务器发出 HTTP 请求,你需要一个具有必要功能的对象实例 XMLHttpRequest
:
const httpRequest = new XMLHttpRequest();
发出请求后,你将收到响应。在此阶段,你需要XMLHttpRequest
通过将对象的属性设置为请求更改状态时调用的函数来告诉对象哪个 JavaScript 函数将处理响应onreadystatechange
,如下所示:
function handler() { // ... } httpRequest.onreadystatechange = handler;
请注意:函数名称后没有括号或参数,因为你是在为函数分配一个引用,而不是实际调用它。或者,你可以使用动态定义函数的 JavaScript 技术(称为“匿名函数”)来定义将处理响应的操作,而不是提供函数名称,如下所示:
httpRequest.onreadystatechange = () => { // ... };
接下来,在声明收到响应时会发生什么之后,你需要通过调用HTTP 请求对象的open()
和方法来实际发出请求,如下所示:send()
httpRequest.open("GET", "http://www.example.org/some.file", true); httpRequest.send();
- 调用
open()
的第一个参数是HTTP请求方法 ——GET
、POST
、HEAD
,或其他你的服务器支持的方法。根据HTTP标准,保持该方法的全大写,否则一些浏览器(如Firefox
)可能无法处理该请求。关于可能的HTTP
请求方法的更多信息,请查看 规范。
- 第二个参数是你要发送请求的URL。作为一项安全
feature
,你默认不能调用第三方域名的URL。请确保在你所有的页面上使用准确的域名,否则当你调用open()
时,你会得到一个 "权限拒绝 "的错误。一个常见的陷阱是用domain.tld
访问你的网站,但试图用www.domain.tld
来调用页面。如果你真的需要向另一个域名发送请求,请参阅 HTTP 访问控制 (CORS)。 - 可选的第三个参数设置请求是否异步。如果是(默认)
true
,JavaScript 将继续执行,并且用户可以在服务器响应尚未到达时与页面进行交互。这是AJAX
中的第一个 A。
send()
方法的参数可以是任何你想发送给服务器的数据,如果是 POST-ing
请求。表单数据应该以服务器可以解析的格式发送,如查询字符串。
"name=value&anothername="+encodeURIComponent(myVar)+"&so=on"
或其他格式,如multipart/form-data
、JSON
、XML
等。
请注:如果你想要
POST
数据,你可能必须设置请求的 MIME 类型。例如,在调用send()
作为查询字符串发送的表单数据之前使用以下内容:
httpRequest.setRequestHeader( "Content-Type", "application/x-www-form-urlencoded" );
1.2.2 处理服务器响应
当你发送请求时,您提供了一个 JavaScript
函数的名称来处理响应:
httpRequest.onreadystatechange = nameOfTheFunction;
这个功能应该做什么?首先,函数需要检查请求的状态。如果 state
的值为XMLHttpRequest.DONE
(对应4),表示已经收到完整的服务器响应,可以继续处理了。
if (httpRequest.readyState === XMLHttpRequest.DONE) { // 处理响应 } else { // 处理异常 }
值的完整列表readyState
记录在XMLHTTPRequest.readyState中,如下所示:
- 0(
uninitialized
)- 请求未初始化 - 1(
loading
)- 建立服务器连接 - 2(
loaded
)- 收到请求 - 3(
interactive
)- 处理请求 - 4(
complete
)- 请求完成且响应准备就绪
接下来,检查HTTP 响应的 HTTP 响应状态代码。在以下示例中,我们通过检查响应代码来区分成功和不成功的 AJAX 调用200 OK
if (httpRequest.status === 200) { // 请求成功~ } else { // 请求出错~ }
检查请求的状态和响应的 HTTP
状态代码后,你可以对服务器发送的数据做任何您想做的事情。你有两个选项来访问该数据:
httpRequest.responseText
– 将服务器响应作为文本字符串返回httpRequest.responseXML
– 将响应作为一个XMLDocument
对象返回,你可以使用JavaScript DOM
函数遍历
请注意,上述步骤仅在你使用异步请求时有效( 的第三个参数
open()
未指定或设置为true
)。如果你使用同步请求,则无需指定函数,但强烈建议不要这样做,因为它会带来糟糕的用户体验。
1.2.3 一个完整的例子
<!DOCTYPE html> <html lang="en"> <head> <title>Home</title> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width" /> </head> <body> <button id="ajaxButton" type="button">Make a request</button> </body> <script> (() => { let httpRequest; document.getElementById('ajaxButton').addEventListener('click', makeRequest); function makeRequest() { // 1. 创建XMLHttpRequest对象 httpRequest = new XMLHttpRequest(); // 2. 检查是否成功创建XMLHttpRequest对象,如果失败,则显示错误消息并返回false if (!httpRequest) { alert('Giving up :( Cannot create an XMLHTTP instance'); return false; } // 3. 设置XMLHttpRequest对象的回调函数,当XMLHttpRequest对象的状态发生变化时,将调用该函数 httpRequest.onreadystatechange = alertContents; // 4. 初始化XMLHttpRequest对象,指定HTTP请求的类型和URL httpRequest.open('GET', 'https://fakestoreapi.com/products/1'); // 5. 发送HTTP请求 httpRequest.send(); } /* 如果发生通信错误(例如服务器宕机),则在 onreadystatechange 访问响应状态时方法中会抛出异常。 为了缓解这个问题,可以将 if...else 语句包装在 try...catch。 */ function alertContents() { try { if (httpRequest.readyState === XMLHttpRequest.DONE) { if (httpRequest.status === 200) { alert(httpRequest.responseText); } else { alert('There was a problem with the request.'); } } } catch (e) { alert(`Caught Exception: ${e.description}`); } } })(); </script> </html>
在这个例子中:
- 用户点击“提出请求”按钮;
- 事件处理程序调用
makeRequest()
函数; - 发出请求,然后 (
onreadystatechange
) 将执行传递给alertContents()
; alertContents()
检查是否收到响应并确定,然后alert()
是接口的内容。
注意1: 如果您不设置标头,
Cache-Control: no-cache
浏览器将缓存响应并且永远不会重新提交请求,从而使调试变得困难。您还可以添加一个始终不同的 GET 参数,例如时间戳或随机数(参阅 绕过缓存)注意2: 如果
httpRequest
全局使用该变量,则竞态函数调用makeRequest()
会相互覆盖,从而导致竞态条件。将httpRequest
变量声明为包含 AJAX 函数的 闭包可以避免这种情况
1.3 Asynchronous JS
Asynchronous JS
:指的是 JavaScript 以非阻塞方式运行的能力。
想象一下,如果每个需要时间给我们响应的网络请求都阻止了任何其他操作的执行?整个互联网将处于停滞状态。为处理异步代码而开发的初始方法是使用回调来提供一个在请求被 resolve
后运行的函数。
以下代码片段是用于处理异步 downloadPhoto
函数结果的回调示例:
downloadPhoto('http://coolcats.com/cat.gif', handlePhoto) funcion handlePhoto(error, photo) { if (error) { console.error('Download error!', error) } else { console.log('Download finished', photo) } } console.log('Download started')
虽然回调很重要,但它们可能会导致所谓的回调地狱
fs.readdir(source, function (err, files) { if (err) { console.log('Error finding files: ' + err) } else { files.forEach(function (filename, fileIndex) { console.log(filename) gm(source + filename).size(function (err, values) { if (err) { console.log('Error identifying file size: ' + err) } else { console.log(filename + ' : ' + values) aspect = (values.width / values.height) widths.forEach(function (width, widthIndex) { height = Math.round(width / aspect) console.log('resizing ' + filename + 'to ' + height + 'x' + height) this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) { if (err) console.log('Error writing file: ' + err) }) }.bind(this)) } }) }) } })
为了更好地理解正确的异步回调用法,有一个很好的网站叫 callbackhell.com,它很好地介绍了组成异步回调函数的最佳实践,避免了可怕的 "回调地狱"。
1.4 Ajax 的优缺点
Ajax(Asynchronous JavaScript and XML)
是一种用于在 Web
页面中实现异步通信的技术,通过在页面不刷新的情况下,使得用户可以与服务器进行数据交互。以下是 Ajax
的优缺点:
优点:
- 减少页面刷新:
Ajax
可以在页面不刷新的情况下获取和显示数据,减少了用户等待时间和流量消耗。 - 提高用户体验:由于
Ajax
可以实现异步请求和响应,使得用户可以在不中断操作的情况下获取数据,从而提高了用户的体验。 - 减轻服务器压力:由于
Ajax
可以部分更新页面,减少了服务器处理请求的次数,从而减轻了服务器的压力。 - 支持多种数据格式:
Ajax
可以支持多种数据格式,如XML
、JSON
等,使得数据的传输和处理更加灵活。
缺点:
- 对
SEO
不友好:由于Ajax
的异步请求不会刷新整个页面,搜索引擎很难获取Ajax
加载的数据,从而降低了网站的SEO
优化效果。 - 安全性问题:
Ajax
可能会导致跨站点脚本攻击(Cross-site scripting, XSS)
和跨站点请求伪造(Cross-site request forgery, CSRF)
等安全问题,开发人员需要采取相应的安全措施。 - 开发复杂度高:
Ajax
的开发需要涉及到多个技术领域,包括HTML
、CSS
、JavaScript
、XML
或JSON
等,开发人员需要具备多方面的技能,开发难度较大。
2、Fetch API
2.1 什么是 Fetch?
Fetch API
ES6 之后出现的基于 Promise
的一个强大而灵活的JavaScript库,是一个现代的网络请求API,可以使客户端与服务器之间的通信变得更加容易和直观。它提供了一种简单的方法来发送和接收数据,并支持各种HTTP请求和响应类型。
在使用Fetch API时,请注意其异步性质,并确保正确处理响应和错误。
以下是使用Fetch API发送GET请求的示例代码:
fetch('https://example.com/data.json') .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error(error));
在这个例子中,我们向https://example.com/data.json
发送了一个GET请求,并在响应中获得JSON数据。使用.then()
方法来处理响应数据,使用.catch()
方法来处理任何错误。
2.1.1 发送GET请求
要发送GET请求,只需调用 fetch()
函数并传递URL作为参数即可。fetch()
返回一个 Promise
对象,该对象将在响应可用时解析为 Response
对象。
fetch('https://example.com/data.json') .then(response => console.log(response)) .catch(error => console.error(error));
在上面的例子中,我们使用 console.log()
方法输出 Response
对象。如果请求成功,该对象将包含有关响应的信息(如状态代码和头信息)。如果请求失败,则 catch()
方法将被调用。
2.1.2 发送POST请求
要发送 POST
请求,需要创建一个包含请求选项的对象,并将其作为fetch()
函数的第二个参数传递。请求选项对象应包含请求的方法、标题、正文等信息。
const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'JohnDoe', password: 'mySecretPassword' }) }; fetch('https://example.com/login', requestOptions) .then(response => console.log(response)) .catch(error => console.error(error));
在这个例子中,我们向https://example.com/login
发送一个POST请求,正文为一个JSON字符串。请注意,headers属性
必须设置为包含 Content-Type
标头的对象,以指示请求正文的类型。
2.1.3 处理响应数据
要处理响应数据,可以使用 Response对象
提供的方法。例如,使用.json()
方法将响应正文解析为 JSON格式
的数据:
fetch('https://example.com/data.json') .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error(error));
在上面的例子中,我们使用.json()
方法将响应正文解析为JSON格式的数据,并使用 console.log()
方法输出解析后的数据。
2.1.4 处理错误
如果请求失败,catch()
方法将被调用。您可以在 catch()
方法中处理错误,并使用 console.error()
方法将其输出到控制台。
fetch('https://example.com/data.json') .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error(error));
在上面的例子中,我们使用 console.error()
方法输出错误信息。
2.1.5 处理HTTP错误
如果服务器返回HTTP错误状态代码(例如404或500),fetch()
方法不会引发错误。相反,它会将响应对象传递给 then()
方法。您可以使用 Response
对象的属性来确定响应的状态代码,并相应地处理响应。
fetch('https://example.com/data.json') .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => console.log(data)) .catch(error => console.error(error));
在上面的例子中,我们使用 Response对象
的.ok属性
来检查响应的状态代码是否成功(即为200-299之间的数字)。如果不是,我们将抛出一个错误,以使 catch()
方法被调用。
2.1.6 自定义请求头
您可以使用 Headers对象
设置自定义请求头。例如,您可以设置 Authorization
标头以进行身份验证。
const myHeaders = new Headers(); myHeaders.append('Authorization', 'Bearer myToken'); fetch('https://example.com/data', { headers: myHeaders }) .then(response => console.log(response)) .catch(error => console.error(error));
在这个例子中,我们创建了一个包含 Authorization标头
的 Headers对象
,并将其传递给 fetch()
函数的请求选项。
2.1.7 使用async/await语法
Fetch API
也可以使用 async/await
语法进行异步请求。下面是一个使用 async/await
语法的示例。
async function fetchData() { try { const response = await fetch('https://example.com/data.json'); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } } fetchData();
在这个例子中,我们定义了一个异步函数 fetchData()
,它使用 async
关键字进行标记。在函数中,我们使用await关键字来等待fetch()
函数的响应,并将其存储在 response
变量中。然后,我们使用await关键字等待将响应解析为JSON的过程,并将结果存储在data变量中。
使用 async/await
语法可以使代码更加简洁和易于理解,特别是对于异步请求和响应的处理。
2.2 Headers
Fetch APIHeaders
的接口允许您对HTTP 请求和响应标头执行各种操作。这些操作包括从请求的标头列表中检索、设置、添加和删除标头。
一个Headers
对象有一个关联的头列表,它最初是空的,由零个或多个名称和值对组成。您可以使用类似的方法添加到此append()
(请参阅示例。)在该接口的所有方法中,标头名称通过不区分大小写的字节序列进行匹配。
出于安全原因,某些标头只能由用户代理控制。这些标头包括禁止的标头名称和禁止的响应标头名称。
Headers 对象也有一个关联的守卫,它的值是immutable
, request
, request-no-cors
, response
, 或none
。这会影响set()
、delete()
和append()
方法是否会改变标头。有关详细信息,请参阅守卫。
您可以Headers
通过Request.headers
和Response.headers
属性检索对象,并Headers
使用Headers()
构造函数创建新对象。
一个对象实现Headers
可以直接在结构中使用for...of
,而不是entries()
:for (const p of myHeaders)
等同于for (const p of myHeaders.entries())
.
Fetch API
中的 Headers对象
表示请求或响应的头信息,包含了键值对的集合,可以通过该对象进行添加、修改、删除头信息。
下面是一个简单的例子,介绍了如何使用 Headers对象
:
const headers = new Headers(); headers.append('Content-Type', 'application/json'); headers.append('Authorization', 'Bearer my-token');
首先,我们创建一个新的 Headers对象
。然后,使用 append()
方法向 Headers对象
中添加两个键值对。第一个键值对的键是"Content-Type"
,值是"application/json"
。第二个键值对的键是"Authorization"
,值是"Bearer my-token"
。
Headers对象
的常用方法包括:
- Headers.append():将新值附加到对象内的现有标头上
Headers
,或者添加标头(如果尚不存在)。 - Headers.delete():从对象中删除标头
Headers
。 - Headers.entries():返回
iterator
允许遍历此对象中包含的所有键/值对。 - Headers.forEach():为此对象中的每个键/值对执行一次提供的函数
Headers
。 - Headers.get():返回具有给定名称的对象
String
中标头的所有值的序列。Headers
- Headers.has():返回一个布尔值,说明对象是否
Headers
包含某个标头。 - Headers.keys():返回一个
iterator
允许您遍历此对象中包含的键/值对的所有键。 - Headers.set():为对象内的现有标头设置新值
Headers
,或者添加标头(如果尚不存在)。 - Headers.values():返回一个
iterator
允许您遍历此对象中包含的键/值对的所有值。
注意1:
Headers.set()
要清楚,和之间的区别Headers.append()
在于,如果指定的标头已经存在并且确实接受多个值,Headers.set()
则会用新值覆盖现有值,而Headers.append()
会将新值附加到值集的末尾。请参阅他们的专用页面以获取示例代码。注意2:
TypeError
如果您尝试传入对不是有效 HTTP 标头名称的名称的引用,则所有标头方法都将抛出异常。TypeError
如果标头具有不可变的Guard ,则变异操作将抛出一个。在任何其他失败情况下,他们都会默默地失败。注意3: 当 Header 值被迭代时,它们会自动按字典顺序排序,并且来自重复 header 名称的值被合并。
Headers对象
还支持使用迭代器遍历其中的键值对。例如:
for (const [name, value] of headers) { console.log(`${name}: ${value}`); }
以上代码将遍历 headers对象
中的所有键值对,并将它们打印到控制台。
Headers对象
常用于 fetch API
中的请求和响应中,例如:
const response = await fetch('https://example.com', { headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer my-token' } });
在 fetch API 中,可以通过传递一个包含 headers对象 的配置对象来设置请求或响应中的头信息。在以上代码中,我们向 fetch() 方法传递了一个包含两个键值对的 headers对象,这些键值对将被添加到发送给服务器的请求头中。
除了在 fetch API 的请求和响应中使用 Headers对象 之外,它还可以用于其他HTTP请求和响应场景。以下是一个使用 Headers对象 的HTTP请求的示例代码:
const headers = new Headers({ 'Content-Type': 'application/json', 'Authorization': 'Bearer my-token' }); fetch('https://example.com/api/data', { method: 'POST', headers: headers, body: JSON.stringify({ data: 'example data' }) }) .then(response => { console.log(response.status); });
在以上代码中,我们首先创建了一个新的 Headers对象
,并使用构造函数中的参数传递了两个键值对。接下来,我们使用 fetch()
方法向服务器发送了一个POST请求,并通过 headers属性
将 Headers对象
传递给了 fetch()
方法。最后,我们将一个JSON对象序列化为字符串,并将其作为请求体发送给了服务器。
此外,Headers对象
还支持类似数组的操作,例如使用数组下标访问Headers对象
中的键值对。例如:
const headers = new Headers({ 'Content-Type': 'application/json', 'Authorization': 'Bearer my-token' }); console.log(headers['Content-Type']); // 输出: "application/json"
需要注意的是,在 Headers对象
中,键值对的键是不区分大小写的。因此,使用 headers.get()
方法获取键值对的值时,可以传入大小写不敏感的键名。例如:
const headers = new Headers({ 'Content-Type': 'application/json', 'Authorization': 'Bearer my-token' }); // or, using an array of arrays: myHeaders = new Headers([ ['Content-Type', 'application/json'], ['Authorization', 'Bearer my-token'] ]); console.log(headers.get('content-type')); // 输出: "application/json"
总之,Headers对象
提供了一种方便的方法来处理 HTTP请求
和响应中的头信息。通过使用 Headers对象
,我们可以轻松地添加、修改和删除请求或响应的头信息,并可以使用类似数组的操作和迭代器遍历来操作 Headers对象
中的键值对。