按照本指南,你将创建一个使用 Browser Run API 和 Durable Objects 从网页截取屏幕截图并存储到 R2 的 Worker。
使用 Durable Objects 持久化浏览器会话可提升性能,消除启动新浏览器会话所需的时间。由于 Durable Objects 复用会话,它减少了所需的并发会话数。
- 注册 Cloudflare 账户 ↗。
- 安装
Node.js↗。
Node.js 版本管理器
使用 Volta ↗ 或 nvm ↗ 等 Node 版本管理器,以避免权限问题并切换 Node.js 版本。本指南后续将介绍的 Wrangler 需要 Node 版本 16.17.0 或更高。
Cloudflare Workers 提供无服务器执行环境,可创建新应用或扩展现有应用,无需配置或维护基础设施。Worker 应用是与无头浏览器交互的容器,可执行截图等操作。
运行以下命令创建名为 browser-worker 的新 Worker 项目:
npm create cloudflare@latest -- browser-workeryarn create cloudflare browser-workerpnpm create cloudflare@latest browser-worker在 browser-worker 目录中,安装 Cloudflare 的 Puppeteer fork:
npm i -D @cloudflare/puppeteeryarn add -D @cloudflare/puppeteerpnpm add -D @cloudflare/puppeteerbun add -d @cloudflare/puppeteer创建两个 R2 存储桶,一个用于生产,一个用于开发。
请注意,存储桶名称必须为小写且只能包含短横线。
wrangler r2 bucket create screenshots
wrangler r2 bucket create screenshots-test要检查存储桶是否已创建,运行:
wrangler r2 bucket list运行 list 命令后,你将看到所有存储桶名称,包括刚刚创建的。
通过添加 browser 绑定(binding) 和 Node.js 兼容性标志 配置 browser-worker 项目的 Wrangler 配置文件。Browser 绑定允许 Worker 与无头浏览器通信,从而执行截图、生成 PDF 等操作。
使用 Browser Run API 绑定、创建的 R2 存储桶和 Durable Object 更新 Wrangler 配置文件:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "rendering-api-demo",
"main": "src/index.js",
// Set this to today's date
"compatibility_date": "2026-08-17",
"compatibility_flags": ["nodejs_compat"],
"account_id": "<ACCOUNT_ID>",
// Browser Run API binding
"browser": {
"binding": "MYBROWSER",
},
// Bind an R2 Bucket
"r2_buckets": [
{
"binding": "BUCKET",
"bucket_name": "screenshots",
"preview_bucket_name": "screenshots-test",
},
],
// Binding to a Durable Object
"durable_objects": {
"bindings": [
{
"name": "BROWSER",
"class_name": "Browser",
},
],
},
"migrations": [
{
"tag": "v1", // Should be unique for each entry
"new_sqlite_classes": [
// Array of new classes
"Browser",
],
},
],
}"$schema" = "./node_modules/wrangler/config-schema.json"
name = "rendering-api-demo"
main = "src/index.js"
# Set this to today's date
compatibility_date = "2026-08-17"
compatibility_flags = [ "nodejs_compat" ]
account_id = "<ACCOUNT_ID>"
[browser]
binding = "MYBROWSER"
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "screenshots"
preview_bucket_name = "screenshots-test"
[[durable_objects.bindings]]
name = "BROWSER"
class_name = "Browser"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "Browser" ]以下代码使用 Durable Object 通过 Puppeteer 实例化浏览器。然后以不同分辨率打开一系列网页,截取每个页面的屏幕截图并上传到 R2。
Durable Object 在最后使用后保持浏览器会话打开 60 秒。如果浏览器会话已打开,任何请求将复用现有会话而非创建新会话。复制并粘贴以下代码更新 Worker:
import { DurableObject } from "cloudflare:workers";
import * as puppeteer from "@cloudflare/puppeteer";
export default {
async fetch(request, env) {
const obj = env.BROWSER.getByName("browser");
// Send a request to the Durable Object, then await its response
const resp = await obj.fetch(request);
return resp;
},
};
const KEEP_BROWSER_ALIVE_IN_SECONDS = 60;
export class Browser extends DurableObject {
browser;
keptAliveInSeconds = 0;
storage;
constructor(state, env) {
super(state, env);
this.storage = state.storage;
}
async fetch(request) {
// Screen resolutions to test out
const width = [1920, 1366, 1536, 360, 414];
const height = [1080, 768, 864, 640, 896];
// Use the current date and time to create a folder structure for R2
const nowDate = new Date();
const coeff = 1000 * 60 * 5;
const roundedDate = new Date(
Math.round(nowDate.getTime() / coeff) * coeff,
).toString();
const folder = roundedDate.split(" GMT")[0];
// If there is a browser session open, re-use it
if (!this.browser || !this.browser.isConnected()) {
console.log(`Browser DO: Starting new instance`);
try {
this.browser = await puppeteer.launch(this.env.MYBROWSER);
} catch (e) {
console.log(
`Browser DO: Could not start browser instance. Error: ${e}`,
);
}
}
// Reset keptAlive after each call to the DO
this.keptAliveInSeconds = 0;
// Check if browser exists before opening page
if (!this.browser)
return new Response("Browser launch failed", { status: 500 });
const page = await this.browser.newPage();
// Take screenshots of each screen size
for (let i = 0; i < width.length; i++) {
await page.setViewport({ width: width[i], height: height[i] });
await page.goto("https://workers.cloudflare.com/");
const fileName = `screenshot_${width[i]}x${height[i]}`;
const sc = await page.screenshot();
await this.env.BUCKET.put(`${folder}/${fileName}.jpg`, sc);
}
// Close tab when there is no more work to be done on the page
await page.close();
// Reset keptAlive after performing tasks to the DO
this.keptAliveInSeconds = 0;
// Set the first alarm to keep DO alive
const currentAlarm = await this.storage.getAlarm();
if (currentAlarm == null) {
console.log(`Browser DO: setting alarm`);
const TEN_SECONDS = 10 * 1000;
await this.storage.setAlarm(Date.now() + TEN_SECONDS);
}
return new Response("success");
}
async alarm() {
this.keptAliveInSeconds += 10;
// Extend browser DO life
if (this.keptAliveInSeconds < KEEP_BROWSER_ALIVE_IN_SECONDS) {
console.log(
`Browser DO: has been kept alive for ${this.keptAliveInSeconds} seconds. Extending lifespan.`,
);
await this.storage.setAlarm(Date.now() + 10 * 1000);
// You can ensure the ws connection is kept alive by requesting something
// or just let it close automatically when there is no work to be done
// for example, `await this.browser.version()`
} else {
console.log(
`Browser DO: exceeded life of ${KEEP_BROWSER_ALIVE_IN_SECONDS}s.`,
);
if (this.browser) {
console.log(`Closing browser.`);
await this.browser.close();
}
}
}
}import { DurableObject } from "cloudflare:workers";
import * as puppeteer from "@cloudflare/puppeteer";
interface Env {
MYBROWSER: Fetcher;
BUCKET: R2Bucket;
BROWSER: DurableObjectNamespace;
}
export default {
async fetch(request, env): Promise<Response> {
const obj = env.BROWSER.getByName("browser");
// Send a request to the Durable Object, then await its response
const resp = await obj.fetch(request);
return resp;
},
} satisfies ExportedHandler<Env>;
const KEEP_BROWSER_ALIVE_IN_SECONDS = 60;
export class Browser extends DurableObject<Env> {
private browser?: puppeteer.Browser;
private keptAliveInSeconds: number = 0;
private storage: DurableObjectStorage;
constructor(state: DurableObjectState, env: Env) {
super(state, env);
this.storage = state.storage;
}
async fetch(request: Request): Promise<Response> {
// Screen resolutions to test out
const width: number[] = [1920, 1366, 1536, 360, 414];
const height: number[] = [1080, 768, 864, 640, 896];
// Use the current date and time to create a folder structure for R2
const nowDate = new Date();
const coeff = 1000 * 60 * 5;
const roundedDate = new Date(
Math.round(nowDate.getTime() / coeff) * coeff,
).toString();
const folder = roundedDate.split(" GMT")[0];
// If there is a browser session open, re-use it
if (!this.browser || !this.browser.isConnected()) {
console.log(`Browser DO: Starting new instance`);
try {
this.browser = await puppeteer.launch(this.env.MYBROWSER);
} catch (e) {
console.log(
`Browser DO: Could not start browser instance. Error: ${e}`,
);
}
}
// Reset keptAlive after each call to the DO
this.keptAliveInSeconds = 0;
// Check if browser exists before opening page
if (!this.browser)
return new Response("Browser launch failed", { status: 500 });
const page = await this.browser.newPage();
// Take screenshots of each screen size
for (let i = 0; i < width.length; i++) {
await page.setViewport({ width: width[i], height: height[i] });
await page.goto("https://workers.cloudflare.com/");
const fileName = `screenshot_${width[i]}x${height[i]}`;
const sc = await page.screenshot();
await this.env.BUCKET.put(`${folder}/${fileName}.jpg`, sc);
}
// Close tab when there is no more work to be done on the page
await page.close();
// Reset keptAlive after performing tasks to the DO
this.keptAliveInSeconds = 0;
// Set the first alarm to keep DO alive
const currentAlarm = await this.storage.getAlarm();
if (currentAlarm == null) {
console.log(`Browser DO: setting alarm`);
const TEN_SECONDS = 10 * 1000;
await this.storage.setAlarm(Date.now() + TEN_SECONDS);
}
return new Response("success");
}
async alarm(): Promise<void> {
this.keptAliveInSeconds += 10;
// Extend browser DO life
if (this.keptAliveInSeconds < KEEP_BROWSER_ALIVE_IN_SECONDS) {
console.log(
`Browser DO: has been kept alive for ${this.keptAliveInSeconds} seconds. Extending lifespan.`,
);
await this.storage.setAlarm(Date.now() + 10 * 1000);
// You can ensure the ws connection is kept alive by requesting something
// or just let it close automatically when there is no work to be done
// for example, `await this.browser.version()`
} else {
console.log(
`Browser DO: exceeded life of ${KEEP_BROWSER_ALIVE_IN_SECONDS}s.`,
);
if (this.browser) {
console.log(`Closing browser.`);
await this.browser.close();
}
}
}
}运行 npx wrangler dev 在本地测试 Worker。
运行 npx wrangler deploy 将 Worker 部署到 Cloudflare 全球网络。