Node 原生支持fetch!不是浏览器那个fetch!

内容分享3周前发布
1 5 0

家好,很高兴又见面了,我是”高级前端‬进阶‬”,由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

Node 原生支持fetch!不是浏览器那个fetch!

高级前端‬进阶‬

前言

Fetch 用在客户端请求发送已有一段时间了,但由于某些限制,它无法添加到 NodeJS 中。 随着 NodeJS v17.0 的发布,它被添加为一个实验性功能,目前它与 LTS(长期支持)版本 V18.0 打包在一起,完成了 NodeJS 开发人员社区期待已久的功能集成。本文将带着大家详细讨论 NodeJS Fetch 的前世今生。

1.Fetch 介绍

网络发展初期,Web 开发人员使用不同的、复杂的方式发送异步请求。 为了标准化此过程,IE 浏览器在 1998 年引入了 XMLHttpRequest,用于通过 HTTP 协议获取 XML数据。

随着 XMLHttpRequest 的普及,它增加了对一些常用数据传输格式(如 HTML、JSON 和纯文本)的支持。 但是随着 Web 技术的进一步发展,使用 XMLHttpRequest 变得越来越繁琐,像 jQuery 这样的 Javascript 框架不得不编写 XMLHttpRequest 的自定义实现,以使其语法和错误处理更容易。

Node 原生支持fetch!不是浏览器那个fetch!

图片来源:https://stevenjhu.com/

Fetch 于 2015 年推出,并迅速成为现代 Web 开发标准。 Fetch 的显著优势是使用了使代码更加清晰的 Promise,并避免了回调地狱,这正是 XMLHttpRequest 面临的主要问题。

尽管 Fetch 已经在浏览器中使用了一段时间,但由于某些限制,它并没有在 NodeJS 中普及‬。 像 node-fetch 这样的第三方包也是采用了对 NodeJS Fetch 的自定义实现。

不过,在 Node v17.5 中,fetch 作为一项实验性功能被引入,并且在 LTS(长期支持)版本 v18.12 中被集成。因此,是时候深入学习fetch了。

2.Nodejs的fetch与客户端 Fetch 的区别

标题‬为什么‬说‬Node中‬的‬fetch不是‬浏览器‬的‬fetch呢‬?那是由于‬与‬浏览器‬fetch相比‬有以下‬明显‬不同‬:

  • NodeJS Fetch 中不支持跨源资源共享(Cross-Origin Resource Sharing)和内容安全策略(Content Security Policy ),由于在服务器上下文中不需要它们。
  • 从服务器进行 调用时没有禁止标头(forbidden headers)的概念。
  • 服务器端 cookie 存储尚未出目前 NodeJS Fetch 中。
  • NodeJS Fetch 没有服务器端缓存。
  • 服务器端 Fetch 中不存在某些请求对象方法,例如 request.formData()。

3.Nodejs 中 Fetch 基本用法

3.1 查看 Node 版本

Fetch API 在 NodeJS v17.5 及更高版本中才存在。 因此,如果要使用 Fetch API,只需升级到 Node v17.5 或更高版本。 但是,提议升级到 Node v18,由于它是最新的 LTS(长期支持),因此更稳定。

node - v;

3.2 使用 fetch 发送 Get/Post 请求

const url = 'https://jsonplaceholder.typicode.com/todos/1';

fetch(url)
  .then((response) => response.json())
  .then((jsonData) => console.log(jsonData));

在上面的代码中,进行了 GET 调用。 fetch() 方法返回一个 promise,它在解析后返回一个响应对象。 在第一个 then() 块中,从响应对象中提取 JSON 数据,该对象再返回一个提供 API 调用返回数据的 Promise,并将其打印到控制台。 当然,还可以使用 async/await 来处理 Promise:

async function getData() {
  const url = 'https://jsonplaceholder.typicode.com/todos/1';
  const response = await fetch(url);
  const jsonResponse = await response.json();
  console.log(jsonResponse);
}
getData();

