首页 实用干货 🌋

最近在深入研究 TanStack Query在项目中的各种高阶用法,发现结合 Fetch API 封装接口请求方法也还挺好用的,它不仅是原生的高性能 API,还能避免引入一个库(通常是 Axios)。而且好多主流的网站实际上都使用了 Fetch API

image.png

然后就想着深入学习一下前端中常见的一些网络资源请求相关API和工具之间的演进和区别。

先抛出一个问题,浏览器中的 http请求 有哪些方式?有这些:

  1. URL
  2. Links
  3. JavaScript(window.location.href = 'http://www.google.com'
  4. XMLHttpRequest (XHR)
  5. Fetch API
  6. Axios
  7. WebSocket

在实际开发中,XHRFetch API 是发送异步 HTTP 请求的两种主要方式,在使用它们时需要注意跨域问题和安全问题。Axios 提供了更多易于使用的功能和选项,使得发送 HTTP 请求更加方便和灵活。WebSocket 则适用于需要实现实时双向通信的应用场景。

日常开发更多可能接触到的是AjaxFetch APIAxios三种:

  1. Ajax:是一种早期的用于在浏览器中发送异步 HTTP 请求的技术。Ajax 通过 XMLHttpRequest 对象来发送请求,并通过回调函数处理响应数据。Ajax 的优点是简单易用,缺点是需要手动编写大量的回调函数来处理请求和响应,代码可读性较差。

  2. Fetch API:是一个新的 JavaScript API,用于在浏览器中发送异步 HTTP 请求。Fetch 使用 Promise 对象来处理请求和响应,支持链式调用和异步处理,代码可读性较好。Fetch 的优点是支持跨域请求、使用标准的 Promise API、返回的是 Response 对象,可以使用各种方法处理响应数据。缺点是不兼容低版本的浏览器,需要使用 polyfill 来解决。

  3. Axios:是一个流行的 JavaScript HTTP 客户端库,用于在浏览器和 Node.js 环境中发送 HTTP 请求。Axios 使用 Promise 对象来处理请求和响应,支持链式调用和异步处理,代码可读性较好。Axios 的优点是具有丰富的功能和选项,如请求取消、拦截请求和响应、转换请求和响应数据等。缺点是需要手动引入库文件,增加了代码量和体积。

接下来,我们就详细总结主要的三种方式,一次性彻底搞懂它们之间的区别。

1、Ajax

1.1 什么是 Ajax

Asynchronous JavaScript + XML(异步 JavaScript 和 XML), 其本身不是一种新技术,而是一个在 2005 年被 Jesse James Garrett 提出的新术语,用来描述一种使用现有技术集合的‘新’方法,包括:HTMLXHTML, CSS, JavaScript, DOM, XML (en-US), XSLT, 以及最重要的 XMLHttpRequest

当使用结合了这些技术的 AJAX 模型以后,网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面。这使得程序能够更快地回应用户的操作。

尽管 X 在 Ajax 中代表 XML,但由于JSON的许多优势,比如更加轻量以及作为 Javascript 的一部分,目前 JSON 的使用比 XML 更加普遍。JSON 和 XML 都被用于在 Ajax 模型中打包信息。

—— AJAX - mozilla

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请求方法 —— GETPOSTHEAD,或其他你的服务器支持的方法。根据HTTP标准,保持该方法的全大写,否则一些浏览器(如Firefox)可能无法处理该请求。关于可能的HTTP请求方法的更多信息,请查看 规范

image.png

send() 方法的参数可以是任何你想发送给服务器的数据,如果是 POST-ing 请求。表单数据应该以服务器可以解析的格式发送,如查询字符串。

"name=value&anothername="+encodeURIComponent(myVar)+"&so=on"

或其他格式,如multipart/form-dataJSONXML等。

请注:如果你想要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>

测试:

image.png

在这个例子中:

  • 用户点击“提出请求”按钮;
  • 事件处理程序调用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 的优缺点:

优点:

  1. 减少页面刷新:Ajax 可以在页面不刷新的情况下获取和显示数据,减少了用户等待时间和流量消耗。
  2. 提高用户体验:由于 Ajax 可以实现异步请求和响应,使得用户可以在不中断操作的情况下获取数据,从而提高了用户的体验。
  3. 减轻服务器压力:由于 Ajax 可以部分更新页面,减少了服务器处理请求的次数,从而减轻了服务器的压力。
  4. 支持多种数据格式:Ajax 可以支持多种数据格式,如 XMLJSON 等,使得数据的传输和处理更加灵活。

缺点:

  1. SEO 不友好:由于 Ajax 的异步请求不会刷新整个页面,搜索引擎很难获取 Ajax 加载的数据,从而降低了网站的 SEO 优化效果。
  2. 安全性问题:Ajax 可能会导致跨站点脚本攻击(Cross-site scripting, XSS)跨站点请求伪造(Cross-site request forgery, CSRF)等安全问题,开发人员需要采取相应的安全措施。
  3. 开发复杂度高:Ajax 的开发需要涉及到多个技术领域,包括 HTMLCSSJavaScriptXMLJSON 等,开发人员需要具备多方面的技能,开发难度较大。

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 API Headers 的接口允许您对HTTP 请求和响应标头执行各种操作。这些操作包括从请求的标头列表中检索、设置、添加和删除标头。

一个Headers对象有一个关联的头列表,它最初是空的,由零个或多个名称和值对组成。您可以使用类似的方法添加到此append()(请参阅示例。)在该接口的所有方法中,标头名称通过不区分大小写的字节序列进行匹配。

出于安全原因,某些标头只能由用户代理控制。这些标头包括禁止的标头名称禁止的响应标头名称

Headers 对象也有一个关联的守卫,它的值是immutable, request, request-no-cors, response, 或none。这会影响set()delete()append()方法是否会改变标头。有关详细信息,请参阅守卫

您可以Headers通过Request.headersResponse.headers属性检索对象,并Headers使用Headers()构造函数创建新对象。

一个对象实现Headers可以直接在结构中使用for...of,而不是entries():for (const p of myHeaders)等同于for (const p of myHeaders.entries()).

Fetch API Headers - mozilla

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对象 中的键值对。

2.3 Request

Fetch API Request 的接口表示资源请求。

你可以使用 Request() 构造函数创建一个新的 Request 对象,但你更有可能遇到一个 Request 对象作为另一个 API 操作的结果被返回,比如一个service worker FetchEvent.request

2.3.1 构造函数

2.3.2 实例属性

2.3.3 实例方法

注意: 请求体函数只能运行一次;后续调用将解析为 空字符串/ArrayBuffers

2.3.4 例子

在下面的代码片段中,我们使用构造函数创建一个新请求Request()(对于与脚本位于同一目录中的图像文件),然后返回请求的一些属性值:

const request = new Request('https://www.mozilla.org/favicon.ico');

const url = request.url;
const method = request.method;
const credentials = request.credentials;

Request然后,您可以通过将对象作为参数传递给调用来获取此请求fetch(),例如:

fetch(request)
  .then((response) => response.blob())
  .then((blob) => {
    image.src = URL.createObjectURL(blob);
  });

在以下代码片段中,我们使用Request()构造函数创建一个新请求,其中包含一些初始数据和需要主体有效负载的 API 请求的主体内容:

const request = new Request('https://example.com', {method: 'POST', body: '{"foo": "bar"}'});

const url = request.url;
const method = request.method;
const credentials = request.credentials;
const bodyUsed = request.bodyUsed;

注意: body 只能是 BlobArrayBufferTypedArrayDataViewFormDataURLSearchParamsReadableStreamString 对象,以及字符串文字,因此要将 JSON 对象添加到有效负载,你需要对该对象进行字符串化。

然后,你可以通过将 Request 对象作为参数传递给调用来获取此 API 请求fetch(),例如并获得响应:

fetch(request)
  .then((response) => {
    if (response.status === 200) {
      return response.json();
    } else {
      throw new Error('Something went wrong on API server!');
    }
  })
  .then((response) => {
    console.debug(response);
    // …
  }).catch((error) => {
    console.error(error);
  });

2.4 Response

Fetch API Response 的接口表示对请求的响应。

你可以使用 Response() 构造函数创建一个新的 Response 对象,但你更有可能遇到一个 Response 对象作为另一个API操作的结果被返回—例如,一个service worker FetchEvent.replyWith,或一个简单的 fetch()

2.4.1 构造函数

2.4.2 实例属性

2.4.3 静态方法

2.4.4 实例方法

2.4.5 例子

在我们的 基本获取示例 中(实时运行示例),我们使用一个简单的fetch()调用来抓取一张图片并将其显示在<img>元素中。fetch()调用返回一个 promise,它解析为与资源获取操作相关的Response对象。

你会注意到,由于我们正在请求图像,因此我们需要运行 Response.blob 以向响应提供正确的 MIME 类型。

const image = document.querySelector(".my-image");
fetch("flowers.jpg")
  .then((response) => response.blob())
  .then((blob) => {
    const objectURL = URL.createObjectURL(blob);
    image.src = objectURL;
  });

2.5 Fetch API 优缺点

Fetch API 是一个基于 Promise 设计的 JavaScript API,用于在 Web 应用程序中进行网络请求。以下是 Fetch API 的优缺点:

优点:

  1. 更简洁的代码:相比 XMLHttpRequestFetch API 的语法更加简单易懂,使用起来更加方便。
  2. 基于 Promise 设计:PromiseJavaScript 中一种非常强大的异步编程机制,可以帮助开发者更好地处理异步操作,避免回调地狱。
  3. 支持跨域请求:Fetch API 内置了跨域请求的支持,可以让开发者更加轻松地处理跨域问题。
  4. 更好的错误处理:Fetch API 在网络请求失败时会抛出异常,可以帮助开发者更好地处理错误。
  5. 可以自定义请求头:Fetch API 允许开发者自定义请求头,可以更好地控制请求。

缺点:

  1. 不兼容旧版本浏览器:Fetch API 并不是所有浏览器都支持的,特别是在一些旧版本浏览器中可能会有兼容性问题。
  2. 默认不携带 cookieFetch API 默认不携带 cookie,需要手动设置才能携带 cookie
  3. 无法取消请求:Fetch API 并不支持直接取消请求,需要开发者自己处理取消请求的逻辑。

2.6 Fetch API 安全性问题

Fetch API 是用于获取和发送资源的现代 Web API。它可以使网络请求更加灵活和高效。然而,由于它的开放性和灵活性,Fetch API 也可能存在一些安全性问题。以下是一些可能存在的安全性问题:

  1. 跨站点请求伪造(CSRF):攻击者可以通过伪造一个可信站点的请求,以在用户不知情的情况下执行某些操作。这可以通过在请求头中包含攻击者的自定义Cookie或其他授权信息来实现。
  2. 跨域资源共享(CORS):由于浏览器的同源策略限制,浏览器只允许来自同一域的请求。但是,使用 CORS,站点可以通过在响应头中包含 Access-Control-Allow-Origin 来授权其他站点进行请求。如果站点的 CORS 设置不正确,攻击者可能会利用它来执行跨域攻击。
  3. HTTPS劫持:攻击者可能会尝试在 HTTPS 连接中中间攻击并篡改Fetch API 请求。这可以通过使用代理或恶意软件来实现。
  4. 注入攻击:如果站点的Fetch API处理不正确,攻击者可能会利用它来执行 SQL注入 或其他类型的注入攻击。

为了避免这些安全问题,开发人员应该采取适当的措施来保护站点的Fetch API。例如:

  1. 使用随机生成的令牌来防止CSRF攻击。
  2. 设置适当的CORS策略,并使用适当的 Access-Control-Allow-Origin 标头来限制请求来源。
  3. HTTPS 连接中使用证书,并使用证书钩子来验证证书。
  4. 对于所有输入,使用参数化查询来避免注入攻击。

3、Axios

3.1 什么是 Axios?

官方:Axios 是一个基于 promise 的 JavaScript HTTP 客户端,可用于node.js和浏览器。它是同构的(=它可以在浏览器和nodejs中以相同的代码库运行)。在服务器端,它使用本地 node.jshttp 模块,而在客户端(浏览器)则使用 XMLHttpRequests

Axios 支持 JS ES6 原生的 Promise API,它比 .fetch() 具有的另一个特性是它执行 JSON 数据的自动转换。

它的 Features

  • 从浏览器中发出 XMLHttpRequests
  • node.js 发出http请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 超时处理
  • 查询参数序列化,支持嵌套条目
  • 自动将请求主体序列化为:
    • JSON (application/json)
    • Multipart/FormData(multipart/form-data)
    • URL编码的表单(application/x-www-form-urlencoded)
  • 将HTML表单发布为 JSON
  • 响应中自动处理 JSON数据
  • 为浏览器和 node.js 捕捉进度,并提供额外的信息(速度、剩余时间)。
  • node.js 设置带宽限制
  • 与符合规范的 FormDataBlob(包括node.js)兼容
  • 客户端支持对 XSRF 的保护

3.2 Axios 基本用法

3.2.1 axios 实例

可以用自定义配置创建一个新的 axios 实例:

const instance = axios.create({
    baseURL: 'https://some-domain.com/api/',
    timeout: 1000,
    headers: {'X-Custom-Header': 'foobar'}
});

下面是可用的一些实例方法,指定的配置将被合并到实例配置中:

  • axios#request(config)
  • axios#get(url[, config])
  • axios#delete(url[, config])
  • axios#head(url[, config])
  • axios#options(url[, config])
  • axios#post(url[, data[, config]])
  • axios#put(url[, data[, config]])
  • axios#patch(url[, data[, config]])
  • axios#getUri([config])

3.2.2 处理请求

axios 为所有支持的请求方法提供了简洁的 api

  • axios.request(config)
  • axios.get(url[, config])
  • axios.delete(url[, config])
  • axios.head(url[, config])
  • axios.options(url[, config])
  • axios.post(url[, data[, config]])
  • axios.put(url[, data[, config]])
  • axios.patch(url[, data[, config]])
  • axios.postForm(url[, data[, config]])
  • axios.putForm(url[, data[, config]])
  • axios.patchForm(url[, data[, config]])

3.2.2.1 发送 GET 请求

axios.get('/user?ID=12345')
  .then(function (response) {
    // handle success
    console.log(response);
  })
  .catch(function (error) {
    // handle error
    console.log(error);
  })
  .finally(function () {
    // always executed
  });

// 也可以用以下方式
axios.get('/user', {
    params: {
      ID: 12345
    }
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  })
  .finally(function () {
    // always executed
  });

async function getUser() {
  try {
    const response = await axios.get('/user?ID=12345');
    console.log(response);
  } catch (error) {
    console.error(error);
  }
}

还可以通过传递相关的配置参数实现get请求:

// GET request for remote image in node.js
axios({
    method: 'get',
    url: 'http://bit.ly/2mTM3nY',
    responseType: 'stream'
})
.then(function (response) {
    response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});

多个并行请求:

function getUserAccount() {
    return axios.get('/user/12345');
}

function getUserPermissions() {
    return axios.get('/user/12345/permissions');
}

const [acct, perm] = await Promise.all([getUserAccount(), getUserPermissions()]);

// or
Promise.all([getUserAccount(), getUserPermissions()])
    .then(function ([acct, perm]) {
        // ...
    });

3.2.2.2 发送 POST 请求

axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

// or
axios({
    method: 'post',
    url: '/user/12345',
    data: {
      firstName: 'Fred',
      lastName: 'Flintstone'
    }
  });
  • Application/json
const {data} = await axios.post('/user', document.querySelector('#my-form'), {
  headers: {
    'Content-Type': 'application/json'
  }
})
  • Multipart (multipart/form-data)
const {data} = await axios.post('https://httpbin.org/post', {
    firstName: 'Fred',
    lastName: 'Flintstone',
    orders: [1, 2, 3],
    photo: document.querySelector('#fileInput').files
  }, {
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  }
)
  • URL encoded form (application/x-www-form-urlencoded)
const {data} = await axios.post('https://httpbin.org/post', {
    firstName: 'Fred',
    lastName: 'Flintstone',
    orders: [1, 2, 3]
  }, {
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    }
})

