訂閱以接收新文章的通知:

Cloudflare Workers 上提供更多 NPM 套件:結合 polyfill 和原生程式碼以支援 Node.js API

2024-09-09

閱讀時間:6 分鐘
本貼文還提供以下語言版本:English简体中文

今天,我們很高興地宣佈,針對 Workers 和 Pages 推出了改進的 Node.js 相容性,本文將對此進行概要介紹。更廣泛的相容性可讓您在編寫 Workers 時使用更多 NPM 套件,並利用 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 或更晚日期的 Worker 的預設行為。在試驗改進的 Node.js 相容性時,可以透過在 GitHub 上提出問題來分享您的意見反應。

Workers 不是 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-interoperable Runtimes Community Group),旨在改進 JavaScript 執行階段的互通性,包括彼此之間以及與 Web 平台的互通性。您可以僅使用 Web 標準 API 建置許多應用程式,但是當您想要從依賴 Node.js API 的 NPM 匯入相依性時,該怎麼辦?

例如,如果您嘗試在未開啟 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 預設不提供該模組。

我們如何實現這一點?

我們的第一種方法——建置時 polyfill

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 執行階段。您可以透過將 nodejs_compat 相容性標誌新增至 Worker 來啟用這些 API,但不能同時使用 node_compat = true 的 polyfill。

此外,在匯入 Node.js API 時,您必須使用 node: 首碼:

import { Buffer } from 'node:buffer';

由於這些 Node.js API 直接內建在 Workers 執行階段中,因此可以用 C++ 編寫,從而比 JavaScript polyfill 更快。像 AsyncLocalStorage 這樣的 API,如果沒有安全性或效能問題就無法進行 polyfill,但可以原生提供。

需要 node: 首碼使匯入更加明確並符合現代 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: 首碼

  • 一種選擇加入的簡單方法

改進後的 Node.js 相容性正好可以做到這一點。

讓我們來看看兩行程式碼,它們看起來很相似,但現在在啟用 nodejs_compat_v2 時,其底層行為有所不同:

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

第一行從 workerd 中由 C++ 程式碼支援的 JavaScript 模組匯入 Buffer。其他各種 Node.js 模組也採用類似的 Typescript 和 C++ 組合方式實作,包括 AsyncLocalStorage Crypto。這允許產生與 Node.js 行為相符的高效能程式碼。

請注意,匯入 buffer 時不需要 node: 首碼,但該程式碼也可以使用 node:buffer

第二行匯入 Wrangler 使用名為 unenv 的庫自動 polyfill 的 net。現在,polyfill 和內建的執行階段 API 可以協同工作。

以前,當您設定 node_compat = true 時,Wrangler 會為它能夠新增的每個 Node.js API 新增 polyfill,即使您的 Worker 及其相依性都不使用該 API。當您啟用 nodejs_compat_v2 相容性標誌時,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 上運作,該套件依賴於 Node.js API,而該 API 尚未在 Workers 執行階段中實作或作為 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 相依性之一的相依性)。

例如,一些 NPM 套件(例如 cross-fetch)依賴於 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日 下午1: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. ...

2024年10月25日 下午1:00

Elephants in tunnels: how Hyperdrive connects to databases inside your VPC networks

Hyperdrive (Cloudflare’s globally distributed SQL connection pooler and cache) recently added support for directing database traffic from Workers across Cloudflare Tunnels. We dive deep on what it took to add this feature....