如果要发送 Post 请求,可以通过为 fetch 方法提供第二个参数,同时将 method 设置为 Post 即可。而 headers、body 等用法与浏览器发送 fetch 请求一致。

async function postData() {
  //此数据将通过 POST 请求发送到服务器。
  const todoObject = {
    userId: 111,
    title: 'Some title',
    completed: false,
  };
  const options = {
    method: 'POST',
    body: JSON.stringify(todoObject),
    headers: { 'Content-Type': 'application/json' },
  };
  const url = 'https://jsonplaceholder.typicode.com/todos';
  try {
    const response = await fetch(url, options);
    const jsonResponse = await response.json();
    //try块中处理3xx/4xx/5xx等响应代码
    console.log('JSON response', jsonResponse);
  } catch (err) {
    console.log('ERROR', err);
  }
}
postData();

开发者可以将 API 请求包装在 try/catch 块中以捕获和处理任何 API 请求异常。 需要注意的一点是:NodeJS Fetch 仅将网络连接问题视为错误,响应状态码如 3xx/4xx/5xx 都被视为非错误响应, API 请求成功, 因此需要在 try 块中处理这种问题。

4.Nodejs 中 Fetch 高级用法

4.1 流(Streams)

NodeJS Fetch 支持处理数据流,列如需要处理大图像、视频或大文本文件。 在这种情况下,总是希望将数据分成更小的块来处理。 这可以使用 Fetch API 轻松实现:

//这是一个虚拟服务,它返回一个包含 4 个数据块的流
const url = 'https://httpbin.org/stream/4';
const data = await fetch(url);
//使用 await foreach 块并在收到后打印
for await (chunk of data.body) {
  console.log('New Stream Received: ', Buffer.from(chunk).toString());
}

4.2 访问标头和其他元数据

fetch() 调用返回一个响应对象,其中包含有关响应的其他有用信息,例如标头、状态等。可以访问响应对象的这些属性,如下所示:

const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
//获取所有响应头
console.log(response.headers);
//从响应对象中获取特定的标头
console.log(response.headers.get('content-type'));
//返回响应状态码
console.log(response.status);
//返回响应状态消息
console.log(response.statusText);
//如果响应状态代码为 2xx,则返回 true
console.log(response.ok);

4.3 提取 Set-Cookie 标头

使用 Fetch API,可以直接访问响应 set-cookie 标头:

response.headers.get('set-cookie');

4.4 发送文件数据

可以使用 NodeJS Fetch API 将任何文件发布到指定 URL。下面使用 fs 模块从本地文件系统读取文件。 创建一个名为 data.txt 的文件,在其中输入一些文本并在 fs.readFile() 方法中指定该文件的相对路径。

const fs = require('fs/promises');
async function postFile() {
  //用于发送数据的虚拟 URL
  const url = 'https://httpbin.org/post';
  //从本地目录读取文件
  const fileData = await fs.readFile('./data.txt');
  //将文件发布到指定的 URL
  const response = await fetch(url, {
    method: 'POST',
    body: fileData,
  });
  //获取 JSON 响应对象
  const data = await response.json();
  console.log(data);
}

4.5 使用 AbortSignal 请求撤销

可以在需要时使用 AbortController 撤销一个或多个正在进行的 API 请求。第一,需要创建一个新的 AbortController 对象:

const controller = new AbortController();

目前,需要一个 AbortSignal 实例,它可用于与获取请求进行通信。

const signal = controller.signal;

然后可以定义中止信号的条件。为了测试,假设想在 100 毫秒后中止请求。

setTimeout(() => {
  controller.abort();
  console.log('SIGNAL ABORTED');
}, 100);

目前,需要将 AbortSignal 的实例作为选项传递给 fetch() 方法。这将控制器和信号与获取请求链接起来,并在满足预定义条件时中止信号。

