跳到主要内容

边缘函数

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
})
}
})

下一步

掌握边缘函数后,你可以继续学习: