Supabase 综合实战
最后一篇把前面所有知识点串起来,做一个完整的小项目。目标是一个迷你博客,支持登录注册、发布文章、首页实时刷新、个人资料页和头像上传。数据库层用 SQL 一次建好,前端用 Nuxt 实现。
项目目标
| 功能 | 用到的知识点 | 章节 |
|---|---|---|
| 登录注册 | Auth 邮箱密码、会话 | 第 06 篇 |
| 发布文章 | 数据库插入、认证 | 第 05 篇 |
| 首页文章列表 | 多表联查、公开读策略 | 第 09 篇 |
| 实时刷新 | Realtime 订阅 | 第 11 篇 |
| 头像上传 | Storage 公开桶 | 第 10 篇 |
| 安全隔离 | RLS 策略 | 第 08 篇 |
数据库设计
在 SQL Editor 一次执行全部建表语句。profiles 表沿用第三篇的结构,加上头像字段
create table public.profiles (
id uuid primary key,
username text,
avatar_url text,
created_at timestamptz default now()
);
create table public.posts (
id bigint generated always as identity primary key,
title text not null,
content text,
user_id uuid references public.profiles (id),
created_at timestamptz default now()
);
全套 RLS 策略
文章公开读,个人资料私有,这是博客最常见的权限模型
-- 资料表,本人可见可改
alter table public.profiles enable row level security;
create policy "本人读自己的资料"
on public.profiles for select
using (auth.uid() = id);
create policy "本人更新自己的资料"
on public.profiles for update
using (auth.uid() = id)
with check (auth.uid() = id);
-- 文章表,公开读,作者管自己的文章
alter table public.posts enable row level security;
create policy "文章公开读"
on public.posts for select
using (true);
create policy "作者插入文章"
on public.posts for insert
with check (auth.uid() = user_id);
create policy "作者管理文章"
on public.posts for update
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
注意注册用户登录后需要先在自己的资料表插入一行,否则本人读自己的资料永远查不到,首页作者信息也关联不上。
注册时创建资料
登录注册组件在注册成功后,顺便插入资料行,这里沿用第六篇的代码,扩展注册分支
const { data, error } = await supabase.auth.signUp({
email: email.value,
password: password.value
})
if (data.user) {
await supabase.from('profiles').insert({
id: data.user.id,
username: email.value.split('@')[0]
})
}
注册成功后把 id 和邮箱前缀一起写入资料表,完成账号和资料的绑定。
首页文章列表
首页用联查一次拿文章和作者,再订阅新文章实时刷新
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import { supabase } from '@/lib/supabase'
const posts = ref([])
let channel
async function loadPosts() {
const { data } = await supabase
.from('posts')
.select('*, author:profiles(username, avatar_url)')
.order('created_at', { ascending: false })
posts.value = data || []
}
onMounted(async () => {
await loadPosts()
channel = supabase
.channel('home-posts')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'posts' },
(payload) => {
// 新文章插入后,联查一次把作者信息补全
loadPosts()
}
)
.subscribe()
})
onUnmounted(() => {
supabase.removeChannel(channel)
})
</script>
<template>
<article v-for="p in posts" :key="p.id">
<h3>{{ p.title }}</h3>
<p>{{ p.content }}</p>
<small>{{ p.author?.username }} 发表于 {{ p.created_at }}</small>
</article>
</template>
postgres_changes 的 payload 只带本表字段,不带关联作者信息,所以收到推送后重新执行一次联查,这是实时场景处理关联数据的常用方式。
发布文章
async function publish() {
const { data: { user } } = await supabase.auth.getUser()
const { error } = await supabase
.from('posts')
.insert({
title: title.value,
content: content.value,
user_id: user.id
})
if (error) console.log(error.message)
}
插入会触发 Realtime,首页立即出现新文章,不需要手动跳转或刷新。
个人资料页与头像
资料页从 Auth 拿当前用户,再查资料表展示。头像上传复用第十篇的完整流程
<script setup>
import { ref } from 'vue'
import { supabase } from '@/lib/supabase'
const profile = ref(null)
const fileInput = ref(null)
const { data: { user } } = await supabase.auth.getUser()
async function loadProfile() {
const { data } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.single()
profile.value = data
}
async function uploadAvatar() {
const file = fileInput.value.files[0]
if (!file) return
const fileName = `${user.id}-${Date.now()}.png`
await supabase.storage.from('avatars').upload(fileName, file)
const { data } = supabase
.storage
.from('avatars')
.getPublicUrl(fileName)
await supabase
.from('profiles')
.update({ avatar_url: data.publicUrl })
.eq('id', user.id)
loadProfile()
}
</script>
<template>
<img v-if="profile?.avatar_url" :src="profile.avatar_url" width="80" />
<p>昵称 {{ profile?.username }}</p>
<input ref="fileInput" type="file" accept="image/*" />
<button @click="uploadAvatar">换头像</button>
</template>
注意在 Dashboard 的 Storage 里创建公开的 avatars 桶,再按第十篇的 SQL 给 storage.objects 配上公开读和登录用户可上传的策略,头像才能上传和显示。
调试技巧
- 前端查不到数据时,先打开浏览器控制台看 error 内容
- 登录状态用 Supabase 的浏览器插件调试,能直接看当前会话
- SQL 写完后在 Table Editor 里核对,再用前端验证,分层排查问题
- 被 RLS 拦截的报错信息会提示权限不足,优先检查策略是否覆盖了对应操作
毕业总结
回看整个教程,你掌握了一条完整的开发链路。用 PostgreSQL 建模数据,用 supabase-js 读写,用 Auth 解决用户身份,用 RLS 守住安全底线,用 Storage 管文件,用 Realtime 做实时体验。这套组合足够支撑绝大多数独立项目的后端需求。
接下来想深入,可以从三个方向继续
| 方向 | 内容 | 难度 |
|---|---|---|
| 数据库函数 | 触发器、存储过程,实现自动化的数据逻辑 | 中 |
| Edge Functions | 在 Supabase 边缘运行自己的服务器代码 | 中 |
| Supabase CLI | 本地开发、数据库迁移、版本管理 | 中 |
把第十二篇的项目跑起来,你已经是一名能独立交付全栈小产品的开发者了。