用 Supabase Edge Functions + Puppeteer 连接 Browserless 实现网页截图服务
本文以开源仓库 examples/edge-functions 中的 puppeteer 示例为蓝本,讲解如何在 Supabase Edge Functions(基于 Deno)中运行 Puppeteer 无头浏览器截图,并说明为何必须借助 Browserless 这类托管浏览器服务、如何完成本地开发与生产部署。读完本文,你可以独立实现一个“传入 URL 即返回网页 PNG 截图”的 Serverless 接口,并掌握相关密钥、鉴权与调用链配置。
示例做了什么:一个可复制的“URL → PNG”截图接口
先看示例目录结构(puppeteer 函数目录):
supabase/functions/puppeteer/
├── README.md # 官方说明文档
└── index.ts # 函数入口与核心实现
示例的核心逻辑非常直观:通过查询字符串传入待截图的网页 url,函数驱动无头浏览器打开该页面并生成一张 PNG 截图返回给调用方。若省略 url 参数,代码会回退到 http://www.example.com 作为默认页面(见 index.ts)。
需要特别说明的是,官方 README 明确指出:Edge Functions 由于资源约束,无法直接运行 Headless Browser 实例。Supabase Edge Functions 以 Deno 运行时为基础、按请求冷启动并受严格的 CPU / 内存配额约束,而 Chrome 内核的单实例开销远高于普通 Serverless 函数预算。因此该示例的落地方式是:让函数扮演“编排者”,通过 WebSocket 远程连接一个托管浏览器服务(示例选用 https://browserless.io),把截图这种重型计算“外包”出去。这也是社区中“Serverless + 托管浏览器”的标准组合模式。
源码实现逐行拆解
完整实现位于 index.ts,仅 30 行,核心代码如下:
import puppeteer from 'npm:puppeteer@^25'
import { withSupabase } from 'npm:@supabase/server@^1'
// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
fetch: withSupabase({ auth: 'user' }, async (req) => {
try {
const browserWSEndpoint = `wss://chrome.browserless.io?token=${Deno.env.get(
'PUPPETEER_BROWSERLESS_IO_KEY'
)}`
// Visit browserless.io to get your free API token
const browser = await puppeteer.connect({
browserWSEndpoint,
})
const page = await browser.newPage()
const url = new URL(req.url).searchParams.get('url') || 'http://www.example.com'
await page.goto(url)
const screenshot = await page.screenshot()
return new Response(screenshot as BodyInit, {
headers: { 'Content-Type': 'image/png' },
})
} catch (e) {
console.error(e)
return Response.json({ error: e.message }, { status: 500 })
}
}),
}
关键点逐条展开:
- npm 依赖直引:Edge Function 运行在 Deno 上,但代码可直接用
npm:前缀 import npm 包。import puppeteer from 'npm:puppeteer@^25'把 Puppeteer v25 拉入函数沙箱;import { withSupabase } from 'npm:@supabase/server@^1'引入 Supabase 的官方服务端辅助库。这与仓库中import_map.json时代的https://deno.land/x/puppeteer@16.2.0老写法形成对照——新示例已切换到更贴近 npm 生态的导入方式。 - 鉴权包装器 withSupabase:
withSupabase({ auth: 'user' }, handler)要求请求必须携带有效的用户级 JWT(而非匿名 key)。这意味着该函数是“受保护端点”,与仓库 config.toml 中[functions.puppeteer]段落的verify_jwt = true设置一一对应,故其部署命令也不需要--no-verify-jwt开关。 - 远程连接而非本地启动:
puppeteer.connect({ browserWSEndpoint })是理解本示例的关键——它不会在你本地进程中 spawn Chromium,而是连接 Browserless 暴露的 WebSocket 端点。端点为wss://chrome.browserless.io?token=...,其中 token 从环境变量PUPPETEER_BROWSERLESS_IO_KEY读取。 - 兜底 URL:
new URL(req.url).searchParams.get('url') || 'http://www.example.com'解析查询参数,缺省则截 example.com,便于本地空手验证。 - 截图与响应:
page.goto(url)打开页面后,page.screenshot()返回 Buffer;随后封装为Content-Type: image/png的Response直接返回,调用方拿到的就是一张 PNG 图片。注意代码未等待networkidle,极端情况下大页面可能截到未完全渲染的画面,可按需在goto中追加{ waitUntil: 'networkidle2' }。 - 错误处理:
try/catch捕获失败路径,console.error落日志,并向客户端返回{ error }与 HTTP 500。
环境变量:密钥配置与命名坑位
从 supabase/.env.local.example 可以看到本示例预留的变量条目:
# puppeteer
PUPPETEER_BROWSERLESS_IO_TOKEN=
而源码中读取的是 PUPPETEER_BROWSERLESS_IO_KEY(见 index.ts)。两者后缀不一致(TOKEN vs KEY)是本仓库内实际存在的细节差异:模板文件写的变量名与运行时 Deno.env.get 读取的变量名并不完全相同。实操时应以运行时代码为准,在 .env.local 与线上 secrets 中设置 PUPPETEER_BROWSERLESS_IO_KEY=<你在 browserless.io 获取的 token>,同时可保留模板中的另一行以免困惑。密钥来源在代码注释与官方 README 中均有提示——访问 browserless.io 注册后可获得免费 API token。
本地开发:serve 一条命令跑起来
官方 README 给出的本地启动命令:
supabase functions serve --env-file ./supabase/.env.local --no-verify-jwt
各参数含义:
supabase functions serve:在本地以监听模式启动 Edge Functions 开发服务器;--env-file ./supabase/.env.local:把本地密钥文件注入运行环境,PUPPETEER_BROWSERLESS_IO_KEY由此生效;--no-verify-jwt:本地调试时跳过 JWT 校验,可直接用浏览器或 curl 触发。
启动后导航到本地端点即可验证:
http://localhost:54321/functions/v1/puppeteer
默认行为是给 example.com 截图;若想验证任意网页,追加查询参数即可,例如:
# 对指定网页截图
curl -o page.png "http://localhost:54321/functions/v1/puppeteer?url=https://supabase.com"
仓库在 examples/edge-functions/README.md 中补充了更完整的本地开发前置步骤,可一并参考:先执行 supabase start(确保 Docker 守护进程运行),再 cp ./supabase/.env.local.example ./supabase/.env.local 生成本地密钥文件并按需填值,最后才启动 supabase functions serve。
部署到云端:鉴权保持开启
与本地调试不同,正式部署时不需要 --no-verify-jwt,因为该函数本就要求用户级鉴权:
supabase functions deploy puppeteer --no-verify-jwt
——注意这是官方 README 给出的示例命令,实际使用时请根据你的鉴权策略二选一:
- 保持 verify_jwt = true(推荐,与 config.toml 一致):直接执行
supabase functions deploy puppeteer,函数入口依赖 Supabase Auth 的 JWT,前端可用 supabase-js 客户端的.functions.invoke('puppeteer')携带用户会话调用; - 放开鉴权做公开服务:可去掉 config.toml 中
verify_jwt的true值或临时加--no-verify-jwt部署,但需自行评估被刷量/滥用风险——截图接口会消耗 Browserless 配额,公开暴露前务必三思。
部署前的标准工序(来自仓库顶层 examples/edge-functions/README.md):
supabase login # 登录 CLI(Dashboard 生成 Access Token)
supabase link --project-ref your-project-ref # 关联远端项目
supabase secrets set --env-file ./supabase/.env.local # 同步密钥到生产
supabase secrets list # 校验密钥是否写入
supabase functions deploy puppeteer # 部署函数
其中 supabase secrets set 是把本地 .env.local 中的 PUPPETEER_BROWSERLESS_IO_KEY 推送到云端 Secrets 的关键步骤;生产环境建议单独维护一份生产密钥文件,与本地密钥隔离。
调用结果与同类函数横向参照
生产部署并完成一次调用后,得到的响应体即为 image/png 截图,可直接以图片形式展示或二次落库。若需把截图存进 Supabase Storage,可参考仓库内 og-image-with-storage-cdn 与 tweet-to-image 两个同族示例,它们展示了“渲染 → 上传 Bucket → CDN 分发”的完整链路,可作为本示例从“返回二进制”升级为“返回公网图片 URL”的直接参考。另需注意,示例目录中还存在基于 og_edge / satori 的 opengraph 方案,其不依赖完整浏览器、冷启动更快——当你的目标是生成固定模板的社交媒体卡片而非通用网页截图时,后者是更轻量的替代路线。
常见问题与边界提醒
- 截图超时或空白:Puppeteer
connect走 WebSocket 远程浏览器,若目标站点过慢,建议为page.goto配置waitUntil与超时,并将函数超时阈值同步调大。 - 密钥命名不一致:以
.env.local.example中PUPPETEER_BROWSERLESS_IO_TOKEN为模板建文件后,务必补充运行时代码真正读取的PUPPETEER_BROWSERLESS_IO_KEY(后缀为 KEY),否则会得到 500 与 token 缺失错误。 - 配额与费用:Browserless 免费额度有限,截图请求消耗对方服务资源,公开暴露的接口可能被恶意遍历消耗配额。
- 鉴权必须配合:结合 config.toml 的
verify_jwt = true,生产调用建议通过 supabase-js 的invoke携带用户令牌,而不是裸奔的匿名请求。
小结
这个 30 行的示例完整展示了一条高价值模式:Deno Edge Function 不自己开浏览器,而是通过 puppeteer.connect 把渲染任务交给 Browserless 托管浏览器,再把 PNG 截图以流式响应回传。从源码(index.ts)到密钥模板(.env.local.example)、从 config.toml 鉴权到 CLI 部署命令,仓库给出了可直接复制运行的完整闭环。若你的业务需要“网页快照、链接预览、定时巡检页面渲染结果”,照此模式即可在 Supabase 体系中快速产出自己的截图服务。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00