六维教程

Supabase Realtime 实时订阅

到目前为止,前端数据都是靠请求拉取,页面不会自己变。这一篇学习 Realtime,让数据库发生变化时,页面自动收到推送,不刷新也更新。

Realtime 是什么

Realtime 是数据库事件的实时推送服务。它建立一条长期连接的通道,当表里发生插入、更新、删除时,服务器立刻把变化推给所有订阅者。

体验上就是,A 用户在聊天室发了一条消息,B 用户的页面几乎同时出现这条消息,全程没有刷新,没有轮询。

轮询与推送

实现”自动更新”有两种思路

方式 原理 特点
轮询 每隔几秒主动问一次 简单但浪费请求,有延迟
推送 变化发生时服务器通知 实时,省流量,体验好

Realtime 基于 WebSocket 技术实现推送,这也是它区别于传统后端的一大亮点。在线聊天、协同编辑、通知中心,都是 Realtime 的典型场景。

开启 Realtime

新建的表默认不广播变化,需要先在 Dashboard 开启。进入 Realtime 菜单,找到你的表,打开 Database changes 下的 INSERT、UPDATE、DELETE 开关。

数据库变化通过 PostgreSQL 的 publication 机制对外发布,这个开关本质就是创建 publication,后台配置一次即可,前端无需额外开启。

订阅表变化

代码模型非常固定,一个 channel 里注册监听器,然后订阅

const channel = supabase
  .channel('posts-list')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'posts'
    },
    (payload) => {
      console.log('新文章', payload.new)
    }
  )
  .subscribe()
参数 含义
channel 名称 这条通道的标识,可以自定义
event INSERT、UPDATE、DELETE,不写表示监听全部
schema 默认 public
table 要监听的表名
payload.new 变化后的数据,删除事件里是 payload.old

监听全部事件的写法

.on(
  'postgres_changes',
  { schema: 'public', table: 'posts' },
  (payload) => {
    if (payload.eventType === 'INSERT') { /* 新数据 */ }
    if (payload.eventType === 'UPDATE') { /* payload.new 是新值 */ }
    if (payload.eventType === 'DELETE') { /* payload.old 是旧值 */ }
  }
)

在线聊天室极简版

用实时订阅做一个多人聊天室,完整感受实时效果。先建消息表并配置 RLS,任何人都能读和发

create table public.messages (
  id bigint generated always as identity primary key,
  content text not null,
  user_name text,
  created_at timestamptz default now()
);

alter table public.messages enable row level security;

create policy "消息公开读"
on public.messages for select
using (true);

create policy "登录用户可发消息"
on public.messages for insert
with check (auth.role() = 'authenticated');

前端组件订阅新消息并渲染

<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import { supabase } from '@/lib/supabase'

const messages = ref([])
const input = ref('')
let channel

onMounted(async () => {
  // 先拉一次历史消息
  const { data } = await supabase
    .from('messages')
    .select('*')
    .order('created_at', { ascending: false })
    .limit(20)
  messages.value = data || []

  // 订阅新消息
  channel = supabase
    .channel('chat-room')
    .on(
      'postgres_changes',
      { event: 'INSERT', schema: 'public', table: 'messages' },
      (payload) => {
        messages.value.unshift(payload.new)
      }
    )
    .subscribe()
})

onUnmounted(() => {
  supabase.removeChannel(channel)  // 离开页面时关闭订阅
})

async function send() {
  const { data: { user } } = await supabase.auth.getUser()
  await supabase.from('messages').insert({
    content: input.value,
    user_name: user.user_metadata.user_name || '匿名'
  })
  input.value = ''
}
</script>

<template>
  <ul>
    <li v-for="m in messages" :key="m.id">
      {{ m.user_name }}:{{ m.content }}
    </li>
  </ul>
  <input v-model="input" placeholder="说点什么" />
  <button @click="send">发送</button>
</template>

自己发送的消息不用手动插入列表,insert 触发数据库变化,订阅回调会自动把它加进列表。两条浏览器窗口并排打开,一边发消息另一边立刻出现,实时效果一目了然。

清理订阅

页面销毁时记得调用 removeChannel 释放连接,否则打开关闭页面反复操作会积累一堆无效通道。组件卸载时清理是标准写法。

Realtime 与 RLS

订阅推送同样受 RLS 约束。没有权限读某行的用户,收不到这行的变化推送。所以前面给 messages 表配了公开读策略,聊天室才能人人可见。设计订阅时先想清楚目标用户的读权限。

广播功能

postgres_changes 是数据库变化的订阅,Realtime 还支持纯广播,不经过数据库,直接给通道内其他客户端发消息,适合光标位置、正在输入这类瞬时状态

channel.send({
  type: 'broadcast',
  event: 'cursor',
  payload: { x: 100, y: 200 }
})

同类客户端用 channel.on(‘broadcast’, { event: ‘cursor’ }, cb) 接收。广播不做持久化,刷新页面就没了,与 postgres_changes 是互补关系。

最后一篇把前十一篇的内容串成一个完整的小项目。

下一篇 Supabase 综合实战

上一篇
Supabase Storage 文件存储
下一篇
Supabase 综合实战