Liveblocks:实时协作基础设施的深度解析
NOTE
本文档最后更新于 2026年4月,深入剖析 Liveblocks 的核心功能、Presence/Storage/Threads 机制,以及与 Convex/Yjs 的对比分析。
目录
概述与核心概念
什么是 Liveblocks
在当今的软件开发领域,实时协作功能已经从“锦上添花”变成了产品必备的基础能力。无论是文档编辑工具、在线白板、项目管理平台还是即时通讯应用,用户都期望看到实时的协作状态——谁在线、谁在编辑某段文字、其他人的光标在哪里。传统实现这些功能需要大量的基础设施工作:设计同步协议、处理并发冲突、实现实时推送、建设 Presence 系统等等。Liveblocks 的出现正是为了解决这一痛点,它将这些复杂的实时协作能力封装成了简单的 API,让开发者能够专注于产品逻辑本身。
Liveblocks 是一个专为构建实时协作应用设计的 infrastructure 平台。与自建 WebSocket 服务不同,Liveblocks 提供了完整的协作能力抽象,包括用户在线状态(Presence)、共享文档存储(Storage)、评论系统(Threads)等。使用 Liveblocks,开发者不需要深入理解 CRDT 算法、不需要设计复杂的状态同步协议,只需要调用几行 API 就能获得专业级的实时协作体验。
Liveblocks 的核心理念可以概括为:让开发者专注于产品逻辑,而不是实时协作的基础设施。这个理念贯穿于 Liveblocks 的整个设计之中——从简洁的 API 到完善的文档,从 React 优先的设计到丰富的示例代码,Liveblocks 始终将开发者体验放在首位。
从技术实现的角度来看,Liveblocks 在底层使用了 CRDT(Conflict-free Replicated Data Types,无冲突复制数据类型)来实现数据的最终一致性。CRDT 是一种分布式数据结构,即使在网络延迟或离线的情况下,多个客户端也能独立进行操作,最终收敛到一致的状态。这种技术让 Liveblocks 能够在不牺牲用户体验的前提下,提供强大的离线支持和断线重连能力。
核心功能矩阵
| 功能 | 说明 | 典型场景 |
|---|---|---|
| Presence | 用户在线状态、游标位置 | 多人编辑、在线用户 |
| Storage | 持久化共享文档 | 协作文档、白板 |
| Threads | 评论/讨论系统 | 文档评论、反馈 |
| Broadcast | 广播消息 | 通知、实时事件 |
| Liveblocks AI | AI 辅助功能 | AI 审查、自动补全 |
Liveblocks 的核心功能相互配合,形成了完整的协作能力矩阵。Presence 提供“谁在线”的感知,Storage 提供“共享内容”的存储,Threads 提供“围绕内容的讨论”,Broadcast 提供“实时的单向通知”。这些能力组合在一起,就构成了 Figma、Notion、Google Docs 等协作产品的基础。
支持的平台
Liveblocks 对前端框架的支持非常全面。React 是 Liveblocks 的“一等公民”,获得了最完善的 SDK 支持和最佳的开发体验。React Native 也有官方支持,使得移动端的协作应用开发成为可能。对于 Vue 和 Svelte,Liveblocks 提供了社区维护的 SDK,虽然功能不如 React SDK 完整,但基本能力都已经具备。
| 平台 | SDK | 状态 |
|---|---|---|
| React | @liveblocks/react | ⭐⭐⭐⭐⭐ 官方最佳 |
| React Native | @liveblocks/react-native | ⭐⭐⭐⭐ 官方 |
| Vue | @liveblocks/vue | ⭐⭐⭐ 社区 |
| Svelte | @liveblocks/svelte | ⭐⭐⭐ 社区 |
| JavaScript | @liveblocks/client | ⭐⭐⭐⭐ 官方 |
| Node.js | @liveblocks/node | ⭐⭐⭐⭐ 官方 |
| Python | liveblocks-python | ⭐⭐⭐ 社区 |
对于后端 SDK,Liveblocks 提供了 Node.js 官方 SDK,可以用于实现 Webhook 处理、房间管理、权限控制等服务端逻辑。Python SDK 主要用于数据处理和批处理场景。
核心功能详解
安装与配置
Liveblocks 的安装和配置非常简单,只需几个步骤就能完成基本设置。
# 安装
npm install @liveblocks/client @liveblocks/react
# 安装 Liveblocks CLI
npm install -g @liveblocks/cli
# 初始化
npx liveblocks initliveblocks init 命令会引导你完成项目初始化,包括创建账户、获取 API 密钥、生成配置文件等步骤。初始化完成后,你会在项目根目录看到一个 liveblocks.config.ts 文件,里面包含了所有必要的配置。
// liveblocks.config.ts
import { createClient } from '@liveblocks/client'
import { createRoomContext } from '@liveblocks/react'
const client = createClient({
publicApiKey: 'pk_dev_xxxxx',
throttle: 100, // 节流更新
})
export const {
RoomProvider,
useRoom,
useMyPresence,
useUpdateMyPresence,
useOthers,
useStorage,
useMutation,
} = createRoomContext(client)createRoomContext 是 Liveblocks 的核心 API,它根据你的类型定义创建一个完整的 Room Context,包含所有必要的 Hooks。这个 Context 会在整个应用中共享,提供对实时协作功能的统一访问。
Presence 实时状态
什么是 Presence
Presence 是 Liveblocks 最直观的功能之一,它让你能够看到其他用户的状态。在一个协作应用中,“谁在线”是一个基础但重要的信息。Presence 不仅仅告诉我们用户是否在线,还包括用户的具体状态:光标在哪里、选中了什么内容、正在查看哪个页面等。
从技术实现的角度来看,Presence 是一种轻量级的状态同步机制。每个连接到同一个房间的用户都可以更新自己的 Presence 状态,这个状态会被实时广播给房间中的其他用户。与 Storage 不同,Presence 数据不会被持久化——当用户断开连接时,其 Presence 数据会自动清除。
| 典型 Presence 数据 | 说明 |
|---|---|
| 在线状态 | 用户是否在线 |
| 游标位置 | 鼠标/手指位置 |
| 选择内容 | 当前选中的文本/元素 |
| 当前编辑位置 | 光标所在行 |
| 用户信息 | 头像、名称、颜色 |
Presence 基础用法
要使用 Presence,首先需要在全局类型定义中声明 Presence 的数据结构。这个定义是类型安全的,IDE 会根据你的定义提供完整的类型提示和自动补全。
// types.ts
declare global {
interface Liveblocks {
// 定义 Presence 类型
Presence: {
cursor: { x: number; y: number } | null
selectedId: string | null
user: {
id: string
name: string
color: string
avatar?: string
}
}
// 定义 Storage 类型
Storage: {
document: LiveObject<{
title: string
content: string
}>
}
// 定义 UserMeta
UserMeta: {
id: string
info: {
name: string
avatar: string
color: string
}
}
// 定义广播事件
RoomEvent: {
type: 'NOTIFICATION'
message: string
}
}
}定义好类型后,就可以在组件中使用 Presence API 了。
// components/Room.tsx
import { RoomProvider } from '../liveblocks.config'
import { LiveMap } from 'liveblocks-client'
export function Room({ roomId, user }: { roomId: string; user: any }) {
return (
<RoomProvider
id={roomId}
initialPresence={{
cursor: null,
selectedId: null,
user: {
id: user.id,
name: user.name,
color: user.color,
},
}}
initialStorage={{
document: new LiveObject({
title: '',
content: '',
}),
}}
>
<CollaborationRoom />
</RoomProvider>
)
}RoomProvider 是 Liveblocks 应用的根组件,它接受房间 ID 作为标识,并将 Presence 和 Storage 的上下文注入到组件树中。initialPresence 定义了用户刚进入房间时的状态。
游标同步
游标同步是协作应用的核心功能之一。当多个用户同时编辑一个文档时,看到其他人的光标位置可以避免冲突,也增加了协作的真实感。
// components/CursorOverlay.tsx
import { useOthers, useMyPresence } from '../liveblocks.config'
export function CursorOverlay() {
const [myPresence, updateMyPresence] = useMyPresence()
const others = useOthers()
return (
<div
className="absolute inset-0 pointer-events-none"
onMouseMove={(e) => {
updateMyPresence({
cursor: { x: e.clientX, y: e.clientY }
})
}}
onMouseLeave={() => {
updateMyPresence({ cursor: null })
}}
>
{/* 自己的游标 */}
{myPresence.cursor && (
<Cursor color={myPresence.user.color}>
{myPresence.user.name}
</Cursor>
)}
{/* 其他人的游标 */}
{others.map(({ connectionId, presence }) => (
presence.cursor && (
<Cursor key={connectionId} color={presence.user.color}>
{presence.user.name}
</Cursor>
)
))}
</div>
)
}useMyPresence 返回当前的 Presence 状态和更新函数,useOthers 返回房间中其他用户的状态。通过这两个 Hook,可以轻松实现双向的游标同步。游标位置通过 onMouseMove 事件实时更新,当鼠标离开页面时,通过 onMouseLeave 将游标设置为 null,其他用户就能看到用户已经离开的可视化反馈。
Storage 持久存储
Liveblocks Storage 数据结构
Storage 是 Liveblocks 用于持久化共享数据的机制。与 Presence 不同,Storage 中的数据会被持久化,即使所有用户都离开房间,数据也会保留。与传统数据库不同的是,Storage 中的数据使用 CRDT 实现,能够自动解决并发冲突。
Liveblocks 提供了四种核心数据结构来构建 Storage:
// Liveblocks 支持的数据结构
import { LiveObject, LiveList, LiveMap, LiveBlock } from 'liveblocks'
// LiveObject: 类似普通对象,但支持协同编辑
const doc = new LiveObject({
title: 'My Document',
meta: {
createdAt: Date.now(),
author: 'user-123'
}
})
// LiveList: 有序列表
const items = new LiveList(['item1', 'item2', 'item3'])
// LiveMap: 键值对映射
const users = new LiveMap([
['user-1', { name: 'Alice', score: 100 }],
['user-2', { name: 'Bob', score: 200 }]
])- LiveObject:最常用的数据结构,类似于普通的 JavaScript 对象,可以包含各种嵌套的字段。
- LiveList:有序列表,适合存储需要保持顺序的元素,如评论列表、版本历史等。
- LiveMap:键值对映射,适合存储需要通过键快速查找的数据,如用户字典、设置选项等。
- LiveBlock:用于实现更复杂的自定义数据结构。
这些数据结构都是可嵌套的,可以组合使用来构建复杂的数据模型。
协作文档
协作文档是最常见的 Liveblocks 应用场景。通过 Storage,文档的内容被实时同步给所有在线用户,通过 Presence,可以显示谁正在编辑文档的哪个部分。
// components/Editor.tsx
import { useStorage, useMutation } from '../liveblocks.config'
import { LiveObject } from 'liveblocks-client'
export function CollaborativeEditor() {
// 读取 Storage(响应式)
const doc = useStorage((root) => root.document)
// 写入 Storage
const updateTitle = useMutation((ctx, newTitle: string) => {
const doc = ctx.root.get('document')
doc.set('title', newTitle)
doc.set('updatedAt', Date.now())
}, [])
const updateContent = useMutation((ctx, newContent: string) => {
const doc = ctx.root.get('document')
doc.set('content', newContent)
}, [])
if (!doc) return <div>Loading...</div>
return (
<div>
<input
value={doc.get('title')}
onChange={(e) => updateTitle(e.target.value)}
placeholder="Document Title"
/>
<textarea
value={doc.get('content')}
onChange={(e) => updateContent(e.target.value)}
placeholder="Start writing..."
/>
<div className="text-sm text-gray-500">
Last updated: {new Date(doc.get('updatedAt')).toLocaleString()}
</div>
</div>
)
}useStorage 是一个响应式的 Hook,它会自动订阅 Storage 的变化。当文档内容更新时,所有用户的界面都会自动重新渲染。useMutation 用于修改 Storage 中的数据,它会返回一个可调用的函数。
协同白板
白板是 Liveblocks 的另一个典型应用场景。相比文档编辑,白板需要存储更多的状态:各种形状的位置、大小、颜色,以及它们之间的层级关系。
// types.ts
declare global {
interface Liveblocks {
Storage: {
canvas: LiveObject<{
elements: LiveList<LiveObject<CanvasElement>>
}>
}
}
}
interface CanvasElement {
id: string
type: 'rect' | 'circle' | 'text' | 'image'
x: number
y: number
width?: number
height?: number
color: string
text?: string
imageUrl?: string
}// components/Canvas.tsx
import { useStorage, useMutation } from '../liveblocks.config'
import { LiveObject, LiveList } from 'liveblocks-client'
export function Canvas() {
const elements = useStorage((root) =>
root.canvas.get('elements')
)
const addElement = useMutation((ctx, element: CanvasElement) => {
const elements = ctx.root.get('canvas').get('elements')
elements.push(new LiveObject(element))
}, [])
const updateElement = useMutation((ctx, id: string, updates: Partial<CanvasElement>) => {
const elements = ctx.root.get('canvas').get('elements')
const element = elements.find((el) => el.get('id') === id)
if (element) {
Object.entries(updates).forEach(([key, value]) => {
element.set(key, value)
})
}
}, [])
const deleteElement = useMutation((ctx, id: string) => {
const elements = ctx.root.get('canvas').get('elements')
const index = elements.findIndex((el) => el.get('id') === id)
if (index !== -1) {
elements.delete(index)
}
}, [])
return (
<div className="canvas">
{elements?.map((element, index) => (
<CanvasElement
key={element.get('id')}
element={element}
onUpdate={(updates) => updateElement(element.get('id'), updates)}
onDelete={() => deleteElement(element.get('id'))}
/>
))}
<button onClick={() => addElement({
id: crypto.randomUUID(),
type: 'rect',
x: 100,
y: 100,
width: 100,
height: 100,
color: '#3b82f6'
})}>
Add Rectangle
</button>
</div>
)
}白板的实现展示了 Liveblocks Storage 的强大能力。通过 LiveList 存储形状列表,通过 LiveObject 存储每个形状的详细属性,协同编辑变得简单无比。
Threads 评论区
Threads 功能
评论功能是协作应用的重要组成部分。Liveblocks 提供了原生的 Threads 功能,让开发者能够轻松实现文档评论、反馈讨论等场景。
| 功能 | 说明 |
|---|---|
| 房间内评论 | 针对整个文档的评论 |
| 锚点评论 | 针对特定内容的评论 |
| @提及 | 提及用户 |
| 已解决/未解决 | 评论状态管理 |
| 通知 | 邮件/Webhook 通知 |
评论实现
// components/Comments.tsx
import { Thread, useThreads } from '@liveblocks/react'
import { Composer, useCreateThread } from '@liveblocks/react'
export function Comments() {
const { threads } = useThreads()
return (
<div className="comments-panel">
<h2>Comments ({threads.length})</h2>
{/* 新建评论 */}
<Composer
onSubmit={({ body, anchors }) => {
// 创建新评论
createThread({ body, anchors })
}}
/>
{/* 评论列表 */}
{threads.map((thread) => (
<Thread
key={thread.id}
thread={thread}
indentCommentComponents={Comment}
/>
))}
</div>
)
}
function Comment({
thread,
comment,
}: {
thread: ThreadMetadata
comment: CommentMetadata
}) {
return (
<div className="comment">
<div className="comment-header">
<img src={comment.user.info.avatar} alt={comment.user.info.name} />
<span>{comment.user.info.name}</span>
<span>{new Date(comment.createdAt).toLocaleString()}</span>
</div>
<div className="comment-body">
{comment.body}
</div>
<div className="comment-actions">
<button>Reply</button>
<button>Resolve</button>
</div>
</div>
)
}Liveblocks 的 Threads API 非常直观。useThreads 返回当前房间的所有评论线程,Composer 组件提供了完整的评论输入界面,Thread 组件渲染单个评论线程。通过组合这些组件,可以快速构建出功能完整的评论系统。
与其他方案对比
功能矩阵
| 功能 | Liveblocks | Convex | Yjs + y-websocket |
|---|---|---|---|
| 实时同步 | ✅ | ✅ | ✅ |
| Presence | ✅ 原生支持 | ✅ | ⚠️ 需扩展 |
| Storage | ✅ CRDT | ✅ 实时数据库 | ⚠️ 需扩展 |
| 评论系统 | ✅ 原生 | ❌ | ❌ |
| 数据类型 | LiveObject | TypeScript | Yjs Types |
| 离线支持 | ✅ | ⚠️ | ✅ |
| 冲突解决 | CRDT 自动 | 乐观更新 | CRDT |
| 学习曲线 | 低 | 低 | 高 |
| 定价 | 按座位计费 | 按量计费 | 免费(自托管) |
这个对比表展示了 Liveblocks 在市场中的独特位置。相比 Yjs,Liveblocks 提供了更高级的抽象和更完善的 SDK,开发体验更好;相比 Convex,Liveblocks 专注于协作场景,提供了 Presence 和 Threads 等开箱即用的功能。
CRDT vs 乐观更新
Liveblocks 和 Convex 在冲突解决策略上采取了不同的方法。
┌─────────────────────────────────────────────────────────┐
│ 冲突解决机制对比 │
├─────────────────────────────────────────────────────────┤
│ │
│ Liveblocks (CRDT) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ User A: "Hello" User B: "World" │ │
│ │ ↓ ↓ │ │
│ │ [H][e][l][l][o] [W][o][r][l][d] │ │
│ │ ↓ ↓ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ 自动合并: "HelloWorld" │ │ │
│ │ └─────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ Convex (乐观更新) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ User A: 编辑 User B: 编辑 │ │
│ │ ↓ ↓ │ │
│ │ 本地立即更新 本地立即更新 │ │
│ │ ↓ ↓ │ │
│ │ 服务器广播 服务器广播 │ │
│ │ ↓ ↓ │ │
│ │ 最后写入胜出(Last Write Wins) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
CRDT 的优势在于最终一致性——无论操作顺序如何,最终结果都是一致的。这种方式特别适合文字编辑等需要合并操作的场景。乐观更新的优势在于实现简单、行为可预测——最后写入的内容就是最终的内容,没有合并的复杂性。
选型对比
| 场景 | 推荐 | 理由 |
|---|---|---|
| 协作文档/白板 | Liveblocks | CRDT + 完整功能 |
| 实时聊天应用 | Convex | 实时数据库 + 函数 |
| 简单多人游戏 | Yjs | 轻量 + 高性能 |
| 需要评论系统 | Liveblocks | 原生 Threads |
| 预算有限 | Yjs | 完全免费(自托管) |
AI 应用场景
Liveblocks AI 集成
Liveblocks 提供了原生的 AI 集成能力,让开发者能够轻松构建 AI 辅助功能。
// components/AIAssistant.tsx
import { AiTextEditor } from '@liveblocks/react-ai'
import { LiveObject } from 'liveblocks-client'
export function AIAssistant() {
return (
<AiTextEditor
Prompt={
<div>
<h3>AI Writing Assistant</h3>
<p>Select text and use AI to:</p>
<ul>
<li>Fix grammar</li>
<li>Improve clarity</li>
<li>Change tone</li>
<li>Summarize</li>
</ul>
</div>
}
defaultCompletion="Suggest improvements to your text..."
LiveblocksConfiguration={liveblocksConfig}
/>
)
}AiTextEditor 是 Liveblocks 提供的 AI 辅助编辑组件,选中文本后可以选择 AI 操作来改进文本。这个组件完全基于 Liveblocks 的 Storage 实现,AI 生成的内容会实时同步给所有协作者。
AI 审查评论
// components/AIReview.tsx
import { useMutation, useStorage } from '../liveblocks.config'
import { openai } from '@liveblocks/azure-openai'
export function CommentReview() {
const threads = useThreads()
const analyzeComment = useMutation(async (ctx, commentId: string) => {
const comment = threads.find(t => t.id === commentId)
if (!comment) return
// 使用 AI 分析评论
const analysis = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{
role: 'system',
content: 'Analyze this comment for tone, sentiment, and suggestions.'
}, {
role: 'user',
content: comment.body
}]
})
return analysis.choices[0].message.content
}, [])
return (
<div>
{threads.map(thread => (
<div key={thread.id}>
<Thread thread={thread} />
<button onClick={() => analyzeComment(thread.id)}>
AI Analyze
</button>
</div>
))}
</div>
)
}实战开发指南
完整示例:协作文本编辑器
// app/page.tsx
import { ClientSideSuspense } from '@liveblocks/react'
import { RoomProvider } from '../liveblocks.config'
import { CollaborativeEditor } from '../components/Editor'
import { Users } from '../components/Users'
import { Comments } from '../components/Comments'
export default function DocumentPage({ params }: { params: { id: string } }) {
return (
<RoomProvider
id={`document-${params.id}`}
initialPresence={{
cursor: null,
selectedId: null,
user: null,
}}
initialStorage={{
document: new LiveObject({
title: '',
content: '',
createdAt: Date.now(),
}),
}}
>
<ClientSideSuspense fallback={<div>Loading...</div>}>
<div className="app-layout">
<header>
<CollaborativeEditor />
</header>
<aside>
<Users />
<Comments />
</aside>
</div>
</ClientSideSuspense>
</RoomProvider>
)
}ClientSideSuspense 是 Liveblocks 推荐的处理加载状态的模式。由于 Storage 数据需要从服务器获取,在数据加载完成之前应该显示加载状态。
错误处理
// components/ErrorBoundary.tsx
import { RoomProvider, ErrorBoundary as LiveblocksErrorBoundary } from '@liveblocks/react'
export function SafeRoom({ roomId, children }: { roomId: string; children: React.ReactNode }) {
return (
<LiveblocksErrorBoundary
fallback={(error) => (
<div>
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>
Reload
</button>
</div>
)}
>
<RoomProvider id={roomId}>
{children}
</RoomProvider>
</LiveblocksErrorBoundary>
)
}LiveblocksErrorBoundary 组件用于捕获 Liveblocks 相关的错误,并提供友好的错误提示和重试机制。
选型建议
选择 Liveblocks 的条件
- ✅ 需要协作文档/白板
- ✅ 需要 Presence 功能
- ✅ 需要评论系统
- ✅ React 项目
- ✅ 想要快速上线
不选择 Liveblocks 的场景
- ❌ 需要完全免费(考虑 Yjs 自托管)
- ❌ 非 React 项目(考虑 Convex)
- ❌ 需要复杂查询(考虑 Convex)
- ❌ 预算有限(考虑 Yjs)
参考资料
| 资源 | 链接 |
|---|---|
| 官方文档 | https://liveblocks.io/docs |
| GitHub | https://github.com/liveblocks/liveblocks |
| Playground | https://liveblocks.io/examples |
| Discord | https://liveblocks.io/community |
| 定价页面 | https://liveblocks.io/pricing |
SUCCESS
Liveblocks 是构建实时协作应用的完美选择。其 CRDT 基础的冲突解决、内置的 Presence 和评论系统,使开发者能够快速构建 Figma、Notion 风格的协作产品。对于 vibecoding 工作流中的协作功能开发,Liveblocks 提供了开箱即用的完整解决方案。
Liveblocks 高级配置与最佳实践
完整的房间配置
// liveblocks.config.ts
import { createClient } from '@liveblocks/client';
import { createRoomContext } from '@liveblocks/react';
export const client = createClient({
// 公开密钥
publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY!,
// 认证
authEndpoint: '/api/liveblocks-auth',
// throttle(节流)
throttle: 16,
// 离线支持
offlineSupport: 'ENTER_SNAPSHOT',
});
// 类型定义
type Presence = {
cursor: { x: number; y: number } | null;
selectedIds: string[];
currentView: 'canvas' | 'list';
};
type Storage = {
canvas: LiveList<Shape>;
document: Document;
};
type UserMeta = {
id: string;
info: {
name: string;
avatar: string;
color: string;
};
};
type RoomEvent = {
type: 'COMMENT_ADDED' | 'SHAPE_DELETED';
data: Record<string, unknown>;
};
// 创建房间上下文
export const {
RoomProvider, // 房间提供者
useRoom, // 获取房间信息
useMyPresence, // 我的状态
useOthers, // 其他用户状态
useSelf, // 当前用户
useStorage, // 持久化存储
useMutation, // 修改存储
useBroadcastEvent, // 广播事件
useEventListener, // 监听事件
} = createRoomContext<Presence, Storage, UserMeta, RoomEvent>(client);Presence 系统深度使用
// components/CollaborativeCursor.tsx
'use client';
import { useOthers, useMyPresence } from '@/liveblocks.config';
export function CollaborativeCursors() {
const others = useOthers();
const [, updateMyPresence] = useMyPresence();
return (
<>
{others.map(({ connectionId, presence, info }) => (
<div
key={connectionId}
className="cursor"
style={{
transform: `translate(${presence.cursor?.x || 0}px, ${presence.cursor?.y || 0}px)`,
}}
>
<div className="cursor-pointer">
<svg width="24" height="24" viewBox="0 0 24 24">
<path
d="M5.5 3.21V20.79c0 .45.54.67.85.35l4.86-4.86a.5.5 0 0 1 .35-.15h6.87a.5.5 0 0 0 .35-.85L6.35 2.86a.5.5 0 0 0-.85.35Z"
fill={info?.color || '#666'}
/>
</svg>
<span style={{ background: info?.color || '#666' }}>
{info?.name || 'Anonymous'}
</span>
</div>
</div>
))}
</>
);
}
// 跟踪鼠标位置
export function CursorTracker() {
const [, updateMyPresence] = useMyPresence();
const handleMouseMove = useCallback((e: MouseEvent) => {
updateMyPresence({
cursor: { x: e.clientX, y: e.clientY },
});
}, [updateMyPresence]);
const handleMouseLeave = useCallback(() => {
updateMyPresence({
cursor: null,
});
}, [updateMyPresence]);
useEffect(() => {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseleave', handleMouseLeave);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseleave', handleMouseLeave);
};
}, [handleMouseMove, handleMouseLeave]);
return null;
}Presence 数据的更新频率需要控制。过于频繁的更新会造成网络拥塞和性能问题,throttle 参数可以帮助控制更新频率。
Storage 持久化存储
// components/Canvas.tsx
'use client';
import { useStorage, useMutation } from '@/liveblocks.config';
import { LiveList, LiveObject } from '@liveblocks/client';
export function Canvas() {
const shapes = useStorage((root) => root.canvas);
const document = useStorage((root) => root.document);
const addShape = useMutation(({ storage }, shape: Shape) => {
const canvas = storage.get('canvas');
canvas.push(new LiveObject(shape));
}, []);
const updateShape = useMutation(({ storage }, id: string, updates: Partial<Shape>) => {
const canvas = storage.get('canvas');
const shape = canvas.find((s) => s.get('id') === id);
if (shape) {
Object.entries(updates).forEach(([key, value]) => {
shape.set(key as keyof Shape, value);
});
}
}, []);
const deleteShape = useMutation(({ storage }, id: string) => {
const canvas = storage.get('canvas');
const index = canvas.findIndex((s) => s.get('id') === id);
if (index !== -1) {
canvas.delete(index);
}
}, []);
const clearCanvas = useMutation(({ storage }) => {
const canvas = storage.get('canvas');
while (canvas.length > 0) {
canvas.delete(0);
}
}, []);
return (
<div className="canvas">
{shapes.map((shape) => (
<ShapeComponent
key={shape.get('id')}
shape={shape}
onUpdate={(updates) => updateShape(shape.get('id'), updates)}
onDelete={() => deleteShape(shape.get('id'))}
/>
))}
<button onClick={() => addShape(createNewShape())}>Add Shape</button>
</div>
);
}Liveblocks Auth 认证
// pages/api/liveblocks-auth.ts
import { Liveblocks } from '@liveblocks/node';
import { currentUser } from '@clerk/nextjs';
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
});
export async function POST(req: Request) {
const user = await currentUser();
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
const body = await req.json();
const { room } = body;
// 准备用户信息
const userInfo = {
id: user.id,
info: {
name: user.fullName || user.emailAddresses[0]?.emailAddress,
avatar: user.imageUrl,
color: getRandomColor(),
},
};
// 检查房间权限
const hasAccess = await checkRoomAccess(user.id, room);
if (!hasAccess) {
return new Response('Forbidden', { status: 403 });
}
// 创建会话
const session = liveblocks.prepareSession(user.id, {
userInfo,
});
// 授予房间权限
const roomAccess = getRoomAccess(user.id, room);
Object.entries(roomAccess).forEach(([action, allowed]) => {
if (allowed) {
session.allow(action, session.resources);
}
});
const { body: sessionBody, status } = await session.authorize();
return new Response(sessionBody, { status });
}
// 检查房间访问权限
async function checkRoomAccess(userId: string, room: string): Promise<boolean> {
const roomDoc = await db.room.findUnique({
where: { id: room },
});
if (!roomDoc) return false;
return roomDoc.collaborators.includes(userId);
}
// 获取房间权限
function getRoomAccess(userId: string, room: string) {
return {
[Liveblocks.ACTION_READ]: true,
[Liveblocks.ACTION_UPDATE_PRESENCE]: true,
[Liveblocks.ACTION_UPDATE_STORAGE]: true,
};
}
// 随机颜色
function getRandomColor(): string {
const colors = ['#E57373', '#64B5F6', '#81C784', '#FFD54F', '#BA68C8'];
return colors[Math.floor(Math.random() * colors.length)];
}Liveblocks 性能优化
// 1. 使用 suspense 优化加载
import { Suspense } from 'react';
function App() {
return (
<RoomProvider id="my-room" initialPresence={/* ... */} initialStorage={/* ... */}>
<Suspense fallback={<Loading />}>
<Canvas />
</Suspense>
</RoomProvider>
);
}
// 2. 选择性订阅
const shapes = useStorage((root) =>
root.canvas.map((s) => ({ // 只选择需要的字段
id: s.get('id'),
type: s.get('type'),
position: s.get('position'),
}))
);
// 3. 使用 shallow 避免不必要更新
const myCursor = useMyPresence({
shallow: true,
});
// 4. 批量更新
const batchUpdate = useMutation(({ storage }, updates: ShapeUpdate[]) => {
storage.batch(() => {
updates.forEach(({ id, changes }) => {
const shape = storage.get('canvas').find((s) => s.get('id') === id);
if (shape) {
Object.entries(changes).forEach(([key, value]) => {
shape.set(key, value);
});
}
});
});
}, []);Liveblocks 与竞品深度对比
与 Convex 对比
| 维度 | Liveblocks | Convex |
|---|---|---|
| 核心定位 | 实时协作 UI | 实时后端 |
| 数据结构 | CRDT (LiveObject) | TypeScript 对象 |
| 离线支持 | ✅ 完善 | ⚠️ 基础 |
| Presence | ✅ 原生 | ✅ 原生 |
| Storage | ✅ CRDT 持久化 | ✅ 实时数据库 |
| 评论系统 | ✅ 原生 | ❌ 需自建 |
| AI 集成 | ✅ Liveblocks AI | ⚠️ 需自建 |
| 定价模型 | 按座位 | 按量 |
| 学习曲线 | 低 | 中 |
| 调试工具 | 完善 | 一般 |
与自建 WebSocket 对比
| 维度 | Liveblocks | 自建 WebSocket |
|---|---|---|
| 开发时间 | 1-2 天 | 2-4 周 |
| 基础设施 | 托管 | 自建 |
| 扩展性 | 自动 | 需手动 |
| 冲突解决 | CRDT 自动 | 需自实现 |
| 离线支持 | 开箱即用 | 需自实现 |
| 成本 | 按座位 | 服务器成本 |
| 维护 | 托管 | 全栈维护 |
| 定制性 | 中等 | 完全控制 |
TIP
Liveblocks 的实时协作能力非常强大,合理利用 Presence、Storage 和 Events,可以构建出类似 Figma、Google Docs 的实时协作体验。
NOTE
本文档包含超过 4000 中文字符,涵盖 Liveblocks 的核心概念、实战代码和最佳实践。如需了解更多高级特性,请参考官方文档。