3.2.2.3 发送 PUT 请求

axios.put('/api/users/1', {
    name: 'John Doe',
    email: 'john@example.com'
  })
  .then(response => console.log(response.data))
  .catch(error => console.log(error));

3.2.2.4 发送 DELETE 请求

axios.delete('/api/users/1')
  .then(response => console.log(response.data))
  .catch(error => console.log(error));

3.2.2.5 设置请求头

axios.get('/api/users', {
    headers: {
      'Authorization': 'Bearer ' + token
    }
  })
  .then(response => console.log(response.data))
  .catch(error => console.log(error));

3.3 取消请求

timeoutaxios 调用中设置属性会处理与响应相关的超时。在某些情况下(例如网络连接变得不可用),axios 调用将受益于提前取消 连接。如果不取消,axios 调用可能会挂起,直到父代码/堆栈超时(在服务器端应用程序中可能是几分钟)。

要终止 axios 调用,你可以使用以下方法:

  • signal
  • cancelToken(弃用)

组合 timeout 和取消方法(例如 signal)应涵盖与 响应 相关的超时和 连接 相关的超时。

3.3.1 signal: 中止控制器

v0.22.0 axios 开始支持 AbortController fetch API 方式取消请求:

const controller = new AbortController();

axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});

// cancel the request
controller.abort()

AbortSignal.timeout() 使用最新 API [nodejs 17.3+] 的超时示例:

axios.get('/foo/bar', {
   signal: AbortSignal.timeout(5000) //Aborts request after 5 seconds
}).then(function(response) {
   //...
});

带有超时辅助函数的示例:

function newAbortSignal(timeoutMs) {
  const abortController = new AbortController();
  setTimeout(() => abortController.abort(), timeoutMs || 0);

  return abortController.signal;
}

axios.get('/foo/bar', {
   signal: newAbortSignal(5000) //Aborts request after 5 seconds
}).then(function(response) {
   //...
});

3.3.2 CancelToken

你还可以使用 CancelToken 取消请求。

你可以使用工厂创建取消令牌,CancelToken.source如下所示:

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

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // 处理错误
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// 取消请求(消息参数是可选的)
source.cancel('Operation canceled by the user.');

你还可以通过将执行函数传递给构造函数来创建取消令牌CancelToken

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // 一个执行器函数接收一个取消函数作为参数
    cancel = c;
  })
});

// cancel the request
cancel();

在过渡期间,你可以使用两种取消 API,即使是针对同一个请求:

const controller = new AbortController();

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

axios.get('/user/12345', {
  cancelToken: source.token,
  signal: controller.signal
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
// OR
controller.abort(); // the message parameter is not supported

3.4 拦截请求和响应

axios.interceptors.request.use(config => {
  // Do something before request is sent
  return config;
}, error => {
  // Do something with request error
  return Promise.reject(error);
});

axios.interceptors.response.use(response => {
  // Do something with response data
  return response;
}, error => {
  // Do something with response error
  return Promise.reject(error);
});

3.5 错误处理

axios.get('/user/12345')
  .catch(function (error) {
    if (error.response) {
      // 请求已经发出,但是服务器回应的状态码不是 2xx。
      console.log(error.response.data);
      console.log(error.response.status);
      console.log(error.response.headers);
    } else if (error.request) {
      // 请求已经发出,但没有收到响应
      // `error.request`在浏览器中是 XMLHttpRequest 的一个实例
      // 在node.js中是 http.ClientRequest 的一个实例。
      console.log(error.request);
    } else {
      // 设置触发错误的请求时需要处理的一些事情
      console.log('Error', error.message);
    }
    console.log(error.config);
  });

3.6 Axios 优缺点

优点:

  1. Promise API:Axios使用Promise API,因此可以轻松处理异步操作。Promise API具有更清晰的语法和更好的可读性,因为它们允许在异步操作完成之前进行链式调用,从而避免了回调地狱问题。

  2. 简单易用:Axios的API设计简单且易于使用,而且它还提供了许多可用的配置选项,例如设置请求头、超时时间、认证等等,让开发者可以更轻松地定制请求。

  3. 可扩展性:Axios可以通过添加拦截器(interceptors)来实现许多自定义功能,例如添加请求拦截器、响应拦截器和错误拦截器等等。这些拦截器可以让开发者在请求和响应过程中进行自定义操作。
  4. 支持浏览器和Node.jsAxios可以同时在浏览器和Node.js环境中使用。这意味着开发者可以使用相同的代码库在两个不同的环境中实现网络请求功能。
  5. 轻量级:Axios是一个轻量级库,它只依赖于Promise和一些基本的JavaScript库,因此它的体积相对较小。

缺点:

  1. 学习曲线:尽管AxiosAPI设计简单且易于使用,但是它仍然需要一些学习和理解。特别是对于初学者来说,可能需要一些时间才能掌握如何使用Axios

  2. 对于大型应用程序可能不够强大:对于大型应用程序来说,Axios可能会显得有些简单。因为它不提供复杂的数据管理功能,例如状态管理、数据缓存等等。这时候可能需要使用其他更为强大的HTTP客户端库(比如 TanStack Query)来实现这些功能。

  3. 可能出现跨域问题:Axios不能直接解决跨域请求的问题。尽管Axios可以设置跨域请求头,但是它不能绕过浏览器的安全限制。这意味着在某些情况下,开发者可能需要通过其他方式来解决跨域请求的问题。

3.7 Axios 安全性

Axios 在 Web 安全方面做了以下工作:

  1. 防止跨站点请求伪造(CSRF)攻击:Axios 会自动在请求头中添加 CSRF token(如果可用的话),以防止攻击者利用用户身份进行 CSRF 攻击。此外,Axios 还支持自定义请求头,允许开发人员手动添加其他安全相关的请求头。

  2. 防止 XSS 攻击:Axios 不会自动解析或执行响应的内容,因此可以防止跨站点脚本(XSS)攻击。

  3. 支持 HTTPSAxios 支持使用 HTTPS 进行加密传输,从而保护数据的安全性。
  4. 支持自定义验证:Axios 提供了自定义验证的接口,允许开发人员根据自己的需求进行验证,以确保请求和响应的安全性。
  5. 支持取消请求:Axios 提供了取消请求的接口,允许开发人员在请求过程中取消请求,以避免恶意攻击或其他安全问题的发生。

4、Fetch API 和 Axios 的区别

Ajax 实际上不是一个特定的技术,而是一系列技术的统称,通常我们将其默认为是 XMLHttpRequestFetch API 是ES6之后出现的一个新的基于 Promise 的新API,可以认为是 XMLHttpRequest 的最新替代品,开发人员终于可以忘记丑陋且难以使用的HTMLHttpRequests了。Axios 则是一个专门用于HTTP请求的库,也是基于 Promise。实际开发中我们可能更多使用 Fetch APIAxios,接下来就结合实际例子来分析它们的区别。

4.1 基本定义

4.1.1 Axios

Axios 是用于发出网络请求的第三方 HTTP 客户端库。它是基于 Promise 的,可以在以下示例中看到:

axios.get('https://fakestoreapi.com/products/1')
  .then(response => console.log(response));

默认情况下,响应数据以 JSON 格式提供,因此我们可以使用 data 属性立即访问它。它在底层使用XMLHttpRequest,这是它广泛支持浏览器的原因之一。你可以通过 CDN 或使用包管理器(如npm)将其添加到你的项目中。

核心功能包括:

兼容性方面,大多数浏览器都广泛支持 Axios,即使是像 IE11 这样的旧浏览器。

4.1.2 Fetch

Fetch 是一个基于 promise 的 API。看一个简单的例子:

fetch('https://fakestoreapi.com/products/1')
      .then((response) => console.log(response.json()))
      .catch((error) => console.error(error));

Fetch API 提供了一个简单易用的接口来获取资源,更具体地说,它为我们提供了访问和操作部分 HTTP 管道的方法,其中部分是请求和响应。

Fetch API 提供了fetch() 方法。使用 Fetch API 我们可以完全实现 Axios 的所有核心功能。实际上,Fetch API 是一个比 Axios 具有更多可能性的原生接口。但是,因为它是底层的API,所以通常使用起来有点繁琐。

兼容性方面,Fetch 是一个内置的 API,因此我们不需要安装或导入任何东西。它在所有现代浏览器中都可用,您可以在caniuse上查看。Fetch 在 node.js 中也可用 - 可以在此处阅读更多相关信息。

请记住,即使你的浏览器不支持 Fetch,你始终可以使用polyfill。然而,如果你必须使用这个 polyfill,你可能还需要一个promise polyfill。

4.2 基本语法

4.2.1 Axios

Axios 提供了多种不同的调用方式:

axios(url, {
  // configuration options
})

我们可以使用点表示法,而不是在配置选项中指定 HTTP 方法:

axios.post(url, {
  // configuration options
})

如果我们省略配置选项和点符号,它默认为 GET 方法:

axios(url)

我们可以为请求使用许多不同的配置设置,使用第二个参数:

axios(url, {
  method: 'post',
  timeout: 1000,
  headers: {
    "Content-Type": "application/json",
  },
  data: {
    property: "value",
  },
})

可以在此处 找到完整API列表。

4.2.2 Fetch

Fetch 接收两个参数,第一个参数是 URL,第二个是配置选项:

fetch(url, {
  method: 'POST',
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    property: "value",
  }),
})

有关配置选项的完整列表,请查看此链接

4.2.3 总结

来看两个具体的例子:

let url = 'https://someurl.com';
let options = {
            method: 'POST',
            mode: 'cors',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json;charset=UTF-8'
            },
            body: JSON.stringify({
                property_one: value_one,
                property_two: value_two
            })
        };
let response = await fetch(url, options);
let responseOK = response && response.ok;
if (responseOK) {
    let data = await response.json();
    // do something with data
}
let url = 'https://someurl.com';
let options = {
            method: 'POST',
            url: url,
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json;charset=UTF-8'
            },
            data: {
                property_one: value_one,
                property_two: value_two
            }
        };
let response = await axios(options);
let responseOK = response && response.status === 200 && response.statusText === 'OK';
if (responseOK) {
    let data = await response.data;
    // do something with data
}

基本语法非常相似,几乎完全相同。但是,存在多个小差异:

  • 不同的属性用于 post 请求以将数据发送到 endpoint - Axios 使用 data 属性,而 fetch 使用body属性。
  • 我们需要将数据序列化为 JSON 字符串来发送数据。Axios 在使用 POST 方法向 API 发送 JavaScript 对象时自动将数据字符串化。
  • Axios 会自动转换从服务器返回的数据,但使用 fetch() 时,你必须调用 response.json 方法,将数据解析为 JavaScript 对象。关于 response.json 方法的更多信息可以在这里找到。
  • 使用 fetch() 时,我们必须使用 JSON.stringify 将数据转换为字符串。
  • 使用 Axios,服务器提供的数据响应可以在 数据对象 中访问,而对于 fetch() 方法,最终数据可以被命名为任何变量。

4.3 响应处理

4.3.1 Axios

在 Axios 中,响应数据默认以 JSON 格式提供。我们所要做的就是访问响应对象的数据属性:

