更新时间:2026-10-21 GMT+08:00
分享

实时订阅(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。

表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。

表2 客户端角色与可见范围

客户端角色

可见范围

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做处理。以下示例中省略该段,正式项目中建议补齐相应处理。

  1. 监听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()
  2. 监听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()
  3. 监听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()
  4. 监听所有事件

    有时只关心“该聊天室有动静”:用于刷新本地缓存、记录审计日志等,不区分是新消息、编辑还是撤回。此时将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()
  5. 带过滤条件的监听

    聊天室按房间(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的发送与接收。

  1. 创建频道并监听

    场景:用户在输入框打字时,需要使同房间其他人看到“对方正在输入…”提示,停止输入则提示消失。此类瞬时状态无需写入数据库、也不必留存,使用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()
  2. 发送广播消息

    用户开始或停止输入时,向频道发送一条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功能。

  1. 上线并注册状态。

    用户进入聊天页,需立即将自己加入其他人的“在线成员”列表。做法是订阅聊天频道,订阅成功后调用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: '力'
        })
      }
    })
  2. 同步在线成员列表。

    自己上线后,还需获取频道成员列表以渲染在线列表。这通过sync事件实现:各方状态合并完成时触发,此时调用presenceState()可获取频道中所有人的最新状态,适合一次性渲染完整在线名单。

    聊天室通常这样配合:sync时全量刷新列表,再用下文的join/leave做增量更新(谁进入、谁离开)。

    channel.on('presence', { event: 'sync' }, () => {
      const state = channel.presenceState()
      console.log('当前在线成员:', Object.values(state).flat().map(p => p.name))
    })
  3. 监听成员加入与离开。

    全量列表之外,还需实时感知“有人进入/有人离开”,以弹出“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))
    })
  4. 更新自身展示状态。

    成员的展示状态可能变化,例如主动切换“忙碌”或“离开”标识,供其他人看到。做法是状态变化时再调用一次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为业务侧示意函数,仅标注调用位置,需自行实现。

取消订阅与清理

页面或组件卸载时要取消订阅、释放连接:removeAllChannels()表示断开所有频道,channel.unsubscribe()表示只断开指定频道。对于Presence,断开还会触发频道里其他人的leave事件。

// 取消所有频道
supabase.removeAllChannels()

// 取消指定频道
channel.unsubscribe()

建议在组件卸载或页面关闭时调用清理方法,避免连接泄漏。

相关文档