一句话:实时订阅让你的界面在数据库发生变化的瞬间作出反应,而不是靠轮询。订阅一张表,每当有行被插入、更新或删除,你就会收到一个事件。它通过 Server-Sent Events 投递,并且按行级安全过滤——已登录用户只会收到自己被允许看到的行。一行调用:lingcode.from('table').subscribe(onChange),它返回一个取消订阅函数,你在销毁时调用它。
多数应用靠一个定时器来伪装「实时」:每隔几秒重新拉取一次,并寄望期间没发生什么要紧的事。什么都没变时这很浪费,真有变化时又会有延迟。实时订阅把它反了过来——服务器在行变化的那一刻就把它推给你,于是你的聊天、仪表盘或共享列表会在写入落地的瞬间更新,其余时候保持安静。本页介绍你为何会用它、SDK、一个完整示例,以及底层在做什么。
轮询是在新鲜度和负载之间做权衡:轮询得勤,你就为大多没变的数据反复敲打后端;轮询得稀,界面又会显得陈旧。实时订阅消除了这个权衡。订阅会保持一条打开的流,服务器只在某行你能看到的数据真正发生变化时,才给你发一个很小的事件。每当屏幕应当反映别人(或别的标签页、或某个后台任务)刚做的事,就该用它:
实时订阅反映的是你的查询所返回的同一份数据——它不是一份独立的副本。所以自然的模式是:用一次初始 .select() 加载历史,再配一个 .subscribe() 实时追踪之后的一切。
表构造器上的一个方法:
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();
}
这就是那个标准形态:一次初始读取拿到状态,一个订阅负责实时追踪,再加一个你真的会调用的取消订阅句柄。
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 里,在你的销毁路径里调用它。