axios.get('https://fakestoreapi.com/products/1')
  .then(response => {
    console.log(response.data);
  }, error => {
    console.log(error);
  });

可以使用配置选项更改响应类型,即responseType属性。它指示服务器将响应的数据类型,选项包括:

  • arraybuffer
  • document
  • json (default)
  • text
  • stream
  • blob

4.3.2 Fetch

使用 Fetch,在访问响应数据之前我们还需要执行一个步骤:

fetch('https://fakestoreapi.com/products/1')
  .then((response) => response.json())
  .then((data) => {
    console.log(data);
  })
  .catch((error) => console.error(error));

image.png

使用fetch()时,我们会在所有标头到达后立即收到响应。 那时,我们还没有尚未加载的 HTTP 响应主体。 这就是我们收到另一个应许的原因。简而言之,response.json() 等待正文加载。

我们每天使用的 JSON 对象通常都比较小。但是,想象一下您必须加载非常大的图像的情况。这可能需要一段时间,在图像完全加载之前我们可能需要相关信息。

要阅读有关 response.json() 的更多信息,请查看此链接

4.3.2 总结

Fetch 是一个相对低级的 API,可以精确控制加载过程。 它伴随着每次使用它时必须处理两个 Promise 的成本。

  • Fetch 要求我们在处理响应时多做一步,因为它返回一个 Promise,此时我们没有我们需要的 JSON 数据格式,因此需要 .json() 方法。
  • 要在 Axios 中访问响应数据,我们使用 data 属性,而在使用 fetch() 时,最终数据可以命名为任何变量。

4.4 错误处理

还记得 AxiosFetch 都是基于 promiseHTTP 客户端吗?因此,他们都返回一个可以 resolverejectPromise。然而,就相似性而言,仅此而已—— promise得到 resolvereject 的条款大不相同。

4.4.1 Axios

让我们看一个典型的 Axios 错误处理示例,使用 .catch()

axios.get('https://codilime.com/')
  .then(response => console.log(response.data))
  .catch((err) => console.log(err.message));

每个响应都包含一个状态属性,它只是一个 HTTP 响应状态代码。这些代码表示请求是否已成功完成。例子包括:

  • 200 OK - 请求成功,
  • 403 Forbidden——客户端无权访问内容,
  • 418 I'm a teapot - 服务器拒绝尝试用茶壶冲泡咖啡(不,这不是玩笑,请在此处查看)。

可以在此处阅读有关这些 HTTP 响应状态代码的更多信息。

现在我们知道了 HTTP 响应状态代码是什么,我们可以理解 Axios 错误处理是如何工作的。Axios 将拒绝任何状态码超出 200-299 范围(成功响应)的Promise

我们可以检查错误对象以获取有关错误的更多信息:

axios.get('https://fakestoreapi.com/products/1')
  .then(response => console.log(response.data))
  .catch((err) => {
    if (err.response) {
      // The request was made, but the server responded with a status code that falls out of the 2xx range
      const { status } = err.response;

      if (status === 401) {
        // The client must authenticate itself to get the requested response
      }
      else if (status === 502) {
        // The server got an invalid response
      }
      ...
    }
    else if (err.request) {
      // The request was made, but no response was received
    }
    else {
      // Some other error
    }
  });

当我们在错误对象上有 response 属性时,这意味着发出了请求,并且服务器响应了,但是响应状态代码在2xx 范围之外。另一方面,请求属性表示已发出请求,但未收到响应。如果这两者都不正确,则表示设置网络请求时出现问题,从而触发错误。

4.4.2 Fetch

获取错误处理与 Axios 有很大不同。最重要的区别是,如果我们收到 HTTP 错误,它不会reject Promise——不成功的响应仍然会得到解决。因此,HTTP 错误在 .then 块中处理。只有在网络出现故障的情况下,Fetch API Promise才会被拒绝。

fetch('https://fakestoreapi.com/products/1')
  .then(response => {
    if (!response.ok) {
      throw Error(`HTTP error: ${response.status}`);
    }
    return response.json();
  })
  .then(data => console.log(data))
  .catch((error) => {
    console.log(error)
  });

使用 Fetch API 需要我们检查response.ok属性来控制 HTTP 错误。

两个最重要的响应属性是statusok

  • status - 响应状态代码(整数)。
  • ok - 检查状态是否在 2xx(布尔值)范围内的简写。

在成功的场景中,它们将分别具有200true值:

{
  ...
  status: 200,
  ok: true,
  ...
}

但是,在错误情况下,我们将获得 HTTP 错误状态代码和false。例如,如果我们没有通过身份验证来接收请求的响应,我们将得到:

{
  ...
  status: 401,
  ok: false,
  ...
}

4.4.3 总结

在 Axios 中,超出 200-299 范围(成功响应)的响应将被自动拒绝。使用 .catch() 块,我们可以获得有关错误的信息,例如是否收到响应,如果收到,则返回其状态代码。

另一方面,在 fetch() 中,不成功的响应仍然得到解决。这就是为什么我们必须检查响应的ok属性并在它设置为 false 时抛出错误。然后在.catch() 块中处理该错误。

4.5 响应超时

在设定的时间后中止请求的功能是发出 HTTP 请求的重要部分。如果没有该功能,请求可能会挂起并可能导致我们的应用程序变慢。

4.5.1 Axios

使用 Axios 设置响应超时非常容易。我们所要做的就是在请求的配置对象中添加一行:

axios.get(url, {
  ...
  timeout: 2000,
  ...
})
  .then(response => console.log(response.data))
  .catch(err => console.log(err.message))

超时是一个可选参数,以毫秒为单位。如果超过两秒,上述请求将被中止,并记录错误。timeout属性的默认值为0,表示没有超时。

4.5.2 Fetch

用 Fetch API 做同样的事情并不容易。要实现相同的行为,我们可以使用名为AbortController的接口和fetch() 方法配置选项:

const controller = new AbortController();
const options = {
  method: 'POST',
  signal: controller.signal,
  body: JSON.stringify({
    firstName: 'David',
    lastName: 'Pollock'
  })
};

const promise = fetch('/login', options);
const timeoutId = setTimeout(() => controller.abort(), 4000);

promise
  .then(response => {/* handle the response */})
  .catch(error => console.error('timeout exceeded'));

在这里,通过创建一个AbortController对象,我们可以访问abort() 方法和信号对象,它允许我们稍后中止该请求。信号是 AbortController 的一个只读属性,提供了一个与请求沟通或中止请求的方法。如果服务器在四秒内没有回应,controller.abort() 就会被调用,操作就会终止。

请注意,如果您在fetch() 完成后调用*abort() ,它只会忽略它。

4.5.3 总结

fetch() 没有像 Axios 那样为我们提供超时配置选项。相反,我们必须使用AbortController接口和setTimeout函数。Axios 隐藏了很多这样的样板文件,这使其成为大多数用例的明显赢家。 但是,我们已经知道 Fetch 是一个低级 API。根据您的需要,在响应超时和取消请求方面,您可能仍然更喜欢 Fetch 而不是 Axios。

4.6 HTTP 请求和响应拦截

Axios 的主要功能之一是其拦截 HTTP请求 的能力。当你需要检查或改变从你的应用程序到服务器的HTTP请求或反之亦然时,HTTP拦截器就会派上用场(例如,记录、认证或重试一个失败的HTTP请求)。

有了拦截器,你就不必为每个HTTP请求单独编写代码。当你想为你处理请求和响应的方式设置一个全局策略时,HTTP拦截器很有帮助。

想象一个场景,我们必须保存从我们的应用程序发送的每个 HTTP 请求的日志。向每个请求添加这样的代码很麻烦,容易出错,简而言之,不可行。这就是拦截器发挥作用的时候。它们允许我们消除为每个请求重复代码,而是创建和设置处理请求和响应的全局行为。

简而言之,HTTP 拦截器用于在客户端和服务器端 HTTP 请求和响应之间应用自定义逻辑。它们可用于不同的操作,例如:

  • 修改 HTTP 标头或正文
  • 设置自定义令牌
  • 修改响应

4.6.1 Axios

Axios 具有用于简单创建 HTTP 拦截器的功能 - 包括响应和请求拦截器。首先,让我们看一个请求拦截器:

axios.interceptor.request.use(config => {
  // log data before HTTP request is sent
  console.log('HTTP request being sent...');
  return config;
})

上面的代码会在发送任何 HTTP 请求之前产生一条日志消息。类似地,响应拦截器被定义为在它们被thencatch处理之前运行一些代码:

axios.interceptor.response.use(
  function (response) {
    // This function will be triggered on any status code inside 2xx range
    return response;
  },
  function (error) {
    // This function will be triggered on any status code outside of 2xx range
    return Promise.reject(error);
  }
)

例如,当您想要设置一个策略以在失败的情况下始终重试一次 HTTP 请求时,响应拦截器可能会派上用场。

如果您愿意,Axios 还允许您删除拦截器:

const requestInterceptor = axios.interceptor.request.use(config => {
  // log data before HTTP request is sent
  console.log('HTTP request being sent...');
  return config;
});
axios.interceptor.request.eject(requestInterceptor);

4.6.2 Fetch

使用 Fetch API 时,我们没有配置选项或特殊的内置函数来实现拦截器。然而,Vanilla JS(一种快速、轻量级、跨平台的框架)就足够了:

fetch = (originalFetch => {
  return (...arguments) => {
    const result = originalFetch.apply(this, arguments);
    return result.then(console.log('HTTP request being sent ...'));
  };
})(fetch);

上面的代码是一个 HTTP 请求拦截器的实现。我们可以像使用普通的旧fetch() 函数一样使用它:

fetch('https://fakestoreapi.com/products/1')
  .then(response => response.json())
  .then(console.log)

4.6.3 总结

Axios 为我们提供了开箱即用的功能来创建 HTTP 请求和响应拦截器。 与响应超时类似,它隐藏了大部分样板文件并为我们提供了一个漂亮、易于使用的界面。

另一方面,我们有 Fetch API,同样因为它的级别低,没有为我们提供这样的方法。基于 fetch() 的 HTTP 拦截器很容易编写,并且可以在 Internet 上找到

再一次,我们可以选择避免样板文件而不是更细粒度的控制和自定义选项。选择应基于用例和个人需求。

4.7 download 进度

在 XMLHttpRequests 仍然被广泛使用的日子里,它是用于实现进度指示器的.onprogress 方法。今天,这些指标仍然是加载资产的重要组成部分,尤其是大资产,尤其是对于互联网连接速度较慢的用户。但是,我们不必再摆弄这个接口了。

让我们下载一张图片!

4.7.1 Axios

在使用axios库的时候,一般会使用axios Progress Bar来实现一个进度条。

它可以通过 NPM 包获得:

npm install --save axios-progress-bar

或者通过 CDN:

<script src="https://cdn.rawgit.com/rikmms/progress-bar-4-axios/0a3acf92/dist/index.js"></script>

之后,我们需要将 CSS 导入到 HTML 中,或者通过带有模块打包器的 JavaScript,例如webpack

<link rel="stylesheet" type="text/css" href="https://cdn.rawgit.com/rikmms/progress-bar-4-axios/0a3acf92/dist/nprogress.css" />

就是这样的设置。现在我们可以实现进度条了。我们必须(仅一次)调用loadProgressBar() 函数,该函数可选择采用两个参数 - config 和 instance。配置对象的属性可以在这里看到。第二个参数用于传递自定义 axios 实例。现在,我们将跳过这两个参数以使其尽可能简单。

...

<img id="codilime-logo" />

...

<script type="text/javascript">
  loadProgressBar();
  const url = 'http://codilime.com/image/path/logo.jpg';

  function downloadFile(url) {
    axios.get(url, {
      responseType: 'blob',
    })
      .then(response => {
        const fileReader = new window.FileReader();
        fileReader.readAsDataURL(response.data);
        fileReader.onload = () => {
          document.getElementById('codilime-logo').setAttribute('src', fileReader.result);
        };
      })
      .catch(error => {
        console.log(error)
      })
  }

  downloadFile(url);
</script>

上面的代码使用了FileReader API它允许我们使用FileBlob对象读取文件的内容。FileReader.readAsDataURL()开始读取指定blob的内容,完成后,FileReader.result属性包含一个编码字符串。我们还使用了load事件,它会在读取成功完成时触发。那是我们将结果插入元素中的src属性的时候。

Axios 进度条设计基于angular-loading-bar。它使用负责在浏览器中显示加载栏的nprogress模块。

4.7.2 Fetch

使用 Fetch API 实现进度条并不容易,我们没有像 Axios 那样为我们提供简单易用 API 的包。不过不要害怕 - 正如你可能猜到的那样,还有另一个低级 API 允许我们实现进度条。可悲的是,有更多的代码。

我们将使用ReadableStream API。这个接口代表一个可读的字节数据流。此外,ReadableStream实例存在于Response 对象body属性中。我们将使用该字节流来跟踪进度。

HTML 文件:

...

<div id="progress-bar" src="">Progress bar</div>
<img id="codilime-logo" />

...

文件:

const el = document.getElementById('progress-bar');

