实时订阅(Realtime)
场景介绍
Supabase自带Realtime WebSocket套件,无需自行开发,它是一个全自动的实时数据同步引擎。简单来说,它就像在客户端与数据库之间架设了一条“超高速公路”,一旦数据库里的数据发生变化(如增、删、改操作),客户端不需要刷新页面,也不需要主动发请求,就能秒级收到最新数据。
Supabase Realtime让客户端与服务端维持一条WebSocket长连接,服务器有数据变化或消息时,主动推送给已连接的客户端。Supabase Realtime提供以下三大功能:
- Postgres Changes:监听数据库表的INSERT/UPDATE/DELETE,表中数据变化时通知客户端。
- Broadcast:客户端之间直接互发消息,适合通知、协同编辑。
- Presence:跟踪频道里“谁在线”。
常见应用场景:
- 实时数据展示:订单量、库存、监控指标随业务发生即时刷新,无需反复刷新页面。
- 协同编辑:多人同时编辑文档或白板,彼此实时看到对方改动。
- 即时通讯:消息即时收发、在线成员列表、“对方正在输入”等聊天能力。
- 实时通知:价格变动、审核结果、系统告警第一时间送达客户端。
三项功能都由@supabase/supabase-js统一管理,连接建立、断线重连由SDK自动处理,权限由服务端RLS按登录凭证判定,开发时只需关注订阅与消息接收。
频道(channel):Realtime中一个命名的消息通道。客户端订阅某频道后,只接收该频道里流转的事件。Postgres Changes、Broadcast、Presence都依托频道传递。
Supabase Realtime的核心价值:
- 告别手动刷新:数据变化时客户端自动收到,无需轮询或手动刷新,体验接近即时通讯应用。
- 接入门槛低:连接管理、断线重连等由SDK封装,调用subscribe()即可订阅。
- 与RLS结合的权限隔离:只推送用户有权查看的行变更,无需额外写鉴权。
对不同角色,价值各有侧重:
- 独立开发者(OPC):能够以低成本实现专业级产品。仅需几行代码,即可为产品添加协同看板、实时图表等实时交互功能;长连接仅在数据变化时推送,从而显著降低CPU和带宽占用;数据写入Postgres后自动广播,数据库承担消息队列职责,无需额外维护实时组件。
- 快速上手的开发者:低门槛接入、即时获得反馈。前端几行代码即可订阅数据变更并实时更新页面;连接、鉴权、广播由Supabase托管,可专注于前端界面实现。
本章节以实时聊天室为案例,系统地介绍三大能力:消息存库并通过Postgres Changes实时同步;使用Broadcast传递“正在输入”等瞬时状态;以及通过Presence追踪在线成员名单。
配置启用
Supabase出于性能考虑,默认不把任何表的变更推给客户端,否则每张表的每次改动都会被推送,开销过大。因此需要先完成一步登记:告知数据库哪些表的变更允许对外发布。
登记的对象称为publication(发布),相当于数据库中的一份“发布清单”:列入清单的表,其变更才会被发出;未列入的表,Realtime不会推送。Supabase预置了名为supabase_realtime的publication,将表加入即可。
聊天室示例涉及表profiles与messages,详见表1。
| 表名 | 用途 | 主要字段 |
|---|---|---|
| profiles | 用户信息表,存储展示名与头像。 | id(主键,关联auth.users)、name(展示名)、avatar(头像)。 |
| messages | 消息表,存储聊天消息。 | id(主键)、room_id(房间)、user_id(发送者,关联profiles)、content(内容)、created_at(时间)。 |
建表SQL:
-- profiles:用户信息表 CREATE TABLE profiles ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, name TEXT NOT NULL, avatar TEXT, created_at TIMESTAMPTZ DEFAULT now() ); -- messages:消息表(须在 profiles 之后创建,因其引用 profiles(id)) CREATE TABLE messages ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, room_id TEXT NOT NULL, user_id UUID REFERENCES profiles(id), content TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT now() );
权限配置(RLS):聊天室需登录,因此只对authenticated放行,消息的修改与删除仅限发送者本人。
-- profiles:登录用户可读所有资料,仅可改自己 ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; CREATE POLICY "profiles_select_all" ON public.profiles FOR SELECT TO authenticated USING (true); CREATE POLICY "profiles_update_own" ON public.profiles FOR UPDATE TO authenticated USING (auth.uid() = id) WITH CHECK (auth.uid() = id); -- messages:登录用户可读所有消息,增改删限本人 ALTER TABLE messages ENABLE ROW LEVEL SECURITY; CREATE POLICY "messages_select_all" ON public.messages FOR SELECT TO authenticated USING (true); CREATE POLICY "messages_insert_own" ON public.messages FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id); CREATE POLICY "messages_update_own" ON public.messages FOR UPDATE TO authenticated USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); CREATE POLICY "messages_delete_own" ON public.messages FOR DELETE TO authenticated USING (auth.uid() = user_id);
将需要监听变更的表加入发布清单(此处监听messages,消息增删改即对外推送):
ALTER PUBLICATION supabase_realtime ADD TABLE public.messages;
被监听的表必须有主键,由于逻辑复制通过主键定位“被改的是哪一行”,没有主键便无法定位具体行,也就无法精确推送变更。
加入清单并不意味着客户端一定能收到,中间还有一道RLS权限校验。RLS是Postgres逐行判断“当前客户端是否可以看到该行”的规则。Supabase依据客户端携带的JWT(一种登录凭证)中声明的角色判断身份,而非更换key。
| 客户端角色 | 可见范围 |
|---|---|
| anon(未登录,仅用公开密钥) | 只能看到允许anon角色查看的行变更。 |
| authenticated(已登录,携带登录凭证) | 只能看到允许authenticated角色查看的行变更。 |
| service_role(使用服务端密钥) | 所有行变更(该角色带BYPASSRLS,直接绕过规则)。 |
因此一条变更要送达客户端,需先通过“发布清单”(表是否已加入),再通过RLS权限校验(RLS是否放行)。
将表加入清单后,一条变更从数据库到浏览器的路径如下:表中某行被增删改时,Postgres通过原生逻辑复制能力(数据库自带的变更通知机制)将变更打包发出;Realtime服务订阅了supabase_realtime这个发布,收到变更后经WebSocket长连接转发给已连接的浏览器。因此加入publication这一步,本质是让某张表的变化进入这条推送链路,未加入则变化不进入链路,客户端无法收到。
监听数据变更(Postgres Changes)
Postgres Changes用于监听数据库表的数据变化。客户端订阅一个频道、指明要监听哪个表的什么操作(如增、改、删操作),此后该表发生对应变化时,Realtime通过WebSocket将变化数据推送给回调函数。下面以聊天室的messages表为例,介绍当有人发消息(INSERT)、编辑消息(UPDATE)、撤回消息(DELETE)时,订阅者实时感知并更新界面。
订阅不会立即生效:调用subscribe()后,频道需先与服务器握手成功才表示真正上线(成功时status为SUBSCRIBED)。正式代码建议为subscribe()传入回调,确认SUBSCRIBED后再依赖监听,并对CHANNEL_ERROR、TIMED_OUT做处理。以下示例中省略该段,正式项目中建议补齐相应处理。
- 监听INSERT
新消息即时上屏:一方在聊天室发出消息,messages表新增一行,其他在线成员无需刷新即可看到这条新消息。这需要监听messages表的INSERT,即表每新增一行,Realtime便向订阅者推送一条INSERT事件。
执行以下代码创建一个频道,用on()声明监听public schema的messages表的INSERT,再调用subscribe()上线;此后该表每插入一条消息,回调触发一次,新消息的整行数据在payload.new里(含room_id、user_id、content等)。
const channel = supabase .channel('new-messages') .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, (payload) => { console.log('新消息:', payload.new) }) .subscribe() - 监听UPDATE
消息编辑同步:发送者修改了自己刚发的消息(如纠正错字),所有打开该聊天室的用户应立即看到新内容;部分场景还需突出“改动了哪些字段”。这通过监听messages表的UPDATE实现,即表中某行被修改时推送UPDATE事件,回调同时拿到修改前(payload.old)和修改后(payload.new)的数据,方便对比差异。
获取旧数据的前提:需将表的REPLICA IDENTITY(副本标识)设为FULL,否则旧值为空。Postgres默认UPDATE只记录主键、不记录旧值,开启FULL(通过ALTER TABLE messages REPLICA IDENTITY FULL;开启)才会一并带上整行旧数据。
const channel = supabase .channel('edited-messages') .on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'messages' }, (payload) => { console.log('编辑前:', payload.old) console.log('编辑后:', payload.new) }) .subscribe() - 监听DELETE
消息撤回即时移除:发送者撤回(删除)了消息,所有客户端应立刻从聊天界面移除该项,避免显示已撤回的内容。这通过监听messages表的DELETE实现,即表中某行被删除时推送DELETE事件,回调从payload.old获取被删行的数据,据此从消息列表移除。
该操作同样需要REPLICA IDENTITY设为FULL。
const channel = supabase .channel('deleted-messages') .on('postgres_changes', { event: 'DELETE', schema: 'public', table: 'messages' }, (payload) => { console.log('已撤回:', payload.old) }) .subscribe() - 监听所有事件
有时只关心“该聊天室有动静”:用于刷新本地缓存、记录审计日志等,不区分是新消息、编辑还是撤回。此时将event设为*,一次监听增、改、删三类。回调里用payload.eventType判断本次是INSERT、UPDATE还是DELETE,再用payload.new或payload.old取对应数据。
代码中“payload.new || payload.old”是一种实用写法:INSERT有“new”无“old”、DELETE有“old”无“new”,使用该写法无论哪种情况都能取到对应数据。
const channel = supabase .channel('all-messages') .on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, (payload) => { console.log('变更类型:', payload.eventType) console.log('变更数据:', payload.new || payload.old) }) .subscribe() - 带过滤条件的监听
聊天室按房间(room)划分,客户端通常只关心当前所在房间的消息,不希望接收其他房间的变动。这通过filter限定监听范围,让服务端只推送满足条件的行,减少无关回调。
以下只监听messages表中room_id为“room-1001”的变更,即仅当该房间的消息发生变动时才通知。
const channel = supabase .channel('room-1001-messages') .on('postgres_changes', { event: '*', schema: 'public', table: 'messages', filter: 'room_id=eq.room-1001' }, (payload) => { console.log('房间消息变更:', payload.new || payload.old) }) .subscribe()filter格式为:<列名>=<操作符>.<值>,支持的操作符如表3所示。
表3 filter支持的操作符 操作符
含义
示例
eq
等于
room_id=eq.room-1001
neq
不等于
room_id=neq.room-1001
gt
大于
created_at=gt.2026-01-01
gte
大于等于
created_at=gte.2026-01-01
lt
小于
created_at=lt.2026-01-01
lte
小于等于
created_at=lte.2026-01-01
in
包含
room_id=in.(room-1001,room-1002)
以上内容聚焦单一场景分频道演示,实际项目中,一个频道可用多个.on()同时监听不同表与事件,无需为每类变更各开一个频道。
消息广播(Broadcast)
监听数据变更(Postgres Changes)是“数据库发生变化时通知你”,Broadcast则相反,是“其他客户端发出消息时通知你”。发送方将消息投递到频道,Realtime立即转发给该频道所有在线订阅者。需要注意:消息只发送给此刻在线的客户端,不持久化保存;数据不经过数据库,仅在客户端之间经Realtime中转。聊天室里的“对方正在输入…”提示正是典型用法:输入状态瞬息变化、无需落库,用Broadcast在客户端之间即时传递。下面以聊天室为例,介绍Broadcast的发送与接收。
- 创建频道并监听
场景:用户在输入框打字时,需要使同房间其他人看到“对方正在输入…”提示,停止输入则提示消失。此类瞬时状态无需写入数据库、也不必留存,使用Broadcast在客户端之间直接互发。
以下创建一个广播频道并监听typing事件:channel()创建频道并完成配置,on()登记要监听的事件名,subscribe()上线。配置中self: false表示发送方收不到自己发出的广播,避免自己打字时也提示自己;如果希望收到自己的消息,则设为true(例如调试时)。此处presence:false仅为聚焦Broadcast演示,实际聊天室的同一频道可同时启用broadcast与presence。
const channel = supabase.channel('chat:room-1001', { config: { broadcast: { self: false }, // 不接收自己发送的消息 presence: false } }) channel.on('broadcast', { event: 'typing' }, (payload) => { console.log('收到输入状态:', payload) }) channel.subscribe() - 发送广播消息
用户开始或停止输入时,向频道发送一条typing广播,通知同房间其他人。通过channel.send()发送:type固定为'broadcast',event为自定义事件名(用于与监听端匹配、区分不同类型的消息),payload为携带的任意数据(此处带用户名与是否正在输入)。服务端收到后转发给该频道在线订阅者。
await channel.send({ type: 'broadcast', event: 'typing', payload: { name: '小张', typing: true } })
在线状态(Presence)
监听数据变更(Postgres Changes)解决了消息持久化与实时同步,消息广播(Broadcast)解决了瞬时状态互发,但二者都无法回答“此刻频道里有哪些人在线”。Presence正是用于补充这一能力,跟踪频道中的在线用户。
底层采用CRDT:一种多端状态同步算法,保证各客户端最终看到一致的在线名单。聊天室的在线成员列表、“xx加入了聊天”提示即由它驱动。成员的展示名与头像取自配置启用中定义的profiles表,是否在线、加入、离开等状态仅在频道内同步,不入库。
完整聊天室中,同一频道可同时承载Broadcast与Presence,在config中同时配置二者即可。下面以聊天室为例,介绍Presence功能。
- 上线并注册状态。
用户进入聊天页,需立即将自己加入其他人的“在线成员”列表。做法是订阅聊天频道,订阅成功后调用track()登记自身状态(展示名、头像)。
- 创建频道时通过presence.key为当前用户指定唯一标识,用于区分同一用户的多个设备(手机和电脑同时在线仍算同一人)。
- 需在subscribe()的回调中确认status === 'SUBSCRIBED'后再track(),否则频道尚未握手成功,状态无法登记。
以下示例中name与avatar取自profiles表当前用户记录。
const channel = supabase.channel('chat:room-1001', { config: { presence: { key: 'user-42' } } }) channel.subscribe(async (status) => { if (status === 'SUBSCRIBED') { await channel.track({ name: '小张', avatar: '力' }) } }) - 同步在线成员列表。
自己上线后,还需获取频道成员列表以渲染在线列表。这通过sync事件实现:各方状态合并完成时触发,此时调用presenceState()可获取频道中所有人的最新状态,适合一次性渲染完整在线名单。
聊天室通常这样配合:sync时全量刷新列表,再用下文的join/leave做增量更新(谁进入、谁离开)。
channel.on('presence', { event: 'sync' }, () => { const state = channel.presenceState() console.log('当前在线成员:', Object.values(state).flat().map(p => p.name)) }) - 监听成员加入与离开。
全量列表之外,还需实时感知“有人进入/有人离开”,以弹出“xx加入了聊天”的提示。这通过join和leave事件实现:join在有人上线时触发,leave在有人下线时触发,回调分别给出加入与离开的成员状态,据此向列表增删成员。
“离开”触发方式:当对方关闭页面或调用取消订阅与清理中的取消订阅时,一旦断开,Realtime即判定其下线,向其他人推送leave。
channel.on('presence', { event: 'join' }, ({ newPresences }) => { console.log('成员加入:', newPresences.map(p => p.name)) }) channel.on('presence', { event: 'leave' }, ({ leftPresences }) => { console.log('成员离开:', leftPresences.map(p => p.name)) }) - 更新自身展示状态。
成员的展示状态可能变化,例如主动切换“忙碌”或“离开”标识,供其他人看到。做法是状态变化时再调用一次track()覆盖自身状态,其他成员通过sync/join感知变化并更新列表。
注意track()是整体覆盖:每次都需传入完整状态(name、avatar、status缺一不可),而非仅提交发生变化的字段,漏传的字段会被清空。
// 切换为忙碌 await channel.track({ name: '小张', avatar: '力', status: '忙碌' }) // 切换回在线 await channel.track({ name: '小张', avatar: '力', status: '在线' })status仅为频道内展示状态(如“忙碌”、“在线”),不写入profiles表,profiles只存固定的展示名与头像。
“对方正在输入…”这种瞬息状态不适合放Presence(频繁track开销大、且需即时消失),在消息广播(Broadcast)中已用Broadcast承载。
完整聊天室示例:一个频道、一次subscribe()即可同时承载消息同步、输入提示与在线名单。
const channel = supabase.channel('chat:room-1001', {
config: {
broadcast: { self: false },
presence: { key: 'user-42' }
}
})
// 消息实时同步(Postgres Changes)
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'messages',
filter: 'room_id=eq.room-1001' },
(payload) => updateMessageList(payload)
)
// 输入提示(Broadcast)
.on('broadcast', { event: 'typing' },
(payload) => showTypingHint(payload)
)
// 在线成员(Presence)
.on('presence', { event: 'sync' },
() => renderOnlineList()
)
.subscribe() 上面示例中的updateMessageList、showTypingHint、renderOnlineList为业务侧示意函数,仅标注调用位置,需自行实现。