本指南将展示如何将 Workers 从 Service Worker ↗ 格式迁移到 ES modules ↗ 格式。
将 Workers 迁移到 ES modules 格式有以下几个原因:
- Worker 运行速度更快。使用 service workers 时,绑定(binding)作为全局变量暴露。这意味着对于每个请求,Workers 运行时必须创建新的 JavaScript 执行上下文,这会增加开销和时间。使用 ES modules 编写的 Workers 可以在多个请求之间复用同一执行上下文。
- 实现 Durable Objects 需要使用 ES modules 的 Workers。
- D1、Workers AI、Vectorize、Workflows 和 Images 的绑定只能在使用 ES modules 的 Workers 中使用。
- 使用 ES modules 格式时,可以逐步部署 Worker 更改。
- 您可以轻松将使用 ES modules 的 Workers 发布到
npm,以便在代码库中导入和复用 Workers。
以下示例演示一个将所有传入请求重定向到 URL 并返回 301 状态码的 Worker。
使用 Service Worker 语法,示例 Worker 如下所示:
async function handler(request) {
const base = 'https://example.com';
const statusCode = 301;
const destination = new URL(request.url, base);
return Response.redirect(destination.toString(), statusCode);
}
// Initialize Worker
addEventListener('fetch', event => {
event.respondWith(handler(event.request));
});使用 ES modules 格式的 Workers 用对象定义替换 addEventListener 语法,该对象必须是文件的默认导出(通过 export default)。上述示例代码变为:
export default {
fetch(request) {
const base = "https://example.com";
const statusCode = 301;
const source = new URL(request.url);
const destination = new URL(source.pathname, base);
return Response.redirect(destination.toString(), statusCode);
},
};绑定(binding) 允许 Workers 与 Cloudflare 开发者平台上的资源交互。
使用 ES modules 格式的 Workers 不依赖任何全局绑定。然而,Service Worker 语法在全局作用域访问绑定。
要理解绑定,请参考以下 TODO KV namespace 绑定示例。要创建 TODO KV namespace 绑定,您将:
- 创建名为
My Tasks的 KV namespace 并获取将在绑定中使用的 ID。 - 创建 Worker。
- 找到 Worker 的 Wrangler 配置文件 并添加 KV namespace 绑定:
{
"kv_namespaces": [
{
"binding": "TODO",
"id": "<ID>"
}
]
}[[kv_namespaces]]
binding = "TODO"
id = "<ID>"在以下部分中,您将在 Service Worker 和 ES modules 格式中使用绑定。
在 Service Worker 语法中,TODO KV namespace 绑定定义在 Worker 的全局作用域中。您的 TODO KV namespace 绑定可在 Worker 应用代码的任何地方使用。
addEventListener("fetch", async (event) => {
return await getTodos()
});
async function getTodos() {
// Get the value for the "to-do:123" key
// NOTE: Relies on the TODO KV binding that maps to the "My Tasks" namespace.
let value = await TODO.get("to-do:123");
// Return the value, as is, for the Response
event.respondWith(new Response(value));
}在 ES modules 格式中,绑定仅在提供给 Worker 入口点的 env 参数内可用。
要在 Worker 代码中访问 TODO KV namespace 绑定,必须从 Worker 的 fetch handler 将 env 参数传递给 getTodos 函数。
import { getTodos } from './todos'
export default {
async fetch(request, env, ctx) {
// Passing the env parameter so other functions
// can reference the bindings available in the Workers application
return await getTodos(env)
},
};以下代码表示调用 TODO KV 绑定上 get 函数的 getTodos 函数。
async function getTodos(env) {
// NOTE: Relies on the TODO KV binding which has been provided inside of
// the env parameter of the `getTodos` function
let value = await env.TODO.get("to-do:123");
return new Response(value);
}
export { getTodos }环境变量 在 ES modules 格式与 Service Worker 格式编写的代码中访问方式不同。
查看 Wrangler 配置文件 中的以下环境变量配置示例:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-worker-dev",
// Define top-level environment variables
// using the {"vars": "key": "value"} format
"vars": {
"API_ACCOUNT_ID": "<EXAMPLE-ACCOUNT-ID>"
}
}"$schema" = "./node_modules/wrangler/config-schema.json"
name = "my-worker-dev"
[vars]
API_ACCOUNT_ID = "<EXAMPLE-ACCOUNT-ID>"在 Service Worker 格式中,API_ACCOUNT_ID 定义在 Worker 应用的全局作用域中。您的 API_ACCOUNT_ID 环境变量可在 Worker 应用代码的任何地方使用。
addEventListener("fetch", async (event) => {
console.log(API_ACCOUNT_ID) // Logs "<EXAMPLE-ACCOUNT-ID>"
return new Response("Hello, world!")
})在 ES modules 格式中,环境变量通过提供给 Worker 应用入口点的 env 参数可用:
export default {
async fetch(request, env, ctx) {
console.log(env.API_ACCOUNT_ID) // Logs "<EXAMPLE-ACCOUNT-ID>"
return new Response("Hello, world!")
},
};您还可以从 cloudflare:workers 导入 env,以从代码的任何位置(包括顶层作用域)访问环境变量:
import { env } from "cloudflare:workers";
// Access environment variables at the top level
const accountId = env.API_ACCOUNT_ID;
export default {
async fetch(request) {
console.log(accountId); // Logs "<EXAMPLE-ACCOUNT-ID>"
return new Response("Hello, world!");
},
};import { env } from "cloudflare:workers";
// Access environment variables at the top level
const accountId = env.API_ACCOUNT_ID;
export default {
async fetch(request: Request): Promise<Response> {
console.log(accountId) // Logs "<EXAMPLE-ACCOUNT-ID>"
return new Response("Hello, world!")
},
};此方法对于初始化配置或从深层嵌套函数访问环境变量而无需在每个函数调用中传递 env 很有用。有关更多详情,请参阅将 env 作为全局变量导入。
要在使用 ES modules 语法编写的 Worker 中处理 Cron Trigger 事件,请实现 scheduled() 事件 handler,这相当于在 Service Worker 语法中监听 scheduled 事件。
此示例代码:
addEventListener("scheduled", (event) => {
// ...
});然后变为:
export default {
async scheduled(event, env, ctx) {
// ...
},
};Workers 通常需要访问不在 request 对象中的数据。例如,有时 Workers 使用 waitUntil 延迟执行。使用 ES modules 格式的 Workers 可以通过 context 参数访问 waitUntil。有关更多信息,请参阅 ES modules 参数。
此示例代码:
async function triggerEvent(event) {
// Fetch some data
console.log('cron processed', event.scheduledTime);
}
// Initialize Worker
addEventListener('scheduled', event => {
event.waitUntil(triggerEvent(event));
});然后变为:
async function triggerEvent(event) {
// Fetch some data
console.log('cron processed', event.scheduledTime);
}
export default {
async scheduled(event, env, ctx) {
ctx.waitUntil(triggerEvent(event));
},
};使用 Service Worker 语法编写的 Worker 由两部分组成:
- 监听
FetchEvents的事件监听器。 - 返回 Response 对象的事件 handler,该对象传递给事件的
.respondWith()方法。
当 Cloudflare 全球网络服务器之一收到与 Worker 匹配的 URL 请求时,Cloudflare 的服务器将请求传递给 Workers 运行时。这会在运行 Worker 的 isolate 中分发 FetchEvent。
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
return new Response('Hello worker!', {
headers: { 'content-type': 'text/plain' },
});
}以下是请求响应工作流的示例:
-
FetchEvent的事件监听器告诉脚本监听到达 Worker 的任何请求。事件 handler 接收event对象,其中包括event.request——一个Request对象,代表触发FetchEvent的 HTTP 请求。 -
调用
.respondWith()让 Workers 运行时拦截请求以发送自定义响应(在此示例中,是纯文本'Hello worker!')。
了解更多关于 fetch() handler 的生命周期方法。
-
event.typestring- 事件类型。这将始终返回
"fetch"。
- 事件类型。这将始终返回
-
event.requestRequest- 传入的 HTTP 请求。
event.respondWith(responseResponse|Promise): void- 请参阅
respondWith。
- 请参阅
-
event.waitUntil(promisePromise): void- 请参阅
waitUntil。
- 请参阅
-
event.passThroughOnException(): void
拦截请求并允许 Worker 发送自定义响应。
如果 fetch 事件 handler 未调用 respondWith,运行时会将事件传递给下一个注册的 fetch 事件 handler。换句话说,虽然不推荐,但这意味着可以在 Worker 内添加多个 fetch 事件 handler。
如果没有 fetch 事件 handler 调用 respondWith,则运行时会将请求转发到源站,就像 Worker 不存在一样。然而,如果没有源站——或者 Worker 本身就是源站服务器(对于 *.workers.dev 域始终如此)——则必须调用 respondWith 才能获得有效响应。
// Format: Service Worker
addEventListener('fetch', event => {
let { pathname } = new URL(event.request.url);
// Allow "/ignore/*" URLs to hit origin
if (pathname.startsWith('/ignore/')) return;
// Otherwise, respond with something
event.respondWith(handler(event));
});waitUntil 命令延长 "fetch" 事件的生命周期。它接受基于 Promise 的任务,Workers 运行时将在 handler 终止之前执行该任务,但不会阻塞响应。例如,这非常适合缓存响应或处理日志。
在 Service Worker 格式中,waitUntil 在 event 内可用,因为它是原生 FetchEvent 属性。
在 ES modules 格式中,waitUntil 移至 context 参数对象上可用。
// Format: Service Worker
addEventListener('fetch', event => {
event.respondWith(handler(event));
});
async function handler(event) {
// Forward / Proxy original request
let res = await fetch(event.request);
// Add custom header(s)
res = new Response(res.body, res);
res.headers.set('x-foo', 'bar');
// Cache the response
// NOTE: Does NOT block / wait
event.waitUntil(caches.default.put(event.request, res.clone()));
// Done
return res;
}passThroughOnException 方法在 Worker 抛出未处理异常时防止运行时错误响应。相反,脚本将故障开放(fail open) ↗,将请求代理到源站服务器,就像 Worker 从未被调用一样。
为防止 JavaScript 错误导致未捕获异常时整个请求失败,passThroughOnException() 使 Workers 运行时将控制权交给源站服务器。
在 Service Worker 格式中,passThroughOnException 添加到 FetchEvent 接口,使其在 event 内可用。
在 ES modules 格式中,passThroughOnException 在 context 参数对象上可用。
// Format: Service Worker
addEventListener('fetch', event => {
// Proxy to origin on unhandled/uncaught exceptions
event.passThroughOnException();
throw new Error('Oops');
});