Supabase 近几年在后端即服务(BaaS)领域热度持续上升,它基于成熟的 PostgreSQL,提供开箱即用的数据库访问、认证、文件存储和实时订阅能力,并且完全开源,可以自托管。对于 Vue 3 项目来说,集成 Supabase 不只是装个依赖、调几个 API 那么简单,要让代码在团队协作和长期维护中保持清晰,需要一套工程化的封装思路。本文将从客户端初始化、数据访问层封装、认证状态管理、实时订阅四个方面,完整讲解如何在 Vue 3 项目中工程化地使用 Supabase。

一、初始化项目与客户端封装
首先创建 Vue 3 项目并安装官方客户端库。推荐使用 Vite 作为构建工具,它与 Vue 3 的组合式 API 配合良好,开发体验也比较流畅。安装命令如下:
npm create vue@latest my-app cd my-app npm install @supabase/supabase-js
安装完成后,不要在组件里直接创建客户端实例。很多教程为了演示方便,在组件内调用 createClient,这种做法在工程化项目中会带来两个问题:一是实例难以复用,容易造成重复连接;二是无法集中管理配置和错误拦截。正确的做法是建立一个专门的模块来导出单例客户端。
在 src/lib 目录下创建 supabase.ts,将项目 URL 和匿名密钥通过环境变量注入。Vite 项目中需要在 .env.local 文件里定义 VITE_SUPABASE_URL 和 VITE_SUPABASE_ANON_KEY 两个变量,代码中通过 import.meta.env 读取:
import { createClient } from '@supabase/supabase-js'
import type { Database } from '@/types/database.types'
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY
if (!supabaseUrl || !supabaseKey) {
throw new Error('缺少 Supabase 环境变量,请检查 .env.local 配置')
}
export const supabase = createClient<Database>(supabaseUrl, supabaseKey, {
auth: {
persistSession: true,
autoRefreshToken: true,
},
})这里的 Database 类型是通过 Supabase CLI 自动生成的。执行 npx supabase gen types typescript --project-id 你的项目ID > src/types/database.types.ts 即可生成完整的类型定义文件。有了这份类型文件,后续所有查询都会获得字段级别的类型提示,这是工程化中非常关键的一步,能显著减少拼写错误和字段类型问题。
二、封装类型安全的数据访问层
直接在组件中拼接查询字符串会让数据逻辑散落在各个页面里,难以测试和复用。推荐按照业务领域建立仓储(Repository)模式的封装,每个数据表对应一个服务模块,统一暴露增删改查方法。这样组件只关心业务表现,数据细节集中在服务层处理。
以一个文章表 posts 为例,可以创建 src/services/postService.ts:
import { supabase } from '@/lib/supabase'
export interface PostQuery {
page?: number
pageSize?: number
keyword?: string
}
export const postService = {
async getList({ page = 1, pageSize = 10, keyword }: PostQuery = {}) {
let query = supabase
.from('posts')
.select('id, title, content, created_at, author:users(id, name)')
.order('created_at', { ascending: false })
.range((page - 1) * pageSize, page * pageSize - 1)
if (keyword) {
query = query.ilike('title', `%${keyword}%`)
}
const { data, error } = await query
if (error) throw new Error(error.message)
return data
},
async getById(id: string) {
const { data, error } = await supabase
.from('posts')
.select('*')
.eq('id', id)
.single()
if (error) throw new Error(error.message)
return data
},
async create(payload: { title: string; content: string }) {
const { data, error } = await supabase
.from('posts')
.insert(payload)
.select()
.single()
if (error) throw new Error(error.message)
return data
},
}这个封装有几个值得注意的设计点。第一,分页使用 range 方法而不是 limit,方便与页码逻辑对接。第二,关联查询直接写在 select 字符串里,Supabase 会自动生成表连接,这是它相对 Firebase 的显著优势——Firebase 的 NoSQL 模型做关联查询非常麻烦,而 Supabase 底层是关系型数据库,天然支持外键关联和聚合。第三,所有方法统一在出错时抛出异常,组件层可以配合统一的错误处理器做提示。
在组件中使用时,可以进一步封装一个 useAsyncData 类型的组合式函数,把加载状态和错误状态一并管理起来,避免每个页面重复写 loading 标志位。这类组合式函数内部可以基于 Vue 3 的 ref 和 watchEffect 实现请求与响应式的绑定,让数据获取代码更加声明式。
三、认证状态管理与路由守卫
Supabase 提供完整的认证体系,支持邮箱密码、魔法链接、OAuth 第三方登录等多种方式。在 Vue 3 中管理认证状态,推荐使用 Pinia 建立独立的 auth store,把登录、登出、会话监听都收敛到一处。
创建 src/stores/auth.ts:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { supabase } from '@/lib/supabase'
export const useAuthStore = defineStore('auth', () => {
const user = ref(supabase.auth.getSession() ? null : null)
const loading = ref(true)
const isLoggedIn = computed(() => !!user.value)
supabase.auth.onAuthStateChange((event, session) => {
user.value = session?.user ?? null
loading.value = false
})
async function login(email: string, password: string) {
const { error } = await supabase.auth.signInWithPassword({ email, password })
if (error) throw error
}
async function logout() {
await supabase.auth.signOut()
}
return { user, loading, isLoggedIn, login, logout }
})这里的关键是 onAuthStateChange 监听。它会在登录、登出、令牌刷新等时机触发回调,通过它驱动响应式的 user 状态,整个应用的数据流就保持单向且可预测。注意 Supabase 的会话持久化默认使用 localStorage,客户端初始化时配置了 persistSession: true 后,刷新页面不会丢失登录态。
路由保护方面,在 router.beforeEach 中结合 isLoggedIn 判断即可。需要注意的是首次加载时认证状态可能尚未恢复,所以要处理一个等待期。可以在守卫中先调用 supabase.auth.getSession() 确保会话初始化完成后再判断跳转,避免用户明明已登录却被重定向到登录页的闪跳问题。
与 Firebase Auth 相比,Supabase 的认证底层同样基于 JWT,但它的行级安全策略(Row Level Security,RLS)与认证深度绑定。你可以在数据库层面直接声明"用户只能读写自己的数据",即使前端代码有漏洞,数据库也会兜底拦截,这在安全工程上是非常有价值的设计。
四、实时订阅与性能优化
Supabase 的实时功能基于 WebSocket 实现,可以监听表级别的插入、更新、删除事件。在 Vue 3 中使用时,务必注意组件卸载时取消订阅,否则会造成内存泄漏和重复回调。推荐封装一个 useRealtime 组合式函数:
import { onUnmounted } from 'vue'
import { supabase } from '@/lib/supabase'
export function useRealtime(
table: string,
onChange: (payload: any) => void
) {
const channel = supabase
.channel(`public:${table}`)
.on('postgres_changes', { event: '*', schema: 'public', table }, onChange)
.subscribe()
onUnmounted(() => {
supabase.removeChannel(channel)
})
return channel
}在组件中调用 useRealtime('posts', payload => refreshList()),列表就会在数据变化时自动刷新,而且组件销毁时订阅会被自动清理。这种把生命周期钩子和第三方订阅绑定的写法,是组合式 API 处理外部资源的典型模式。
性能层面还有几点建议。一是启用 Row Level Security 后,所有查询都会经过策略检查,务必为高频查询的字段建立索引,避免全表扫描。二是列表页只查询需要的字段,不要习惯性使用 select('*'),PostgreSQL 传输宽表的开销不容忽视。三是图片等静态资源优先走 Supabase Storage 的 CDN,而不是通过数据库接口中转。四是生成环境记得定期重新生成类型定义文件,保持前端类型与数据库结构同步,避免类型漂移。
总结
把 Supabase 工程化地引入 Vue 3 项目,核心思路是收敛与分层:客户端单例收敛在 lib 层,数据访问收敛在服务层,认证状态收敛在 Pinia store,实时订阅收敛在组合式函数。再配合自动生成的 TypeScript 类型定义和数据库端的行级安全策略,前后端就能在类型安全和权限安全两个维度都得到保障。相比 Firebase,Supabase 凭借 PostgreSQL 的关系型能力和开源可自托管的特性,为中大型 Vue 项目提供了一个更灵活、迁移成本更低的后端选择。