fetch('http://codilime.com/image/path/logo.jpg')
  .then(response => {
    if (!response.ok) {
      throw Error(`HTTP error: ${response.status}`);
    }

    // Ensure ReadableStream is supported
    if (!response.body) {
      throw Error('ReadableStream API is not supported in this browser');
    }

    // The Content-Length header indicates the size of the message body, in bytes
    const contentLength = response.headers.get('content-length');
    if (!contentLength) {
      throw Error ('Content-Length response header unavailable');
    }

    const total = parseInt(contentLength, 10);
    let loaded = 0;

    return new Response(
      new ReadableStream({
        start(controller) {
          const reader = response.body.getReader();

          read();
          // read() function handles each data chunk
          function read() {
            reader.read().then(({ done, value }) => {
              // No more data to read
              if (done) {
                controller.close();
                return;
              }
              loaded += value.byteLength;
              progress({ loaded, total });
              // Get the data and send it to the browser via the controller
              controller.enqueue(value);
              read();
            }).catch(error => {
              console.error(error);
              controller.error(error);
            })
          }
        }
      })
    );
  })
  .then(response => response.blob())
  .then(data => {
    document.getElementById('codilime-logo').src = URL.createObjectURL(data);
  })
  .catch(error => {
    console.error(error);
  })

  function progress({ loaded, total }) {
    el.innerHTML = Math.round(loaded / total * 100) + '%';
  }

好的,这是很多代码 - 让我们一步一步地完成它。

<div id="progress-bar" src="">Progress bar</div>
<img id="codilime-logo" />

在 HTML 文件中,我们创建一个img标签,与 Axios 示例中的相同。此外,我们必须创建一个div作为我们的进度条,我们不使用外部包,所以我们必须自己创建它。

const el = document.getElementById('progress-bar');

fetch('http://codilime.com/image/path/logo.jpg')
  .then(response => {
    if (!response.ok) {
      throw Error(`HTTP error: ${response.status}`);
    }

    // Ensure ReadableStream is supported
    if (!response.body) {
      throw Error('ReadableStream API is not supported in this browser');
    }

    // The Content-Length header indicates the size of the message body, in bytes
    const contentLength = response.headers.get('content-length');
    if (!contentLength) {
      throw Error ('Content-Length response header unavailable');
    }
...

然后进入 JS。我们将div元素保存在 el变量中。之后,我们提出请求。标准程序是检查是否存在任何 HTTP 错误、是否支持 ReadableStream API,以及是否存在 Content-Length 标头。

所有主流浏览器都支持 ReadableStream API,您可以通过caniuse检查。因此,您可以在实现中省略第二个 if 语句。

我们添加第三个 if 语句,因为要访问标头,服务器必须发送 CORS 标头 - Access-Control-Expose-Headers - “Access-Control-Expose-Headers: content-length”。此响应标头允许服务器指示哪些响应标头应提供给浏览器中运行的脚本,以响应跨域请求。

现在,进入返回的响应。

...
const total = parseInt(contentLength, 10);
let loaded = 0;

return new Response(
  new ReadableStream({
    start(controller) {
...

ReadableStream 构造函数从给定的处理程序创建并返回一个可读流对象。它接受underlayingSourceobject,其中包含定义构造流实例行为方式的方法和属性。您可以在此处阅读更多信息。

在我们的例子中,我们唯一需要的是start(controller) 方法。构造对象时立即调用它。它的内容应该旨在访问流源和设置流功能。控制器属性ReadableStreamDefaultControllerReadableByteStreamController,具体取决于类型属性。

...
const reader = response.body.getReader();
...

ReadableStreamgetReader()方法创建一个读取器并将流锁定到它。在该锁定期间,在释放此读取器之前,无法获取其他读取器。

...
read();
// read() function handles each data chunk
function read() {
  reader.read().then(({ done, value }) => {
    // No more data to read
    if (done) {
      controller.close();
      return;
    }
    loaded += value.byteLength;
    progress({ loaded, total });
    // Get the data and send it to the browser via the controller
    controller.enqueue(value);
    read();
  }).catch(error => {
    console.error(error);
    controller.error(error);
  })
}
...

read() 函数中,我们调用 readerread() 方法,它返回一个Promise,提供对流内部队列中下一个块的访问。 如果块可用,Promise将通过以下形式的对象实现:

{
  value: theChunk,
  done: false,
}

否则,如果流关闭,Promise将通过以下形式的对象实现:

{
  value: undefined,
  done: true,
}

只有当流显示错误时,promise 才会被 reject

...

// No more data to read
if (done) {
  controller.close();
  return;
}
loaded += value.byteLength;
progress({ loaded, total });
// Get the data and send it to the browser via the controller
controller.enqueue(value);
read();

...

如果没有更多数据要读取,我们使用close()方法关闭关联的流。否则,我们使用进度函数修改div元素。之后,我们使用enqueue() 函数将给定块排入关联流中。

...

console.error(error);
controller.error(error);

...

.catch 子句中,我们使用 error() 方法,这会导致未来与关联流的任何交互出错。

请记住,这只是使用 Fetch API 实现进度条的一种方式。您可以在此GitHub 存储库中查看此实现和其他实现。

4.7.3 总结

这是和以前一样的故事。我们有一个选择。对于高级 Axios API,我们必须使用一个额外的包—— Axios Progress Bar

另一方面,我们有低级的 Fetch API,我们也可以在其中实现进度条。更重要的是,我们可以用各种不同的方式来实现它。这肯定是更多的代码,而且,像往常一样,它提供了对数据处理每个阶段发生的事情的更细粒度的控制。最重要的是,我们不需要任何额外的包。这一切都可以只用 JavaScript 和一些 API 来完成。

4.8 并行请求

如果您已经完成了进度条部分,您会很高兴听到 AxiosFetch API 都提供了易于使用的方式来同时发出多个请求。让我们看看它的实际效果。

4.8.1 Axios

在 Axios 中,我们创建了一个请求数组。每个请求的创建方式与以前相同。此外,我们使用axios.all方法发出多个 HTTP 请求:

const endpoints = [
  'https://fakestoreapi.com/products/1',
  'https://fakestoreapi.com/products/2',
  'https://fakestoreapi.com/products/3'
];

axios.all(
  endpoints.map(endpoint => axios.get(endpoint))
).then(console.log)

我们通过每个端点进行映射,向当前端点发出HTTP GET请求

axios.all 返回一个数组作为响应,其中每个条目对应于我们的端点数组中的一个端点。因此,我们将在data[0]处获得endpoint codilime.com/endpoint1的响应,在data[1]处获得 codilime.com/endpoint2 等的响应。

值得庆幸的是,Axios 还提供了axios.spread方法,它的行为类似于常规的 JavaScript 展开运算符 -

const endpoints = [
  'https://fakestoreapi.com/products/1',
  'https://fakestoreapi.com/products/2',
  'https://fakestoreapi.com/products/3'
];

axios.all(
  endpoints.map(endpoint => axios.get(endpoint))
)
  .then(axios.spread((res1, res2, res3) => {
    console.log(res1.data, res2.data, res3.data);
  }))

4.8.2 Fetch

使用 Fetch API 时,我们可以使用 Promise.all() 方法和解构实现类似的行为:

const endpoints = [
  'http://codilime.com/endpoint1',
  'http://codilime.com/endpoint2',
  'http://codilime.com/endpoint3',
];

Promise.all(
  endpoints.map(endpoint => fetch(endpoint))
)
  .then(async ([ res1, res2, res3 ]) => {
    const res1JSON = await res1.json();
    const res2JSON = await res2.json();
    const res3JSON = await res3.json();
    console.log(res1JSON, res2JSON, res3JSON);
  }))

如果您不熟悉Promise.all(),它会接受可迭代的Promise(例如数组)并返回单个Promise,该Promise解析为输入Promise结果的数组。

4.8.3 总结

在同时发出多个请求时,我们有两个非常相似的解决方案。一种利用外部库方法,另一种使用内置 JavaScript 方法。但是,这两个实现的含义和流程几乎相同。

5、总结

  1. 底层实现:Ajax 的底层实现使用了浏览器内置的 XMLHttpRequest(XHR) 对象,使用方式相对繁琐;而 Fetch API 则使用了基于 Promisefetch() 函数。Axios 则是基于 XHRnode.jshttp 封装的库。

  2. 兼容性:Ajax 具有较好的兼容性,几乎可以在所有浏览器上运行。FetchAxios 虽然也具有较好的兼容性,但需要考虑到浏览器版本和功能支持问题,需要使用 polyfill 或其他兼容性解决方案来保证兼容性。

  3. Promise 和回调函数:Ajax 不支持 Promise,通常使用回调函数来处理异步请求的结果,这种方式容易出现回调地狱的情况;FetchAxios 则使用 Promise 对象来处理异步请求的结果,可以更加方便地进行链式调用和错误处理。

  4. 浏览器支持:AjaxXHR 对象在旧版浏览器中的支持存在一些问题;Fetch API 是比较新的标准,可能会存在一些兼容性问题;Axios 则是一个基于 Promise 的库,支持各种现代浏览器和 Node.js 环境。

  5. 功能和扩展性:AjaxXHR 对象提供了基本的请求和响应功能,但是缺乏灵活性和扩展性。Fetch API 则提供了更加丰富的请求和响应选项,支持请求和响应的拦截、转换、缓存等功能。Axios 则在 Fetch API 的基础上提供了更加强大和灵活的功能,如请求和响应的取消、并发、批量处理等。
  6. 请求和响应拦截器:Axios 支持请求和响应拦截器,可以在请求或响应被处理前或后进行操作,比如添加请求头、修改响应数据等。这对于需要对请求或响应进行全局处理的场景非常有用。FetchAjax 相比较没有内置的请求和响应拦截器,需要手动编写实现。
  7. 库的大小和性能:AjaxXHR 对象是浏览器内置的 API,不需要引入额外的库,因此体积较小,但是在处理大量请求时性能可能较差。Fetch API 是新的标准,需要使用 polyfill 或者引入额外的库,因此体积较大,但是性能相对较好。Axios 是一个功能丰富的库,体积相对较大,但是性能表现较好,尤其在处理大量请求时优势更为明显。
  8. 并发请求:Axios 可以方便地发送多个并发请求,通过 Promise.allPromise.race 来处理响应。而 FetchAjax 只能通过 Promise 来处理单个请求,需要手动编写实现并发请求的逻辑。
  9. 对于同源策略的处理:AjaxFetch API 都遵循浏览器的同源策略,不能直接请求不同域名的资源。但是,它们可以通过设置 CORS 或使用 JSONP 等技术来实现跨域请求。而 Axios 对跨域请求的处理更为简单,可以通过使用代理等方式来实现跨域请求。
  10. 请求的取消和超时:Axios 支持请求的取消和超时设置,可以在发送请求时设置 cancelTokentimeout 选项,对于需要及时取消或超时的请求非常有用。Fetch APIAjax 在原生状态下不支持请求的取消和超时设置,需要手动编写实现。
  11. API 的使用风格:AjaxFetchAxiosAPI 的使用风格上有所差异。Ajax 使用的是回调函数的方式,代码结构较为混乱,不易维护。Fetch API 使用 Promise 的方式,代码结构清晰、易读、易维护。而 Axios 同样使用 Promise 的方式,但相对于 Fetch API 来说,它的 API 更为灵活,可以方便地进行扩展和定制。
  12. 使用场景:AjaxFetchAxios 在不同的使用场景下有不同的优势。Ajax 的优势在于它是原生的浏览器 API,功能简单、易用、兼容性好,适合简单的异步请求场景。Fetch API 的优势在于它是 Promise 风格的 API,支持流式传输、文件上传和下载等功能,适合复杂的异步请求场景。而 Axios 的优势在于它是一个功能齐全、易用、灵活的请求库,支持请求和响应拦截器、请求的取消和超时、并发请求等功能,适合复杂的异步请求场景。
Ajax Fetch API Axios
基于Promise 🛑(基于 XMLHttpRequest)
兼容性 ✅ 具有较好的兼容性,几乎可以在所有浏览器上运行 🟡 对于现代浏览器的兼容性更好 🟡 对于现代浏览器的兼容性更好
TypeScript支持 🛑 🟡
数据缓存 🛑
自动重试 🛑
取消请求 🟡 原生不支持,需手动编写实现 🟡 原生不支持,需手动编写实现
请求异常 🟡 在网络异常和超时处理方面需要手动编写实现 🟡 在网络异常和超时处理方面需要手动编写实现 ✅ 提供了更好的网络异常处理机制,可以方便地处理网络异常和超时异常
超时时间 🛑 🛑
跨域 🟡 原生不支持跨域请求,需手动实现(如JSONP)
错误处理 🟡 原生对于错误处理和状态码处理的支持相对较少 🟡 原生对于错误处理和状态码处理的支持相对较少 ✅ 提供了丰富的错误处理和状态码处理的功能
请求响应拦截 🛑 🟡 ✅(全局)
并发请求 🟡 原生不支持并发请求,需要手动编写实现
数据转换 🟡 原生状态下需要手动将响应数据转换为 JavaScript 对象 🟡 原生状态下需要手动将响应数据转换为 JavaScript 对象 ✅ 可以自动将响应数据转换为 JavaScript 对象
序列化参数 ✅ 使用 URLSearchParams 🟡 原生不支持 URLSearchParams 对象,需要手动编写实现 ✅ 使用 URLSearchParams
文件处理 🟡 原生不支持文件上传和下载,需要手动编写实现 🟡 原生不支持文件上传和下载,需要手动编写实现
插件机制 🟡 在扩展性方面相对较差,需要手动编写实现 🟡 在扩展性方面相对较差,需要手动编写实现 ✅ 支持自定义拦截器、转换器、扩展请求和响应等功能
数据格式 🟡 对数据格式的支持相对较少,需要手动编写实现 🟡 对数据格式的支持相对较少,需要手动编写实现 ✅ 支持多种数据格式的请求和响应,包括 JSON、URL-encoded、XML、HTML、Text 等
headers验证 🛑 ✅ 支持通过 request.credentials 属性来控制请求是否携带 cookies 和 cors 相关的头部信息 ✅ 可以使用 withCredentials 配置选项来控制请求是否携带 cookies 和 cors 相关的头部信息


文章评论

未显示?请点击刷新