文档 / 搜索 / LingCode Cloud / 实时订阅
📘 参考 ● 已上线 更新于 2026-06-11

实时订阅

一句话:实时订阅让你的界面在数据库发生变化的瞬间作出反应,而不是靠轮询。订阅一张表,每当有行被插入、更新或删除,你就会收到一个事件。它通过 Server-Sent Events 投递,并且按行级安全过滤——已登录用户只会收到自己被允许看到的行。一行调用:lingcode.from('table').subscribe(onChange),它返回一个取消订阅函数,你在销毁时调用它。

多数应用靠一个定时器来伪装「实时」:每隔几秒重新拉取一次,并寄望期间没发生什么要紧的事。什么都没变时这很浪费,真有变化时又会有延迟。实时订阅把它反了过来——服务器在行变化的那一刻就把它推给你,于是你的聊天、仪表盘或共享列表会在写入落地的瞬间更新,其余时候保持安静。本页介绍你为何会用它、SDK、一个完整示例,以及底层在做什么。

为什么用实时订阅而不是轮询

轮询是在新鲜度和负载之间做权衡:轮询得勤,你就为大多没变的数据反复敲打后端;轮询得稀,界面又会显得陈旧。实时订阅消除了这个权衡。订阅会保持一条打开的流,服务器在某行你能看到的数据真正发生变化时,才给你发一个很小的事件。每当屏幕应当反映别人(或别的标签页、或某个后台任务)刚做的事,就该用它:

实时订阅反映的是你的查询所返回的同一份数据——它不是一份独立的副本。所以自然的模式是:用一次初始 .select() 加载历史,再配一个 .subscribe() 实时追踪之后的一切。

SDK

表构造器上的一个方法:

client.from('table').subscribe(onChange, onError?) // → unsubscribe

每当有行变化,你的 onChange 回调会收到一个事件对象:

{
  table: string,                          // 变更来自哪张表
  type: "INSERT" | "UPDATE" | "DELETE",   // 发生了什么
  row: object                             // 受影响的行
}

subscribe 的返回值是一个取消订阅函数。用完时调用它——通常是在组件卸载时——来关闭这条流。保留这个句柄并调用它,是唯一要紧的规则;忘了它,你就会泄漏打开的流。

const off = lingcode.from('messages').subscribe(({ type, row }) => {
  if (type === 'INSERT') appendMessage(row);
});

// 稍后,在销毁时:
off();

过滤是怎么工作的

每一次变更在投递之前都会针对订阅者的权限重新核对,所以用户永远不会收到那些用普通查询读不到的行——守门你 .select() 的同一套行级安全,也守门你的订阅。由于被删除的行已经不存在了,DELETE 事件退而采用尽力而为的归属检查。而非常大的行——接近数据库通知大小上限(~8 KB)的那种——会以一个轻量的「有东西变了,请重新拉取」信号来投递,而不是完整的行。要为此做好准备:如果一次变更到达时没有完整的 row 负载,就重新查询以获取当前状态。

一个完整示例

这是一个聊天视图的完整闭环:用 .select() 加载一次最近历史,然后订阅以实时追加新消息,再在销毁时取消订阅。在 /try 和 Mac 预览里,lingcode 已经存在;在你自己的应用里,先创建客户端(参见 API 参考)。

// 1. 加载历史。
const { data: history } = await lingcode
  .from('messages')
  .order('created_at', { ascending: true })
  .limit(100)
  .select();

for (const row of history) appendMessage(row);

// 2. 实时追踪之后的一切。
const off = lingcode.from('messages').subscribe(
  ({ type, row }) => {
    if (!row) { reloadMessages(); return; } // 大行的「重新拉取」信号
    if (type === 'INSERT') appendMessage(row);
    if (type === 'UPDATE') replaceMessage(row);
    if (type === 'DELETE') removeMessage(row);
  },
  (err) => console.error('realtime stream error', err)
);

// 3. 在销毁时(例如组件卸载),关闭这条流。
function teardown() {
  off();
}

这就是那个标准形态:一次初始读取拿到状态,一个订阅负责实时追踪,再加一个你真的会调用的取消订阅句柄。

底层原理(REST)

SDK 的 subscribe 会向一个端点打开一条 SSE 连接:

GET https://lingcode.dev/api/cloud/be/<backend-id>/realtime

这是一条 Server-Sent Events 流——响应是 Content-Type: text/event-stream。用你的 anon key 或已登录的用户令牌来认证(和你做查询时用的是同一个凭证;正是它界定了 RLS 过滤的范围)。一个可选的查询参数用来收窄你会听到哪些表的变更:

GET /api/cloud/be/<backend-id>/realtime?table=messages,presence

?table= 过滤器以逗号分隔,把流限制到那些表。每次变更都作为一个 SSE change 事件到达:

event: change
data: {"table":"messages","type":"INSERT","row":{"id":42,"body":"hi"}}

这条流还会大约每 25 秒发一次心跳,以穿过代理和空闲超时、保持连接存活。SDK 会替你处理好这一切——这个端点摆在这里,是为了让你能看清订阅到底在做什么,或者直接从一个非 JS 客户端去消费它。

实时订阅是什么——又不是什么

实时订阅流式传输的是你表里的行变更。这就是它的全部范围。它有意不是

好消息是你很少需要把它们当作独立系统。要做任意的服务器→客户端消息传递,写一张表并订阅它——插入一行来发送,每个订阅者都会收到这次变更。同样的招数也能给你在线状态:保留一张 presence 表,写入一行心跳,并订阅它。

始终保留取消订阅句柄并调用它。subscribe 把一个函数交给你是有原因的——在视图消失时调用它。一个没关闭的订阅就是一条持续占用连接的打开的流。在组件框架里,从你 effect 的清理函数里返回 off;在纯 JS 里,在你的销毁路径里调用它。