Supabase Realtime 即時訂閱:打造即時協作應用
Supabase Realtime 即時訂閱:打造即時協作應用
即時功能(Real-time)是現代應用的重要特性。聊天室、協作文件、即時通知、線上遊戲……這些場景都需要伺服器能主動推送資料到客戶端。
過去實作這類功能需要自己架設 WebSocket 伺服器、管理連線狀態,相當繁瑣。Supabase Realtime 讓這件事變得非常簡單——它建立在 PostgreSQL 的邏輯複製機制(Logical Replication)之上,讓你可以訂閱資料庫的變更,當有資料新增、修改或刪除時,立刻收到通知。
Supabase Realtime 的三種機制
1. Postgres Changes
監聽資料庫表的 INSERT、UPDATE、DELETE 事件:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
)
// 訂閱 messages 表的所有變更
const channel = supabase
.channel('messages-changes')
.on(
'postgres_changes',
{
event: '*', // INSERT | UPDATE | DELETE | *
schema: 'public',
table: 'messages'
},
(payload) => {
console.log('資料變更:', payload)
if (payload.eventType === 'INSERT') {
addMessageToUI(payload.new)
} else if (payload.eventType === 'UPDATE') {
updateMessageInUI(payload.new)
} else if (payload.eventType === 'DELETE') {
removeMessageFromUI(payload.old)
}
}
)
.subscribe()
// 取消訂閱
supabase.removeChannel(channel)2. Broadcast
讓客戶端之間互相傳送訊息,不經過資料庫:
// 建立頻道
const channel = supabase.channel('room:game-lobby')
// 訂閱廣播
channel.on(
'broadcast',
{ event: 'cursor_move' },
(payload) => {
updateCursorPosition(payload.payload.userId, payload.payload.x, payload.payload.y)
}
)
channel.subscribe()
// 發送廣播(其他訂閱者會收到,但發送者自己不收)
await channel.send({
type: 'broadcast',
event: 'cursor_move',
payload: {
userId: currentUserId,
x: mouseX,
y: mouseY
}
})3. Presence
追蹤目前線上的使用者:
const channel = supabase.channel('room:document-123')
// 監聽 Presence 事件
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('目前線上使用者:', state)
updateOnlineUsers(state)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('使用者加入:', newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('使用者離開:', leftPresences)
})
await channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// 追蹤自己的狀態
await channel.track({
userId: currentUser.id,
username: currentUser.name,
avatar: currentUser.avatarUrl,
cursor: null
})
}
})實戰:打造即時聊天室
讓我們用 Vue 3 + Supabase Realtime 打造一個完整的聊天室:
<!-- ChatRoom.vue -->
<template>
<div class="chat-room">
<!-- 線上使用者 -->
<div class="online-users">
<span class="online-count">{{ onlineUsers.length }} 人在線</span>
<div class="user-avatars">
<img
v-for="user in onlineUsers"
:key="user.userId"
:src="user.avatar"
:title="user.username"
class="avatar"
/>
</div>
</div>
<!-- 訊息列表 -->
<div class="messages" ref="messagesEl">
<div
v-for="msg in messages"
:key="msg.id"
:class="['message', { 'own': msg.user_id === currentUser.id }]"
>
<img :src="msg.users.avatar_url" class="avatar" />
<div class="bubble">
<span class="username">{{ msg.users.username }}</span>
<p>{{ msg.content }}</p>
<time>{{ formatTime(msg.created_at) }}</time>
</div>
</div>
</div>
<!-- 輸入框 -->
<form @submit.prevent="sendMessage" class="message-form">
<input
v-model="newMessage"
placeholder="輸入訊息..."
:disabled="!isConnected"
/>
<button type="submit" :disabled="!newMessage.trim()">送出</button>
</form>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { supabase } from '@/lib/supabase'
import type { RealtimeChannel } from '@supabase/supabase-js'
interface Message {
id: string
content: string
user_id: string
created_at: string
users: {
username: string
avatar_url: string
}
}
interface OnlineUser {
userId: string
username: string
avatar: string
}
const props = defineProps<{ roomId: string; currentUser: any }>()
const messages = ref<Message[]>([])
const onlineUsers = ref<OnlineUser[]>([])
const newMessage = ref('')
const isConnected = ref(false)
const messagesEl = ref<HTMLElement>()
let channel: RealtimeChannel
onMounted(async () => {
// 載入歷史訊息
await loadMessages()
// 建立 Realtime 頻道
channel = supabase.channel(`room:${props.roomId}`)
// 監聽新訊息
channel.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `room_id=eq.${props.roomId}`
},
async (payload) => {
// 取得完整訊息(含使用者資訊)
const { data } = await supabase
.from('messages')
.select('*, users(username, avatar_url)')
.eq('id', payload.new.id)
.single()
if (data) {
messages.value.push(data)
await nextTick()
scrollToBottom()
}
}
)
// Presence:追蹤線上使用者
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState<OnlineUser>()
onlineUsers.value = Object.values(state).flat()
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
isConnected.value = true
await channel.track({
userId: props.currentUser.id,
username: props.currentUser.username,
avatar: props.currentUser.avatar_url
})
}
})
})
onUnmounted(() => {
supabase.removeChannel(channel)
})
async function loadMessages() {
const { data } = await supabase
.from('messages')
.select('*, users(username, avatar_url)')
.eq('room_id', props.roomId)
.order('created_at', { ascending: true })
.limit(50)
if (data) {
messages.value = data
await nextTick()
scrollToBottom()
}
}
async function sendMessage() {
if (!newMessage.value.trim()) return
await supabase.from('messages').insert({
room_id: props.roomId,
user_id: props.currentUser.id,
content: newMessage.value.trim()
})
newMessage.value = ''
}
function scrollToBottom() {
if (messagesEl.value) {
messagesEl.value.scrollTop = messagesEl.value.scrollHeight
}
}
function formatTime(dateStr: string) {
return new Date(dateStr).toLocaleTimeString('zh-TW', {
hour: '2-digit',
minute: '2-digit'
})
}
</script>設定 Row Level Security
在使用 Realtime 時,Row Level Security(RLS)仍然有效。確保你的資料表設定了正確的政策:
-- 啟用 RLS
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
-- 已登入的使用者可以讀取訊息
CREATE POLICY "已登入使用者可讀取訊息"
ON messages FOR SELECT
TO authenticated
USING (true);
-- 使用者只能插入自己的訊息
CREATE POLICY "使用者只能新增自己的訊息"
ON messages FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);效能考量
- 使用 filter 縮小範圍:訂閱時盡量加上
filter,避免接收不相關的變更 - 適當取消訂閱:元件卸載時務必
removeChannel,避免記憶體洩漏 - Broadcast 優先於 Postgres Changes:對於不需要持久化的資料(如游標位置),使用 Broadcast 效能更好
- 批次更新 UI:接收到大量即時更新時,考慮使用 debounce 批次更新
總結
Supabase Realtime 讓即時功能的實作門檻大幅降低。透過 Postgres Changes、Broadcast 和 Presence 三種機制,你可以覆蓋大多數即時應用的需求。結合 RLS 保障資料安全,加上 Vue 3 的響應式系統,就能快速打造出功能完整的協作應用。
分享這篇文章