用 Supabase Edge Functions 实现流式响应:基于 streams 示例的 SSE 实战指南
本篇指南聚焦 Supabase 官方示例仓库中 streams 边缘函数(Edge Function),讲解如何在 Supabase Edge Functions 中通过 Web 标准 ReadableStream 向客户端持续推送 text/event-stream(SSE)数据。读完本文,你将掌握从本地运行、cURL 验证到云端部署一条完整的流式接口开发路径,并能基于仓库源码理解 withSupabase 鉴权包装、计时器清理等底层细节,从而把同样的模式迁移到实时通知、日志推送、AI 逐字输出等场景。
示例在仓库中的位置与定位
streams 是 examples/edge-functions 目录下众多 Supabase Edge Function 示例之一。它的完整代码只由两个文件组成:
| 文件 | 作用 |
|---|---|
| index.ts | 函数唯一入口,实现流式 SSE 响应 |
| README.md | 说明本地运行与部署两条命令 |
该目录整体布局遵循 Supabase CLI 约定:所有函数都放在 supabase/functions/<函数名>/index.ts 下,函数级配置(如是否校验 JWT)集中在 supabase/config.toml 中。可以看到 config.toml 里专门有一段:
[functions.streams]
verify_jwt = true
这表示该函数在云端默认被配置为需要校验 JWT,这也与 index.ts 中 withSupabase({ auth: 'user' }) 的鉴权设定保持一致。示例仓库还附带一个 app/ 测试客户端,可用来对本地及已部署函数发起测试请求。
源码逐行拆解:如何构建一个 SSE 流
先完整读一遍核心实现 index.ts:
import { withSupabase } from 'npm:@supabase/server@^1'
const msg = new TextEncoder().encode('data: hello\r\n\r\n')
// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
fetch: withSupabase({ auth: 'user' }, (req, ctx) => {
let timerId: number | undefined
const body = new ReadableStream({
start(controller) {
timerId = setInterval(() => {
controller.enqueue(msg)
}, 1000)
},
cancel() {
if (typeof timerId === 'number') {
clearInterval(timerId)
}
},
})
return new Response(body, {
headers: {
'Content-Type': 'text/event-stream',
},
})
}),
}
尽管只有三十来行,这段代码完整覆盖了流式边缘函数的全部关键要素。
1. withSupabase:统一的鉴权与 CORS 包装
第一行通过 npm: 前缀从 Deno 生态引入 @supabase/server 包(@^1 表示安装 1.x 最新版本)。withSupabase 是这个包的便捷包装器,它接收一个鉴权配置对象与一个处理函数:
{ auth: 'user' }表示要求调用者携带已登录用户的 JWT,函数内部才能拿到当前用户上下文;- 示例中注释也明确指出这是 "Authenticated endpoint",因此仓库 config.toml 中
[functions.streams]的verify_jwt = true与之呼应。
横向对比同仓库其他示例可以发现该 API 的三种常见用法:
{ auth: 'user' }:需要用户登录态,例如 streams、browser-with-cors;{ auth: 'none' }:完全公开的端点,例如 Auth 回调类 auth-hook-react-email-resend;{ auth: 'secret' }:面向服务端密钥调用,例如 background-upload-storage。
在 browser-with-cors 等示例中还有注释说明 "withSupabase handles CORS automatically",即该包装器同时负责处理跨域预检请求,省去手写 CORS 逻辑的工作。
2. ReadableStream:流的源头
函数返回值的核心是标准 Web API ReadableStream。它通过 start(controller) 回调在流建立后立即执行:
timerId = setInterval(() => {
controller.enqueue(msg)
}, 1000)
也就是每 1000 毫秒向流中推入一条消息 msg,形成“每秒一次”的持续输出节奏。controller.enqueue() 是把数据块(chunk)交给下游消费者的标准方式。
3. cancel():及时清理定时器
ReadableStream 支持 cancel() 钩子:当客户端主动断开连接、或上层中止读取时,该回调会被触发。示例在这里调用 clearInterval(timerId) 停止定时器。这是一个很容易被遗漏但非常重要的生产级细节——如果客户端断开后定时器仍在运行,会造成无效的持续推送,浪费边缘运行时的执行时长与资源。仓库示例通过 typeof timerId === 'number' 先判断再清理,避免重复清理。
4. TextEncoder 与 SSE 消息格式
const msg = new TextEncoder().encode('data: hello\r\n\r\n')
TextEncoder 将字符串编码为 Uint8Array,因为流的 chunk 需要是二进制或字符串数据。编码后的内容遵循 SSE(Server-Sent Events)协议:
data: hello:以data:开头定义一条消息的负载;\r\n\r\n:空行分隔符,表示一条事件结束。SSE 规范要求事件之间用空行隔开,客户端(如浏览器EventSource)正是依据这个空行把流切分成一条条独立事件。
因此任何兼容 SSE 的客户端收到的将是每隔一秒一个的 hello 事件。
5. Response 与 Content-Type: text/event-stream
最后把 ReadableStream 作为响应体返回,并显式声明:
headers: {
'Content-Type': 'text/event-stream',
}
该 MIME 类型告诉浏览器与代理:这是一个持续打开的流式响应,不应被当作普通 JSON 一次性读完。这也是 fetch 处理 SSE 时必须自行识别的关键头。示例中 fetch 方法签名带有 (req, ctx),其中 ctx 供需要访问请求上下文(如 Supabase 用户信息)的场景使用。
本地运行:从命令到请求链路
streams 示例的 README.md 给出了本地运行的最小步骤。
首先启动本地函数服务:
supabase functions serve --no-verify-jwt
serve 是 Supabase CLI 提供的本地开发命令,会以 watch 模式监听 supabase/functions 目录下的函数并在文件变更时自动重载。示例函数默认由 CLI 挂载到本机网关地址,按仓库惯例本地 API 端口为 54321(见 config.toml 中 [api] port = 54321),函数通过固定前缀 functions/v1/ 暴露。
关于 --no-verify-jwt:该参数让本地网关跳过 JWT 校验,从而允许不带 Authorization 头的裸请求直接命中函数,便于用浏览器或 cURL 快速调试。这也与同仓库根级 examples/edge-functions/README.md 中 supabase functions serve --env-file ./supabase/.env.local --no-verify-jwt 的开发约定一致。需要说明的是,serve 依赖本地 Supabase 栈(通常先用 supabase start 拉起 Docker 中的 Postgres/网关等容器),示例级目录本身并不包含 .env.local.example,如函数需要自定义环境变量,可参照根级 README 在项目内自行准备 .env 文件并通过 --env-file 指定。
按 README 建议,用 cURL 或 Postman 发起一个 GET 请求即可看到效果:
curl http://localhost:54321/functions/v1/streams
命令发出后,终端会像日志一样每秒钟打印一行 data: hello,直到你主动中断(Ctrl+C)。如果希望观察响应头,可以追加 -i:
curl -i http://localhost:54321/functions/v1/streams
返回中会出现 Content-Type: text/event-stream,证明这是一个被正确声明的 SSE 流式连接,而不是一次性返回完的普通响应。
在浏览器端消费流:前端接入方式
由于该端点默认按 SSE 格式输出,前端可用原生 EventSource 在几行内接入(仅示例性示意,需根据业务自行封装):
const source = new EventSource('/functions/v1/streams')
source.onmessage = (event) => {
console.log(event.data) // 每秒打印一次 "hello"
}
需要留意两个前提:
- 仓库 config.toml 与源码注释均表明该函数是需要用户鉴权的端点(
verify_jwt = true、auth: 'user')。EventSource无法自定义请求头,因此带 JWT 的接入需要改用fetch+ReadableStream手动解析 SSE,或先解决鉴权携带问题。 - 若仅做本地联调,
serve --no-verify-jwt下匿名即可访问;正式部署后行为取决于部署时写入的verify_jwt配置。
部署到云端
示例 README 的部署命令为:
supabase functions deploy --no-verify-jwt streams
其中 streams 是函数名(对应 supabase/functions/streams/ 目录)。这里需要特别澄清一个容易误解的点:虽然 README 用 --no-verify-jwt 便于裸 curl 直接访问,但仓库源码注释与 config.toml 都将其标记为认证端点(verify_jwt = true)。实际部署时应以你的业务安全需求为准——若希望云端强制鉴权,可去掉 --no-verify-jwt 让 config.toml 中的 verify_jwt = true 生效,此时调用方必须携带有效的用户 JWT。
完整部署前通常还需要完成以下前置步骤(详见 examples/edge-functions/README.md):
# 1. 登录并关联云端项目
supabase login
supabase link --project-ref your-project-ref
# 2. 若函数需要密钥等环境变量,先写入云端
supabase secrets set --env-file ./path/to/your.env
# 3. 确认密钥已生效
supabase secrets list
# 4. 部署指定函数
supabase functions deploy streams
仓库根级 README 还补充了两种工程化部署方式:其一,将函数级配置固化到 config.toml,例如把 verify_jwt 与 import map 等放在 [functions.<name>] 段下随项目版本管理;其二,通过 GitHub Actions 集成 supabase/setup-cli,在推送或合并到主干时自动执行 supabase functions deploy,实现 CI 化发布。
从示例出发:把流式能力用到真实场景
streams 示例演示了边缘函数流式响应的最小闭环,这套骨架可以直接扩展为多种生产场景:
- 定时状态推送:把
setInterval换成消息队列或数据库变化轮询,向订阅者持续广播状态; - AI 逐字输出:将大模型流式 token 通过
controller.enqueue()按 SSE 格式逐块下推,用户侧实时渲染,这也是当前 AI 应用最常见的用法; - 长任务进度通知:在
start()中开启任务、用cancel()保证客户端断开即释放资源。
无论哪种场景,有两个结构性要点值得沿袭自本例:
- SSE 帧格式必须规范:每条消息以
data: <内容>\r\n\r\n结束,内容可扩展为 JSON 字符串以携带结构化数据; - 资源生命周期管理:在
cancel()中清理定时器或中止任务,避免断开连接后仍持续消耗边缘函数配额。
小结
通过 streams 这个精炼示例可以确认,Supabase Edge Functions 完全建立在 Web 标准之上:ReadableStream 负责生产数据,Response 携带 text/event-stream 声明协议,withSupabase 统一处理鉴权与 CORS,而 --no-verify-jwt 与 config.toml 的 verify_jwt 配合则给出了从本地裸调、到云端强制鉴权的完整控制手段。参考源码:函数入口、config.toml、示例根 README。将其作为模板,即可在最短时间内为自己的项目搭建一条可运行、可部署、可安全收敛的流式数据通道。
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 StartedRust0627
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