Supabase 集成
约 2069 字大约 7 分钟
2025-08-16
概述
Evoliant 前端通过 @nuxtjs/supabase 模块与 Supabase 深度集成,提供了类型安全的数据库操作、实时数据同步、认证管理等功能。
技术栈
- @nuxtjs/supabase v1.6.0: Nuxt 官方 Supabase 集成
- @supabase/supabase-js v2.54.0: Supabase JavaScript 客户端
- TypeScript: 完整的数据库类型安全
- Vue 3 Composition API: 响应式数据操作
配置设置
Nuxt 配置
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/supabase'],
supabase: {
url: process.env.SUPABASE_URL,
key: process.env.SUPABASE_ANON_KEY,
// SSR 支持
useSsrCookies: true,
// 自动重定向
redirect: true,
redirectOptions: {
login: '/login',
callback: '/confirm',
exclude: ['/', '/login', '/register', '/confirm'],
saveRedirectToCookie: true
},
// Cookie 配置
cookieOptions: {
maxAge: 60 * 60 * 8, // 8 小时
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
httpOnly: false
},
// 类型定义文件
types: './app/types/database.types.ts'
},
// 运行时配置
runtimeConfig: {
supabaseServiceKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
public: {
supabaseUrl: process.env.SUPABASE_URL,
supabaseAnonKey: process.env.SUPABASE_ANON_KEY
}
}
})环境变量
# .env
SUPABASE_URL=http://127.0.0.1:54321
SUPABASE_ANON_KEY=your_anon_key_here
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_here类型系统
数据库类型生成
# 生成 TypeScript 类型定义
npx supabase gen types typescript --local > app/types/database.types.ts类型定义结构
// types/database.types.ts
export interface Database {
public: {
Tables: {
profiles: {
Row: {
id: string
email: string
full_name: string
role: 'student' | 'teacher' | 'admin'
avatar_url?: string
created_at: string
updated_at?: string
}
Insert: {
id: string
email: string
full_name: string
role: 'student' | 'teacher' | 'admin'
avatar_url?: string
created_at?: string
updated_at?: string
}
Update: {
id?: string
email?: string
full_name?: string
role?: 'student' | 'teacher' | 'admin'
avatar_url?: string
updated_at?: string
}
}
courses: {
Row: {
id: string
name: string
code: string
description?: string
subject_id: string
difficulty_level?: number
is_active: boolean
created_at: string
updated_at?: string
}
Insert: {
id?: string
name: string
code: string
description?: string
subject_id: string
difficulty_level?: number
is_active?: boolean
created_at?: string
updated_at?: string
}
Update: {
id?: string
name?: string
code?: string
description?: string
subject_id?: string
difficulty_level?: number
is_active?: boolean
updated_at?: string
}
}
// ... 其他表的类型定义
}
}
}
// 重新导出类型
export type Tables<T extends keyof Database['public']['Tables']> =
Database['public']['Tables'][T]['Row']
export type TablesInsert<T extends keyof Database['public']['Tables']> =
Database['public']['Tables'][T]['Insert']
export type TablesUpdate<T extends keyof Database['public']['Tables']> =
Database['public']['Tables'][T]['Update']组合式函数
类型安全的 Supabase 客户端
// composables/useSupabase.ts
import type { Database } from '@/types'
/**
* 类型安全的 Supabase 客户端
*/
export const useTypedSupabaseClient = () => {
return useSupabaseClient<Database>()
}
/**
* 类型安全的用户获取
*/
export const useTypedSupabaseUser = () => {
return useSupabaseUser()
}
/**
* 类型安全的会话获取
*/
export const useTypedSupabaseSession = () => {
return useSupabaseSession()
}实时数据订阅
// composables/useSupabase.ts
/**
* 实时数据订阅工具函数
*/
export const useSupabaseRealtime = <T = any>(
tableName: keyof Database['public']['Tables'],
callback: (payload: any) => void
) => {
const client = useTypedSupabaseClient()
const channel = client
.channel(`public:${tableName}`)
.on('postgres_changes',
{ event: '*', schema: 'public', table: tableName as string },
callback
)
.subscribe()
// 清理函数
const unsubscribe = () => {
client.removeChannel(channel)
}
// 在组件卸载时自动取消订阅
onUnmounted(() => {
unsubscribe()
})
return { unsubscribe }
}数据操作模式
查询数据
// 基础查询
const loadCourses = async () => {
const supabase = useTypedSupabaseClient()
const { data: courses, error } = await supabase
.from('courses')
.select('*')
.eq('is_active', true)
.order('created_at', { ascending: false })
if (error) {
console.error('查询课程失败:', error)
return []
}
return courses
}
// 复杂查询 - 关联查询
const loadCoursesWithDetails = async () => {
const supabase = useTypedSupabaseClient()
const { data: courses, error } = await supabase
.from('courses')
.select(`
*,
subjects!inner(name, code),
student_course_enrollments(count),
teacher_course_assignments(count)
`)
.eq('is_active', true)
.order('created_at', { ascending: false })
if (error) {
console.error('查询课程详情失败:', error)
return []
}
return courses
}
// 分页查询
const loadCoursesWithPagination = async (page: number, pageSize: number = 10) => {
const supabase = useTypedSupabaseClient()
const from = page * pageSize
const to = from + pageSize - 1
const { data: courses, error, count } = await supabase
.from('courses')
.select('*', { count: 'exact' })
.eq('is_active', true)
.range(from, to)
.order('created_at', { ascending: false })
return {
data: courses || [],
total: count || 0,
error
}
}插入数据
// 单条插入
const createCourse = async (courseData: TablesInsert<'courses'>) => {
const supabase = useTypedSupabaseClient()
const { data: course, error } = await supabase
.from('courses')
.insert(courseData)
.select()
.single()
if (error) {
throw new Error(`创建课程失败: ${error.message}`)
}
return course
}
// 批量插入
const createMultipleCourses = async (coursesData: TablesInsert<'courses'>[]) => {
const supabase = useTypedSupabaseClient()
const { data: courses, error } = await supabase
.from('courses')
.insert(coursesData)
.select()
if (error) {
throw new Error(`批量创建课程失败: ${error.message}`)
}
return courses
}更新数据
// 单条更新
const updateCourse = async (id: string, updates: TablesUpdate<'courses'>) => {
const supabase = useTypedSupabaseClient()
const { data: course, error } = await supabase
.from('courses')
.update({
...updates,
updated_at: new Date().toISOString()
})
.eq('id', id)
.select()
.single()
if (error) {
throw new Error(`更新课程失败: ${error.message}`)
}
return course
}
// 批量更新
const updateMultipleCourses = async (updates: { id: string; data: TablesUpdate<'courses'> }[]) => {
const supabase = useTypedSupabaseClient()
const promises = updates.map(({ id, data }) =>
supabase
.from('courses')
.update({
...data,
updated_at: new Date().toISOString()
})
.eq('id', id)
.select()
.single()
)
const results = await Promise.allSettled(promises)
const successful = results
.filter((result): result is PromiseFulfilledResult<any> => result.status === 'fulfilled')
.map(result => result.value.data)
.filter(Boolean)
const failed = results
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
.map(result => result.reason)
return { successful, failed }
}删除数据
// 软删除(推荐)
const softDeleteCourse = async (id: string) => {
return await updateCourse(id, {
is_active: false,
updated_at: new Date().toISOString()
})
}
// 硬删除
const deleteCourse = async (id: string) => {
const supabase = useTypedSupabaseClient()
const { error } = await supabase
.from('courses')
.delete()
.eq('id', id)
if (error) {
throw new Error(`删除课程失败: ${error.message}`)
}
}实时功能
基础实时订阅
<script setup lang="ts">
const courses = ref<Tables<'courses'>[]>([])
// 初始加载数据
const loadCourses = async () => {
const supabase = useTypedSupabaseClient()
const { data } = await supabase
.from('courses')
.select('*')
.eq('is_active', true)
if (data) {
courses.value = data
}
}
// 实时订阅课程变化
const { unsubscribe } = useSupabaseRealtime('courses', (payload) => {
const { eventType, new: newRecord, old: oldRecord } = payload
switch (eventType) {
case 'INSERT':
if (newRecord.is_active) {
courses.value.unshift(newRecord)
}
break
case 'UPDATE':
const index = courses.value.findIndex(c => c.id === newRecord.id)
if (index !== -1) {
if (newRecord.is_active) {
courses.value[index] = newRecord
} else {
courses.value.splice(index, 1)
}
}
break
case 'DELETE':
const deleteIndex = courses.value.findIndex(c => c.id === oldRecord.id)
if (deleteIndex !== -1) {
courses.value.splice(deleteIndex, 1)
}
break
}
})
onMounted(loadCourses)
</script>高级实时订阅
// composables/useCourseRealtime.ts
export const useCourseRealtime = () => {
const courses = ref<Tables<'courses'>[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const supabase = useTypedSupabaseClient()
// 加载初始数据
const load = async () => {
try {
loading.value = true
error.value = null
const { data, error: queryError } = await supabase
.from('courses')
.select(`
*,
subjects!inner(name, code)
`)
.eq('is_active', true)
.order('created_at', { ascending: false })
if (queryError) throw queryError
courses.value = data || []
} catch (err) {
error.value = err instanceof Error ? err.message : '加载失败'
} finally {
loading.value = false
}
}
// 实时订阅
const channel = supabase
.channel('courses-changes')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'courses' },
async (payload) => {
const { eventType, new: newRecord, old: oldRecord } = payload
// 对于更新和插入,需要重新获取关联数据
if (eventType === 'INSERT' || eventType === 'UPDATE') {
const { data: fullRecord } = await supabase
.from('courses')
.select(`
*,
subjects!inner(name, code)
`)
.eq('id', newRecord.id)
.single()
if (fullRecord) {
const index = courses.value.findIndex(c => c.id === fullRecord.id)
if (eventType === 'INSERT' && fullRecord.is_active) {
courses.value.unshift(fullRecord)
} else if (eventType === 'UPDATE') {
if (index !== -1) {
if (fullRecord.is_active) {
courses.value[index] = fullRecord
} else {
courses.value.splice(index, 1)
}
} else if (fullRecord.is_active) {
courses.value.unshift(fullRecord)
}
}
}
} else if (eventType === 'DELETE') {
const index = courses.value.findIndex(c => c.id === oldRecord.id)
if (index !== -1) {
courses.value.splice(index, 1)
}
}
}
)
.subscribe()
// 清理函数
const cleanup = () => {
supabase.removeChannel(channel)
}
onUnmounted(cleanup)
return {
courses: readonly(courses),
loading: readonly(loading),
error: readonly(error),
load,
cleanup
}
}错误处理
统一错误处理
// utils/supabase-error.ts
export class SupabaseError extends Error {
constructor(
message: string,
public code?: string,
public details?: any
) {
super(message)
this.name = 'SupabaseError'
}
}
export const handleSupabaseError = (error: any): SupabaseError => {
const errorMessages: Record<string, string> = {
'23505': '数据已存在',
'23503': '关联数据不存在',
'42501': '权限不足',
'PGRST116': '数据不存在',
'PGRST301': '行级安全策略限制'
}
const message = errorMessages[error.code] || error.message || '操作失败'
return new SupabaseError(message, error.code, error.details)
}使用错误处理
const createCourseWithErrorHandling = async (courseData: TablesInsert<'courses'>) => {
try {
const supabase = useTypedSupabaseClient()
const { data: course, error } = await supabase
.from('courses')
.insert(courseData)
.select()
.single()
if (error) {
throw handleSupabaseError(error)
}
return { success: true, data: course }
} catch (error) {
console.error('创建课程失败:', error)
return {
success: false,
error: error instanceof SupabaseError ? error.message : '创建失败'
}
}
}性能优化
1. 查询优化
// 使用索引优化查询
const optimizedCourseQuery = async (subjectId?: string, difficulty?: number) => {
const supabase = useTypedSupabaseClient()
let query = supabase
.from('courses')
.select('id, name, code, description, difficulty_level')
.eq('is_active', true)
// 条件查询,利用数据库索引
if (subjectId) {
query = query.eq('subject_id', subjectId)
}
if (difficulty) {
query = query.eq('difficulty_level', difficulty)
}
const { data, error } = await query
.order('created_at', { ascending: false })
.limit(50) // 限制返回数量
return { data: data || [], error }
}2. 缓存策略
// composables/useCachedQuery.ts
export const useCachedQuery = <T>(
key: string,
queryFn: () => Promise<T>,
ttl: number = 5 * 60 * 1000 // 5分钟
) => {
const cache = new Map<string, { data: T; timestamp: number }>()
const execute = async (): Promise<T> => {
const cached = cache.get(key)
const now = Date.now()
if (cached && (now - cached.timestamp) < ttl) {
return cached.data
}
const data = await queryFn()
cache.set(key, { data, timestamp: now })
return data
}
const invalidate = () => {
cache.delete(key)
}
return { execute, invalidate }
}
// 使用缓存查询
const { execute: getCachedCourses } = useCachedQuery(
'courses-active',
() => loadCourses(),
5 * 60 * 1000 // 5分钟缓存
)3. 批量操作
// 批量插入优化
const batchInsertCourses = async (courses: TablesInsert<'courses'>[], batchSize = 100) => {
const supabase = useTypedSupabaseClient()
const results = []
for (let i = 0; i < courses.length; i += batchSize) {
const batch = courses.slice(i, i + batchSize)
const { data, error } = await supabase
.from('courses')
.insert(batch)
.select()
if (error) {
console.error(`批次 ${i / batchSize + 1} 插入失败:`, error)
continue
}
results.push(...(data || []))
}
return results
}最佳实践
1. 类型安全
// 始终使用类型化的客户端
const supabase = useTypedSupabaseClient()
// 使用类型化的查询构建器
const { data } = await supabase
.from('courses') // 自动补全表名
.select('id, name, code') // 自动补全字段名
.eq('is_active', true) // 类型检查2. 错误处理
// 统一的错误处理模式
const safeQuery = async <T>(
queryFn: () => Promise<{ data: T | null; error: any }>
): Promise<{ data: T | null; error: string | null }> => {
try {
const { data, error } = await queryFn()
if (error) {
return { data: null, error: handleSupabaseError(error).message }
}
return { data, error: null }
} catch (err) {
return {
data: null,
error: err instanceof Error ? err.message : '未知错误'
}
}
}3. 资源清理
// 确保订阅被正确清理
onUnmounted(() => {
// 清理 Supabase 订阅
channel?.unsubscribe()
// 清理其他资源
cleanup()
})4. 性能监控
// 查询性能监控
const monitoredQuery = async () => {
const startTime = performance.now()
const result = await supabase
.from('courses')
.select('*')
const endTime = performance.now()
if (endTime - startTime > 1000) {
console.warn(`慢查询检测: ${endTime - startTime}ms`)
}
return result
}调试工具
1. 开发环境日志
// 启用详细日志(开发环境)
if (process.env.NODE_ENV === 'development') {
supabase.auth.onAuthStateChange((event, session) => {
console.log('Auth state changed:', event, session)
})
}2. 查询调试
// 查询调试辅助函数
const debugQuery = async (queryBuilder: any) => {
console.log('Query:', queryBuilder.toString())
const startTime = performance.now()
const result = await queryBuilder
const endTime = performance.now()
console.log(`Query executed in ${endTime - startTime}ms`)
console.log('Result:', result)
return result
}3. 网络监控
// 监控网络状态
const { isOnline } = useOnline()
watch(isOnline, (online) => {
if (online) {
// 网络恢复,重新同步数据
syncData()
}
})版权所有
版权归属:Evoliant
许可证:MIT