const response = await fetch('https://jsonplaceholder.typicode.com/todos/1', {
  signal,
});

当通过 node 命令执行文件时,将会看到一个 AbortError,表明请求已中止。

DOMException [AbortError]: The operation was aborted.

5.Node 低版本支持 Fetch策略

5.1 node-fetch

与其在 Node.js 中实现 XMLHttpRequest 来运行浏览器特定的 Fetch polyfill,node-fetch 采用了直接从原生 http 到 fetch 的方案。

Node 原生支持fetch!不是浏览器那个fetch!

node-fetch 是 Node.js 运行时上与 window.fetch 兼容的 API 的最小代码。node-fetch 具有以下特点:

  • 与 window.fetch API 保持一致。
  • 在遵循 WHATWG fetch 规范(WHATWG fetch spec )和流规范( stream spec )实现细节时进行有意识的权衡,记录已知差异。
  • 使用原生 Promise 和 async 函数。
  • 在请求和响应中为 body 使用原生 Node 流。
  • 正确解码内容编码(gzip/deflate/brotli),并自动将字符串输出(如 res.text() 和 res.json())转换为 UTF-8。
  • 支持许多有用的扩展,例如:重定向限制、响应大小限制、用于故障排除的显式错误等等。

列如下面的 node-fetch 示例:

// fetch-polyfill.js
// 要在不导入的情况下使用 fetch() ,可以patch Node.js中的全局对象:
import fetch, {
  Blob,
  blobFrom,
  blobFromSync,
  File,
  fileFrom,
  fileFromSync,
  FormData,
  Headers,
  Request,
  Response,
} from 'node-fetch';
if (!globalThis.fetch) {
  globalThis.fetch = fetch;
  globalThis.Headers = Headers;
  globalThis.Request = Request;
  globalThis.Response = Response;
}
// index.js
import './fetch-polyfill';
// ...

5.2 cross-fetch

用于 Node.js、浏览器、 React Native 的通用 WHATWG Fetch API。 cross-fetch 真正发挥作用的场景是当一样的 JavaScript 代码库需要在不同平台上运行时。cross-fetch 具有以下明显特点:

  • 平台无关:支持浏览器、Node 或 React Native
  • 可选的 polyfill:是否要将某些内容添加到全局对象由开发者自行决定
  • 简单的接口:没有实例化,没有配置,没有额外的依赖
  • WHATWG 兼容:无论代码在哪里运行,它都以一样的方式工作
  • TypeScript 支持:更好的类型开发体验。

下面作为 Ponyfill 引入:

// Using ES6 modules with Babel or TypeScript
import fetch from 'cross-fetch';
// Using CommonJS modules
const fetch = require('cross-fetch');

也可以通过 polyfill 引入:

// Using ES6 modules
import 'cross-fetch/polyfill';
// Using CommonJS modules
require('cross-fetch/polyfill');

当然,cross-fetch 也支持 CDN 方式引入:

<script src="//unpkg.com/cross-fetch/dist/cross-fetch.js"></script>

关于 polyfill、Ponyfill 等不同方案的差别可以继续在我的主页阅读相关文章。

5.3 isomorphic-fetch

isomorphic-fetch 可用于 Node 和浏览器环境,建立在 GitHub 的 WHATWG Fetch polyfill 之上。出于易于维护和向后兼容的缘由,这个库始终是一个 polyfill。作为不修改全局的“安全”替代方案,可以思考使用 fetch-ponyfill。

可以通过如下方式使用 isomorphic-fetch:

require('isomorphic-fetch');
fetch('//offline-news-api.herokuapp.com/stories')
  .then(function (response) {
    if (response.status >= 400) {
      throw new Error('Bad response from server');
    }
    return response.json();
  })
  .then(function (stories) {
    console.log(stories);
  });

