六维教程

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 本地开发、数据库迁移、版本管理

把第十二篇的项目跑起来,你已经是一名能独立交付全栈小产品的开发者了。

上一篇 Supabase Realtime 实时订阅

上一篇
Supabase Realtime 实时订阅