订阅以接收新文章的通知:

Cloudflare Workers 提供更多 NPM 包:结合 polyfill 和原生代码以支持 Node.js API

2024-09-09

6 分钟阅读时间
这篇博文也有 English繁體中文版本。

今天,我们很高兴地宣布为 Workers 和 Pages 推出改进的 Node.js 兼容性预览。更广泛的兼容性让您可以使用更多 NPM 包,并在编写 Workers 时利用 JavaScript 生态系统的优势。

我们最新版本的 Node.js 兼容性结合了我们先前努力的最佳特征。Cloudflare Workers 以某种形式支持 Node.js 有相当长一段时间了。我们于 2021 年 首次宣布 polyfill 支持 ,后来随着时间的推移不断 扩展对部分 Node.js API 的内置支持

最新的改进使其变得更好:

如要尝试,请将以下标志添加到 wrangler.toml ,并使用 Wrangler 部署您的 Worker :

compatibility_flags = ["nodejs_compat_v2"]

无法使用 nodejs_compat 导入的包(即使是作为另一个包的依赖)现已可以加载。其中包括流行的包,例如 body-parserjsonwebtoken 、 {} Gotpassportmd5knexmailparsercsv-stringifycookie-signaturestream-slice 等。

对于所有启用了现有 nodejs_compat 兼容性标志兼容性日期为 2024-09-23 或之后的 Workers,此行为很快将成为默认。在您试验改进的 Node.js 兼容性时,欢迎通过在 GitHub 提交问题来分享您的反馈

Workerd 不是 Node.js

要了解这些最新变化,我们先来简单了解一下 Workers 运行时与 Node.js 的不同之处。

Node.js 主要专为直接在主机操作系统上运行的服务而构建,是服务器端 JavaScript 的先驱。因此,它包括与主机交互所需的功能(例如 processfs ),以及各种实用模块(例如 crypto )。

Cloudflare Workers 在一个名为 workerd 的开源 JavaScript/Wasm 运行时上运行。虽然 Node.js 和 workerd 都是在 V8 上构建的 ,但 workerd 设计为在共享进程中运行不受信任的代码 ,暴露 绑定以便与其他 Cloudflare 服务进行互操作,包括  JavaScript 原生 RPC ,并尽可能使用 Web 标准 API

Cloudflare 帮助建立了  WinterCG (Web 互操作运行时社区小组) ,其旨在提高 JavaScript 运行时之间以及运行时与 Web 平台的互操作性。您可以仅使用 Web 标准 API 构建许多应用,但是当您想从 NPM 导入依赖于 Node.js API 的依赖项时,该怎么办呢?

例如,如果您在未开启Node.js 兼容性的情况下尝试导入 pg  (一个 PostgreSQL 驱动程序)……

import pg from 'pg'

当运行 wrangler dev 以构建您的 Worker 时,将看到如下错误:

✘ [ERROR] Could not resolve "events"
    ../node_modules/.pnpm/[email protected]/node_modules/pg-cloudflare/dist/index.js:1:29:
      1 │ import { EventEmitter } from 'events';
        ╵                              ~~~~~~~~
  The package "events" wasn't found on the file system but is built into node.

发生这种情况是因为 pg 包从 Node.js 导入 events 模块,而 workerd 默认情况下不提供该模块。

我们如何实现这一点?

我们的第一种方法 —— 构建时 polyfills

Polyfill 是为原生不支持某项功能的运行时添加功能的代码。这些代码通常用于为旧版浏览器提供现代 JavaScript 功能,但也可以用于服务器端运行时。

在 2022 年,我们 为 Wrangler 添加了功能, 如果您在 wrangler.toml 中设置 node_compat = true , 则可以将一些 Node.js API 的 polyfill 实现注入 Worker 中。以下代码在开启该标志时可以正常工作,但在关闭时则不行:

import EventEmitter from 'events';
import { inherits } from 'util';

这些 polyfill 本质上就是在部署 Worker 时由 Wrangler 添加到 Worker 的额外 JavaScript 代码。这个行为由 @esbuild-plugins/node-globals- polyfill 支持实现,后者本身使用  rollup-plugin-node-polyfills

这允许您导入和使用一些 NPM 包,例如 pg。然而,许多模块无法用足够快的代码进行 polyfill,或者根本无法被 polyfill。