注意:上面说的 fetch-ponyfill 模块将 github/fetch polyfill 包装在 CommonJS 模块中以实现浏览器化,并避免向窗口附加任何内容,而是在需要 fetch-ponyfill 时返回设置函数。而 fetch-ponyfill 在 Node 环境中使用时,所有功能都是基于 node-fetch。

import fetchPonyfill from 'fetch-ponyfill';
// node环境中将使用node-fetch
const { fetch, Request, Response, Headers } = fetchPonyfill(options);

6.Node.js 中集成 Fetch API 的好处

由于 Node 已经天然支持 fetch,这对开发人员社区超级有益。

跨服务器和客户端的一致性

以前在客户端使用过 Fetch 的前端开发人员会觉得使用一样的语法在服务器上发出 API 请求将会超级有意思。 因此,与使用外部包实现一样功能相比,fetch 变得更简单、更直观。

fetch('http://example.com/api/endpoint')
  .then((response) => {
    // 处理响应
  })
  .catch(function (err) {
    console.log('Unable to fetch -', err);
  });

速度更快

2018 年,Undici 作为更新、更快的 Node.js HTTP/1.1 客户端首次推出,支持流水线(pipelining)和池化(pooling)等核心功能。

在此基础上,经过 Node.js 核心团队的大量努力,Undici 为开发者在 Node.js 环境中真正带来了 fetch。

import { request } from 'undici';
const { statusCode, headers, trailers, body } = await request(
  'http://localhost:3000/foo'
);
console.log('response received', statusCode);
console.log('headers', headers);
for await (const data of body) {
  console.log('data', data);
}
console.log('trailers', trailers);

不需要第三方包

借助于 NodeJS 中内置的 Fetch API,其他用于一样目的的第三方包(如 node-fetch、cross-fetch 等)将不再需要。

import fetch from 'node-fetch';
import fetch from 'cross-fetch';
// node-fetch、cross-fetch不再需要

7.本文总结

本文主要和大家介绍 Nodejs对fetch的集成,文章从Fetch 介绍 、与客户端 Fetch 的区别 、Nodejs 中 Fetch 基本用法 、Nodejs 中 Fetch 高级用法 、Node 低版本支持 Fetch策略 、Node.js 中集成 Fetch API 的好处等诸多维度展开。

由于篇幅有限,文章并没有过多展开,如果有兴趣,可以在我的主页继续阅读,同时文末的参考资料提供了大量优秀文档以供学习。最后,欢迎大家点赞、评论、转发、收藏!

参考资料

https://www.scaler.com/topics/nodejs/node-js-fetch/

https://blog.logrocket.com/fetch-api-node-js/

https://undici.nodejs.org/#/

https://www.npmjs.com/package/node-fetch

https://www.npmjs.com/package/cross-fetch

https://www.npmjs.com/package/whatwg-fetch

https://github.com/github/fetch

https://www.npmjs.com/package/isomorphic-fetch

https://github.com/qubyte/fetch-ponyfill

封面图版权:来自Andrew Baisden的《The NodeJS 18 Fetch API》,链接
https://dev.to/andrewbaisden/the-nodejs-18-fetch-api-72m

文章部分内容来自Yash Solanki的《Fetch API in Node.js》

© 版权声明

相关文章

5 条评论

您必须登录才能参与评论!
立即登录
  • 头像
    根正苗红祖国好青年 读者

    注意几点和浏览器端fetch差异

    无记录
  • 头像
    奇迹爱喝雪碧 读者

    react fetch也放在一起说说吧

    无记录
  • 头像
    日常碎碎念念 读者

    以前:先查到有这个 API,然后使用它未来:先使用 GPT 生成代码,然后在 CR 时学习到有这个 API

    无记录
  • 头像
    saku_3941 投稿者

    工作流的颠覆、不过我还没用上

    无记录
  • 头像
    Qianqianjiey 投稿者

    收藏了,感谢分享

    无记录