边缘函数
Supabase Edge Functions 是基于 Deno 的无服务器函数,运行在全球边缘节点,提供低延迟的执行环境。本章节介绍如何创建、部署和使用 Edge Functions。
边缘函数概述
Edge Functions 允许你在服务端执行自定义代码,适用于以下场景:
- 处理 Webhook 回调
- 调用第三方 API(需要保密的 API Key)
- 执行敏感的后端逻辑
- 数据验证和转换
- 定时任务
特点
- 基于 Deno:使用 TypeScript/JavaScript 编写
- 全球边缘部署:代码运行在离用户最近的节点
- 安全执行:敏感逻辑在服务端运行
- 自动扩展:无需管理服务器
创建函数
使用 CLI 创建
# 创建新函数
supabase functions new hello-world
# 这会创建文件:
# supabase/functions/hello-world/index.ts
函数结构
// supabase/functions/hello-world/index.ts
// 处理请求
Deno.serve(async (req: Request) => {
const { name } = await req.json()
const data = {
message: `Hello, ${name || 'World'}!`
}
return new Response(
JSON.stringify(data),
{
headers: {
'Content-Type': 'application/json',
'Connection': 'keep-alive'
}
}
)
})
本地开发
启动本地开发服务器:
# 启动所有服务(包括数据库)
supabase start
# 启动函数服务
supabase functions serve
# 测试函数
curl -X POST 'http://localhost:54321/functions/v1/hello-world' \
-H 'Content-Type: application/json' \
-d '{"name": "Supabase"}'
部署函数
部署单个函数
supabase functions deploy hello-world
部署所有函数
supabase functions deploy
查看函数列表
supabase functions list
查看函数日志
supabase functions logs hello-world
请求处理
获取请求信息
Deno.serve(async (req: Request) => {
// 请求方法
const method = req.method
// 请求头
const authHeader = req.headers.get('Authorization')
const contentType = req.headers.get('Content-Type')
// URL 信息
const url = new URL(req.url)
const pathname = url.pathname
const searchParams = url.searchParams
const query = Object.fromEntries(searchParams)
// 请求体
const body = await req.json()
return new Response(JSON.stringify({
method,
pathname,
query,
body
}), {
headers: { 'Content-Type': 'application/json' }
})
})
处理不同 HTTP 方法
Deno.serve(async (req: Request) => {
const method = req.method
if (method === 'GET') {
return handleGet(req)
} else if (method === 'POST') {
return handlePost(req)
} else if (method === 'PUT') {
return handlePut(req)
} else if (method === 'DELETE') {
return handleDelete(req)
}
return new Response('Method Not Allowed', { status: 405 })
})
async function handleGet(req: Request) {
const url = new URL(req.url)
const id = url.searchParams.get('id')
return new Response(JSON.stringify({ id }), {
headers: { 'Content-Type': 'application/json' }
})
}
async function handlePost(req: Request) {
const body = await req.json()
return new Response(JSON.stringify({ created: body }), {
status: 201,
headers: { 'Content-Type': 'application/json' }
})
}
返回不同状态码
// 成功响应
return new Response(JSON.stringify(data), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
// 创建成功
return new Response(JSON.stringify(data), {
status: 201,
headers: { 'Content-Type': 'application/json' }
})
// 错误响应
return new Response(JSON.stringify({ error: 'Not Found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' }
})
// 服务器错误
return new Response(JSON.stringify({ error: 'Internal Error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
})
使用 Supabase 客户端
在函数中可以使用 Supabase 客户端操作数据库:
import { createClient } from 'jsr:@supabase/supabase-js@2'
const supabaseUrl = Deno.env.get('SUPABASE_URL')!
const supabaseAnonKey = Deno.env.get('SUPABASE_ANON_KEY')!
Deno.serve(async (req: Request) => {
// 从请求头获取用户令牌
const authHeader = req.headers.get('Authorization')
const token = authHeader?.replace('Bearer ', '')
// 创建客户端
const supabase = createClient(supabaseUrl, supabaseAnonKey, {
global: {
headers: { Authorization: `Bearer ${token}` }
}
})
// 获取当前用户
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return new Response('Unauthorized', { status: 401 })
}
// 查询数据
const { data, error } = await supabase
.from('posts')
.select('*')
.eq('author_id', user.id)
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
})
}
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
})
})
使用服务角色
需要绕过 RLS 时使用服务角色:
import { createClient } from 'jsr:@supabase/supabase-js@2'
const supabaseUrl = Deno.env.get('SUPABASE_URL')!
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
Deno.serve(async (req: Request) => {
// 使用服务角色(绕过 RLS)
const supabase = createClient(supabaseUrl, supabaseServiceKey)
// 可以访问所有数据
const { data } = await supabase.from('posts').select('*')
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
})
})
环境变量
设置环境变量
# 设置环境变量
supabase secrets set API_KEY=your-api-key
# 设置多个环境变量
supabase secrets set API_KEY=key1 SECRET_KEY=key2
# 从文件设置
supabase secrets set --env-file .env
使用环境变量
Deno.serve(async (req: Request) => {
const apiKey = Deno.env.get('API_KEY')
const dbUrl = Deno.env.get('SUPABASE_URL')
// 内置环境变量
// SUPABASE_URL - 项目 URL
// SUPABASE_ANON_KEY - 匿名密钥
// SUPABASE_SERVICE_ROLE_KEY - 服务密钥
// SUPABASE_DB_URL - 数据库连接字符串
return new Response(JSON.stringify({
hasApiKey: !!apiKey
}), {
headers: { 'Content-Type': 'application/json' }
})
})
本地环境变量
创建 supabase/.env 文件:
API_KEY=local-api-key
SECRET_KEY=local-secret
调用第三方 API
Deno.serve(async (req: Request) => {
const apiKey = Deno.env.get('OPENAI_API_KEY')
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: 'gpt-3.5-turbo',
messages: [
{ role: 'user', content: 'Hello!' }
]
})
})
const data = await response.json()
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
})
})
Webhook 处理
处理来自第三方服务的 Webhook:
import { createClient } from 'jsr:@supabase/supabase-js@2'
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
Deno.serve(async (req: Request) => {
// 验证签名(以 Stripe 为例)
const signature = req.headers.get('stripe-signature')
const body = await req.text()
// 验证签名逻辑...
const event = JSON.parse(body)
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object)
break
case 'customer.created':
await handleCustomerCreated(event.data.object)
break
}
return new Response(JSON.stringify({ received: true }), {
headers: { 'Content-Type': 'application/json' }
})
})
async function handlePaymentSuccess(paymentIntent: any) {
// 更新数据库
await supabase
.from('payments')
.update({ status: 'succeeded' })
.eq('payment_intent_id', paymentIntent.id)
}
定时任务
使用 pg_cron 定时触发函数:
-- 每小时执行一次
select cron.schedule(
'hourly-cleanup',
'0 * * * *',
$$
select
net.http_post(
url := 'https://your-project.supabase.co/functions/v1/cleanup',
headers := '{"Content-Type": "application/json", "Authorization": "Bearer YOUR_SERVICE_ROLE_KEY"}'::jsonb
)
$$
);
函数处理:
Deno.serve(async (req: Request) => {
// 验证来源(可选)
const authHeader = req.headers.get('Authorization')
if (authHeader !== `Bearer ${Deno.env.get('CRON_SECRET')}`) {
return new Response('Unauthorized', { status: 401 })
}
// 执行清理任务
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// 删除过期数据
await supabase
.from('sessions')
.delete()
.lt('expires_at', new Date().toISOString())
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' }
})
})
CORS 处理
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers':
'authorization, x-client-info, apikey, content-type',
}
Deno.serve(async (req: Request) => {
// 处理预检请求
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
// 处理实际请求
const data = { message: 'Hello' }
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
})
})
错误处理
Deno.serve(async (req: Request) => {
try {
const body = await req.json()
// 验证输入
if (!body.email) {
throw new Error('Email is required')
}
// 处理逻辑
const result = await processData(body)
return new Response(JSON.stringify(result), {
headers: { 'Content-Type': 'application/json' }
})
} catch (error) {
console.error('Error:', error)
return new Response(JSON.stringify({
error: error.message
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
})
}
})
调用函数
从客户端调用
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key'
)
// 调用函数
const { data, error } = await supabase.functions.invoke('hello-world', {
body: { name: 'Supabase' }
})
console.log(data)
传递用户认证
// 函数会自动使用当前用户的令牌
const { data, error } = await supabase.functions.invoke('my-function', {
body: { some: 'data' }
})
使用 fetch 调用
const response = await fetch(
'https://your-project.supabase.co/functions/v1/hello-world',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userToken}`,
'apikey': 'your-anon-key'
},
body: JSON.stringify({ name: 'Supabase' })
}
)
const data = await response.json()
实用示例
发送邮件
Deno.serve(async (req: Request) => {
const { to, subject, body } = await req.json()
// 使用 Resend 发送邮件
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`
},
body: JSON.stringify({
from: '[email protected]',
to,
subject,
html: body
})
})
const data = await response.json()
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
})
})
图片处理
Deno.serve(async (req: Request) => {
const formData = await req.formData()
const file = formData.get('file') as File
// 读取图片
const imageBuffer = await file.arrayBuffer()
// 使用 ImageMagick 或其他库处理
// 这里简化为返回原图
return new Response(imageBuffer, {
headers: {
'Content-Type': file.type
}
})
})
生成 PDF
Deno.serve(async (req: Request) => {
const { content } = await req.json()
// 使用 PDF 生成库
// 这里返回简单文本
return new Response(content, {
headers: {
'Content-Type': 'text/plain'
}
})
})
最佳实践
函数拆分
将复杂逻辑拆分为多个函数:
supabase/functions/
├── auth/
│ └── index.ts # 认证相关
├── payments/
│ └── index.ts # 支付处理
├── notifications/
│ └── index.ts # 通知发送
└── webhooks/
├── stripe.ts # Stripe Webhook
└── github.ts # GitHub Webhook
共享代码
创建共享模块:
// supabase/functions/_shared/cors.ts
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers':
'authorization, x-client-info, apikey, content-type',
}
在函数中使用:
import { corsHeaders } from '../_shared/cors.ts'
日志记录
Deno.serve(async (req: Request) => {
console.log('Request:', req.method, req.url)
try {
const result = await processRequest(req)
console.log('Success:', result)
return new Response(JSON.stringify(result))
} catch (error) {
console.error('Error:', error)
return new Response(JSON.stringify({ error: error.message }), {
status: 500
})
}
})
下一步
掌握边缘函数后,你可以继续学习: