← 使用场景

用实时更新取代轮询

别再每隔几秒就问一次 API:每个变化只发一次,减少请求与服务器负载,变化一发生就显示。

很多应用是靠「问」来得知变化:每个打开的页面每隔几秒就调用一次你的 API,看看订单、工单或报表有没有变。几乎每次的答案都是「没有新的」,但每次都要花一个请求、一次数据库查询,还有用户手机的电量;真的有变化时,也还要等上一个间隔才看得到。用 Hibira,你的服务器在变化发生时只发一次,所有显示这条数据的页面在几毫秒内就更新。

用户得到什么

  • 变化一发生就出现在屏幕上,不必等最多五秒。
  • 每个标签页都保持最新,不用刷新。
  • 手机不再每隔几秒白白唤醒网络。

轮询的代价

1,000 人开着页面、每 5 秒轮询一次,一小时就是 720,000 个请求。如果这一小时数据只变了 200 次,其中 99.97% 的请求都没有新内容。改用推送,同一个小时只要 200 条消息,你的 API 只需要处理每个页面第一次加载。

用到的功能

  • 私有频道(private-order-42),页面上显示的每一样东西一个频道,由你的服务器授权,用户只会收到自己的数据。
  • 官方服务端 SDK 的 trigger,在你保存变更后立刻调用。
  • pusher:subscription_succeeded,第一次连接和每次重新连接后都会触发:正是加载当前状态的时机。

轮询

每个打开的页面每 5 秒
GET /api/orders/42
你的 API大多是「没有新的」

推送

你的 API保存变更
trigger() 一次
Hibira
WebSocket
每个显示它的页面立刻更新
轮询一问再问;推送每次变化只发一次

步骤 1:找到轮询循环

通常长这样:

setInterval(async () => {
  render(await fetch('/api/orders/42').then((r) => r.json()));
}, 5000);

步骤 2:加载一次,然后监听

按 JavaScript 指南 连接,并为私有频道准备频道授权接口。订阅就绪时加载当前状态,之后每次变化到达就应用:

const load = async () => render(await fetch('/api/orders/42').then((r) => r.json()));

const channel = pusher.subscribe('private-order-42');
channel.bind('pusher:subscription_succeeded', load);
channel.bind('updated', (order) => render(order));

步骤 3:保存时发出变化

数据在哪里改变(API 路由、worker、后台操作、webhook),就在写入成功后发布:

app.patch('/api/orders/:id', requireLogin, async (req, res) => {
  const order = await db.orders.update(req.params.id, req.body);
  await pusher.trigger(`private-order-${order.id}`, 'updated', order);
  res.json(order);
});

步骤 4:留一个轻量的保险

因为重新连接后订阅会再触发一次,load() 已经会补上断线期间的空缺。浏览器也可能冻结后台标签页,所以标签页回到前台时再加载一次:

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible') load();
});

小提醒

  • 发送页面重绘需要的内容;数据很大时只发 ID 和版本号,让页面自己获取其余部分。
  • 同一条数据每秒变很多次?把它们合并,做法请看实时仪表盘。
  • 经常变动的列表可以一个列表一个频道(private-orders-team-7),不必每一行一个。
  • 事件大小取决于方案,请看价格。
  • 想在发布前知道有多少人在看?请看实时在线人数。