# 多标签页、多设备同步：退出登录、权限与购物车

> 一次退出所有地方、撤销的角色立即生效，并让购物车与设置在用户打开的每个标签页和设备上保持一致。

Source: https://hibira.io/zh-cn/use-cases/multi-tab-sync

用户常在好几个标签页、好几台设备上开着你的应用，而每一份都要刷新才知道有变化。有人在公用电脑上退出了，手机上的标签页却还显示他的账号。管理员撤掉了同事的编辑权限，同事却能一直编辑到刷新为止。手机上把商品加进购物车，笔记本的标签页却还说购物车是空的。用 Hibira，你的服务器能一次通知应用的每一份副本。

## 用户得到什么

- 「退出所有设备」真的会立刻关闭每个打开的会话。
- 角色变更或权限撤销不必刷新就生效。
- 每个标签页、每台设备上的购物车、设置和未读数都一致。

## 用到的功能

- **用户认证**（`pusher.signin()`），做法同[实时站内通知](https://hibira.io/zh-cn/use-cases/in-app-notifications)，让每个登录的连接都属于某位用户。
- **`sendToUser`**，一次调用就送到这位用户的每个标签页和设备。
- **`terminateUserConnections`**，关闭以这位用户登录的每个连接。

```text
flow: 服务器的一次调用，送到用户打开的每一份应用
== 某位用户的数据变了
你的服务器|退出、新角色、购物车 --`sendToUser()`--> *Hibira --WebSocket--> 每个标签页和设备|属于这位用户
== 退出所有设备
你的服务器|会话已撤销 --全部断开--> *Hibira --关闭--> 每个连接|以这位用户登录
```

## 步骤 1：退出所有设备

先在数据库撤销会话，再通知每一份打开的应用，最后关闭连接：

```js
app.post('/account/sign-out-everywhere', requireLogin, async (req, res) => {
  const id = String(req.user.id);
  await db.sessions.revokeAll(id);
  await pusher.sendToUser(id, 'signed-out', {});
  await pusher.terminateUserConnections(id);
  res.sendStatus(204);
});
```

被关闭的连接无法再次登录，因为你的用户认证接口已经不接受被撤销的会话。

## 步骤 2：权限变更立刻生效

管理员变更某人的角色时，发出一个提醒。页面会向你的服务器重新获取权限，服务器仍然是唯一的依据：

```js
await db.members.update(member.id, { role: 'viewer' });
await pusher.sendToUser(String(member.userId), 'permissions-changed', {});
```

## 步骤 3：让每个地方的购物车都一样

每次购物车变动后，发出页头要显示的摘要：

```js
app.post('/api/cart/items', requireLogin, async (req, res) => {
  const cart = await db.carts.add(req.user.id, req.body);
  await pusher.sendToUser(String(req.user.id), 'cart', { count: cart.count, total: cart.total });
  res.json(cart);
});
```

## 步骤 4：在每个标签页做出反应

```js
pusher.user.bind('signed-out', () => location.assign('/login'));
pusher.user.bind('permissions-changed', async () => {
  applyPermissions(await fetch('/api/me/permissions').then((r) => r.json()));
});
pusher.user.bind('cart', ({ count, total }) => cartBadge.set(count, total));
```

## 小提醒

- 安全上不要相信事件本身：它只代表「请重新检查」。你的 API 仍然要在每个请求检查会话和角色。
- `BroadcastChannel` 能同步同一个浏览器的标签页，但无法同步其他设备，也收不到管理员或服务器做的变更。
- 访客购物车可以一个购物车一个私有频道（`private-cart-<id>`），用购物车的 cookie 授权。
- 发数量和总额就好，不要发整个购物车；事件大小取决于方案，请看[价格](https://hibira.io/zh-cn/pricing)。