例如, Buffer 是用于处理二进制数据的常见 Node.js API。存在支持它的 polyfill,但 JavaScript 通常并没有针对它在内部执行的操作进行优化,例如 copy、concat、子字符串搜索或转码。虽然可以用纯 JavaScript 实现,但如果底层运行时可以使用来自不同语言的基元,那么速度还会快得多。其他流行的 API,如 CryptoAsyncLocalStorageStream ,也存在类似的限制。

我们的第二种方法——在 Workers 运行时中原生支持一些 Node.js API

2023 年,我们开始将一部分 Node.js API 直接添加到 Workers 运行时中。您可以通过向 Worker 添加nodejs_compatcompatible 标志来启用这些 API ,但不能同时将 polyfills 与 node_compat = true 一起使用。

此外,在导入 Node.js API 时,必须使用 node : prefix:

import { Buffer } from 'node:buffer';

由于这些 Node.js API 直接内置于 Workers 运行时中,因此可以用 C++ 编写,这使得它们比 JavaScript polyfill 更快。像 AsyncLocalStorage 这样的 API (不能在不影响安全性和性能的情况下进行 polyfill)可以原生提供。

要求  node: prefix 使导入更加明确,并与现代 Node.js 约定保持一致。不幸的是,现有的 NPM 包可能不使用  node: 导入。例如,回顾一下上面的示例,如果您在带有  nodejs_compat 标志的 Worker 中导入流行程序包 pg,您仍然会看到以下错误:

✘ [ERROR] Could not resolve "events"
    ../node_modules/.pnpm/[email protected]/node_modules/pg-cloudflare/dist/index.js:1:29:
      1 │ import { EventEmitter } from 'events';
        ╵                              ~~~~~~~~
  The package "events" wasn't found on the file system but is built into node.

即使您启用了 nodejs_compat 兼容性标志,许多 NPM 包仍然不能在 Workers 中运行。您必须在较小的高性能 API 集(以许多 NPM 包无法访问的方式暴露)之间进行选择,而是在较大的不完整、性能较低的 API 集之间进行选择。而在 Node.js 中暴露为全局变量的 API ,例如 process ,仍然只能通过将其作为模块导入来访问。

新方式:混合模型

如果我们可以两全其美并能顺利运行,那会如何?

  • 在 Workers 运行时中直接实现的 Node.js API 子集 

  • 适用于其他大多数 Node.js API 的 Polyfill

  • 不需要 node : prefix

  • 一个简单的选择使用方式

改进后的 Node.js 兼容性就能做到这一点。

我们来看看两行代码,看起来相似,但在启用 nodejs_compat_v2 后,内部行为有所不同:

import { Buffer } from 'buffer';  // natively implemented
import { isIP } from 'net'; // polyfilled

第一行从 workerd 中的 JavaScript 模块导入 Buffer该模块由 C++ 代码支持。其他各种 Node.js 模块也类似地以 Typescript 和 C++ 的组合实现,包括  AsyncLocalStorage Crypto 。这样允许编写与 Node.js 行为相匹配的高性能代码。

请注意,导入 buffer 时不需要 node: prefix ,但使用 node:buffer 代码也能工作。

第二行导入 net,Wrangler 使用一个名为 unenv 的库自动 polyfill 。 Polyfill 和内置运行时 API 现在可以协同工作。

在以前的版本中,当您设置 node_compat = true 时,Wrangler 会在能够的情况下为每个 Node.js API 添加 polyfill,即使您的 Worker 及其依赖项都没有使用该 API。当您启用 nodejs_compat_v2compatible_flag 时,Wrangler 只会为您的 Worker 或其依赖项实际使用的 Node.js API 添加 polyfill。结果是,即使使用了 polyfill,Worker 也会很小。

对于某些 Node.js API,Workers 运行时中尚未提供原生支持,也没有 polyfill 实现。在这些情况下,unenv 会“模拟”接口。这意味着它将将该模块及其方法添加到您的 Worker,但调用该模块的方法要么不执行任何操作,要么抛出错误,并显示类似以下的消息:

[unenv] <method name> is not implemented yet!

这比看起来更重要。因为如果一个 Node.js API 被“模拟”,那么依赖它的 NPM 包仍然可以被导入。请考虑以下代码:

// Package name: my-module

import fs from "fs";

export function foo(path) {
  const data = fs.readFileSync(path, 'utf8');
  return data;
}

export function bar() {
  return "baz";
}
import { bar } from "my-module"

bar(); // returns "baz"
foo(); // throws readFileSync is not implemented yet!

以前,即使启用了现有的 nodejs_compat 兼容性标志,尝试导入 my-module 也会在构建时失败,因为无法解析 fs 模块。现在,fs 模块可以解析,不依赖于未实现的 Node.js API 的方法可以工作,而那些确实抛出错误的方法会给出更具体的错误信息——一个运行时错误,表明某个特定的 Node.js API 方法尚不支持,而不是构建时错误,指明模块无法被解析。

这就是为什么某些包从“在 Workers 上甚至不加载”转变为“可以加载,但有一些不受支持的方法”。

依然缺少 Node.js 的某个 API?模块别名来帮忙

假设您需要一个 NPM 包在 Workers 上工作,其依赖于尚未在 Workers 运行时中实现的 Node.js API ,或作为 unenv 中的 polyfill 。您可以使用模块别名来实现刚好足够正常工作的 API。

例如,假设您需要工作的 NPM 包调用 fs.readFile 。您可以通过将以下内容添加到 Worker 的 wrangler.toml 来为 fs 模块起别名:

[alias]
"fs" = "./fs-polyfill"

然后,在 fs- polyfill.js 文件中,您可以定义自己对 fs 模块的任何方法的实现:

export function readFile() {
  console.log("readFile was called");
  // ...
}

以下代码之前抛出错误信息“[unenv] readFile is not implemented yet!”,现在可以正常运行:

import { readFile } from 'fs';

export default {
  async fetch(request, env, ctx) {
    readFile();
    return new Response('Hello World!');
  },
};

您还可以使用模块别名来提供一个在 Workers 上不起作用的 NPM 包的实现,即使您只是间接依赖该 NPM 包(作为您的 Worker 的某个依赖项的依赖项)。

例如, cross-fetch 等一些 NPM 包依赖于 node-fetch ,后者在 fetch() API在内置到 Node.js 之前提供其 polyfill。 Workers 中不需要 node-fetch 包,因为 fetch() API 由 Workers 运行时提供。node-fetch 在 Workers 不能工作 ,因为它依赖的 httphttps 模块中的 Node.js API 目前不受支持。

您可以为 node-fetch 的所有导入设置别名,使其直接指向使用流行的 nolyfill 包内置于 Workers 运行时的 fetch() API :

[alias]
"node-fetch" = "./fetch-nolyfill"

在这种情况下,您的替代模块只需重新导出 Workers 运行时内置的 fetch API 即可:

export default fetch;

向 unenv 回报贡献

Cloudflare 正在为 unenv 积极做贡献。我们认为 unenv 正在以正确的方式解决跨运行时兼容性问题——它会根据您使用的 API 和针对的运行时,仅向您的应用程序添加必要的 polyfill。该项目支持也 workerd 之外的各种运行时,并且已经被包括 NuxtNitro  在内的其他流行项目使用。我们要感谢 Pooya Parsa和 unenv 的维护者,并鼓励生态系统中的其他人采用或贡献。

前进道路

目前,您可以通过在 wrangler.toml 中设置 nodejs_compat_v2 标志来启用改进的 Node.js 兼容性 。我们计划从 9 月 23 日起使该新行为成为使用 nodejs_compat 标志时的默认行为。这将需要更新 compatibility_date

我们对即将到来的 Node.js 兼容性变化感到兴奋,鼓励您今天就尝试一下。查看文档,了解如何为你的 Workers 选择加入。如果有任何反馈或错误,请通过提交问题告知我们。这样可以帮助我们识别支持中的任何错漏,确保尽可能多的 Node.js 生态系统能够在 Workers 上运行。

我们保护整个企业网络,帮助客户高效构建互联网规模的应用程序,加速任何网站或互联网应用程序抵御 DDoS 攻击,防止黑客入侵,并能协助您实现 Zero Trust 的过程

从任何设备访问 1.1.1.1,以开始使用我们的免费应用程序,帮助您更快、更安全地访问互联网。要进一步了解我们帮助构建更美好互联网的使命,请从这里开始。如果您正在寻找新的职业方向,请查看我们的空缺职位
Cloudflare WorkersNode.jsServerlessJavaScript

在 X 上关注

James M Snell|@jasnell
Igor Minar|@IgorMinar
Cloudflare|@cloudflare

相关帖子

2024年10月31日 13:00

Moving Baselime from AWS to Cloudflare: simpler architecture, improved performance, over 80% lower cloud costs

Post-acquisition, we migrated Baselime from AWS to the Cloudflare Developer Platform and in the process, we improved query times, simplified data ingestion, and now handle far more events, all while cutting costs. Here’s how we built a modern, high-performing observability platform on Cloudflare’s network. ...