返回首页
🎨 前端 / Web

Yjs + Liveblocks 2026:实时协作应用完整实战指南

实时协作是 2026 年 SaaS 标准功能。本文 Yjs / Liveblocks / PartyKit / Electric SQL 4 大方案对比 + 4 个实战 + 选型决策树。

Yjs · Liveblocks · CRDT · 实时协作 · PartyKit · Electric SQL · Figma · Notion
��

今日技术简讯

📰 技术简讯 · 2026-08-20

今日聚合 6 条热门技术内容(中文素材优先)。

🤖 AI / LLM

1. Liveblocks 推出 AI Presence

2. Yjs 推出 14.0

  • 链接https://yjs.dev/blog/14-0
  • 来源:Yjs
  • 摘要:Yjs 14.0 推出 AI-aware CRDT + 性能提升 3x + TypeScript 完整支持。

🎨 前端 / Web

3. PartyKit 推出 Durable Objects

4. Electric SQL 推出 2.0

  • 链接https://electric-sql.com/blog/2-0
  • 来源:Electric SQL
  • 摘要:Electric SQL 2.0 推出实时 PostgreSQL 双向同步,本地优先 + 自动冲突解决。

⚙️ 后端 / 架构

5. Replicate 推出实时流

🚀 独立开发 / OPC

6. 即刻"实时协作"专题


数据来源:掘金 / InfoQ 中文 / 即刻 / 少数派 / HN 采集日期:2026-08-20 (UTC+8)

��

今日深度文

Yjs + Liveblocks 2026:实时协作应用完整实战指南

一句话结论:2026 年 SaaS 标准 = 实时协作。Notion / Figma / Linear 都用。本文 4 大方案对比 + 实战 + 选型。

背景

2026 年实时协作成为 SaaS 标准:

传统 SaaS(单用户编辑):
- 一个人编辑
- 保存后刷新
- 不支持冲突解决

→ 用户体验差

实时协作 SaaS(Notion / Figma / Linear 类)

实时协作(多人同时编辑):
- 多人同时编辑
- 光标实时显示
- 离线后自动同步
- 冲突自动解决(CRDT)
- 历史版本回溯

→ 用户体验极佳

为什么实时协作是 2026 年关键:

  1. 用户期望:Notion / Figma 教育市场
  2. 竞争优势:差异化功能
  3. 远程办公:协作工具必备
  4. AI 协作:Agent + 人类协作
  5. 技术成熟:Yjs / CRDT 等开源方案成熟

4 大方案对比

方案 类型 计费 自托管 学习曲线
Yjs 开源 CRDT 免费 ⭐⭐⭐
Liveblocks SaaS $0/1K MAU ⭐⭐⭐⭐⭐
PartyKit Serverless $0/1K ⭐⭐⭐⭐
Electric SQL SaaS + 开源 $0/1K ⭐⭐⭐

1. Yjs(CRDT 开源标准)

Yjs:
- CRDT(Conflict-free Replicated Data Types)
- 离线优先
- 实时同步
- 与任何编辑器(Tiptap / ProseMirror / Slate)兼容
- 自托管(无需付费)

2. Liveblocks(托管 SaaS)

Liveblocks:
- 全托管(无后端)
- Presence(在线状态 / 光标)
- Yjs 兼容
- Comments(评论)
- Notifications(通知)
- AI Presence(AI Agent 协作)

3. PartyKit(Cloudflare 实时)

PartyKit:
- 基于 Cloudflare Workers + Durable Objects
- 全球边缘
- WebSocket 原生
- Serverless

4. Electric SQL(实时数据库)

Electric SQL:
- PostgreSQL 双向同步
- 本地优先
- 自动冲突解决
- 支持离线

6 大核心概念

概念 1:CRDT(无冲突复制数据类型)

// 传统数据库(冲突需要解决)
// 用户A + 用户B 同时修改同一行 → 覆盖

// CRDT(自动合并,无冲突)
// 用户A: Y.Array.insert(0, "Hello")
// 用户B: Y.Array.insert(0, "World")
// → Y.Array = ["Hello", "World"]  // 自动排序

import * as Y from "yjs";

// 1. 共享文档
const doc = new Y.Doc();

// 2. 共享类型
const yText = doc.getText("content");
const yArray = doc.getArray("items");
const yMap = doc.getMap("metadata");

// 3. 操作(自动 CRDT 合并)
yText.insert(0, "Hello");
yArray.push(["item1", "item2"]);
yMap.set("title", "My Doc");

概念 2:Provider(网络层)

// y-websocket(自托管 WebSocket)
import { WebsocketProvider } from "y-websocket";

const provider = new WebsocketProvider(
  "wss://your-server.com",
  "room-id",
  doc
);

// y-webrtc(P2P)
import { WebrtcProvider } from "y-webrtc";

const provider = new WebrtcProvider("room-id", doc, {
  signaling: ["wss://signaling.yjs.dev"],
});

// Liveblocks(托管)
import { Liveblocks } from "@liveblocks/yjs";
// Liveblocks 自动处理网络

概念 3:Awareness(在线状态)

// 用户光标 / 在线状态
provider.awareness.setLocalStateField("user", {
  id: "user_123",
  name: "Alice",
  color: "#ff0000",
  cursor: { x: 100, y: 200 },
});

// 监听其他用户
provider.awareness.on("change", ({ added, updated, removed }) => {
  const states = provider.awareness.getStates();
  for (const id of [...added, ...updated, ...removed]) {
    console.log("User:", id, states.get(id));
  }
});

概念 4:持久化(IndexedDB + Postgres)

// 1. 浏览器持久化(离线)
import { IndexeddbPersistence } from "y-indexeddb";

const persistence = new IndexeddbPersistence("room-id", doc);

// 2. 服务端持久化(Postgres)
import { LeveldbPersistence } from "y-leveldb";
const persistence = new LeveldbPersistence("./db", doc);

// 3. y-postgres(自定义)
async function saveDoc(doc: Y.Doc) {
  const state = Y.encodeStateAsUpdate(doc);
  await pg.query(
    "INSERT INTO docs (id, state) VALUES ($1, $2)",
    ["room-id", Buffer.from(state)]
  );
}

async function loadDoc(roomId: string) {
  const result = await pg.query(
    "SELECT state FROM docs WHERE id = $1",
    [roomId]
  );
  if (result.rows[0]) {
    Y.applyUpdate(doc, result.rows[0].state);
  }
}

概念 5:编辑器集成(Tiptap / ProseMirror)

// Tiptap + Yjs(最流行的富文本方案)
import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";

const editor = new Editor({
  element: document.querySelector("#editor")!,
  extensions: [
    StarterKit.configure({
      history: false,  // 禁用默认历史(Yjs 处理)
    }),
    Collaboration.configure({
      document: doc,
    }),
    CollaborationCursor.configure({
      provider: wsProvider,
      user: {
        name: "Alice",
        color: "#ff0000",
      },
    }),
  ],
});

概念 6:版本历史(Snapshot)

// 保存版本
async function saveSnapshot(doc: Y.Doc, name: string) {
  const snapshot = Y.snapshot(doc);
  const stateVector = Y.encodeStateVector(doc);

  await db.query(
    "INSERT INTO snapshots (doc_id, name, snapshot, state_vector, created_at) VALUES ($1, $2, $3, $4, NOW())",
    ["room-id", name, Buffer.from(Y.encodeSnapshot(snapshot)), Buffer.from(stateVector)]
  );
}

// 加载版本
async function loadSnapshot(doc: Y.Doc, snapshotId: string) {
  const result = await db.query(
    "SELECT snapshot FROM snapshots WHERE id = $1",
    [snapshotId]
  );

  const snapshot = Y.decodeSnapshot(result.rows[0].snapshot);
  Y.createDocFromSnapshot(doc, snapshot);
}

4 个实战项目

项目 1:Tiptap 富文本协作

# 安装
npm install @tiptap/react @tiptap/starter-kit \
  @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor \
  yjs y-websocket y-indexeddb
// app/doc/[id]/page.tsx
"use client";

import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
import { IndexeddbPersistence } from "y-indexeddb";
import { useEffect, useState } from "react";

export default function DocEditor({ docId }: { docId: string }) {
  const [provider, setProvider] = useState<WebsocketProvider | null>(null);

  useEffect(() => {
    const doc = new Y.Doc();
    const wsProvider = new WebsocketProvider(
      "wss://sync.example.com",
      docId,
      doc
    );
    const idbProvider = new IndexeddbPersistence(docId, doc);

    setProvider(wsProvider);
    return () => {
      wsProvider.destroy();
    };
  }, [docId]);

  const editor = useEditor({
    extensions: provider
      ? [
          StarterKit.configure({ history: false }),
          Collaboration.configure({ document: provider.doc }),
          CollaborationCursor.configure({
            provider,
            user: {
              name: "Current User",
              color: "#7c3aed",
            },
          }),
        ]
      : [StarterKit],
    immediatelyRender: false,
  }, [provider]);

  return (
    <div>
      <h1>Real-time Doc: {docId}</h1>
      <EditorContent editor={editor} />
    </div>
  );
}

项目 2:Liveblocks(托管,5 分钟上线)

// app/api/liveblocks-auth/route.ts
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: process.env.LIVEBLOCKS_SECRET_KEY!,
});

export async function POST(req: Request) {
  const { userId, roomId } = await req.json();

  // 创建 session
  const session = liveblocks.prepareSession(userId, {
    userInfo: { name: "Alice", color: "#7c3aed" },
  });

  // 权限
  if (roomId) {
    session.allow(roomId, session.FULL_ACCESS);
  }

  const { status, body } = await session.authorize();
  return new Response(body, { status });
}
// app/doc/[id]/page.tsx
"use client";

import { RoomProvider, useOthers } from "@/liveblocks.config";
import { ClientSideSuspense } from "@liveblocks/react";

function Cursors() {
  const others = useOthers();
  return others.map(({ connectionId, presence }) => (
    <div key={connectionId} style={{ color: presence?.color }}>
      {presence?.name}
    </div>
  ));
}

export default function Doc({ docId }: { docId: string }) {
  return (
    <RoomProvider id={docId} fallback={<div>Loading...</div>}>
      <ClientSideSuspense fallback={<div>Loading...</div>}>
        <Cursors />
        {/* Tiptap + Liveblocks Yjs 集成 */}
        <Editor docId={docId} />
      </ClientSideSuspense>
    </RoomProvider>
  );
}

项目 3:白板 / 绘图(Canvas 协作)

// canvas.ts
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";

const doc = new Y.Doc();
const provider = new WebsocketProvider("wss://sync.example.com", "whiteboard", doc);

// 共享 Y.Array 存储所有图形
const shapes = doc.getArray<Shape>("shapes");

interface Shape {
  id: string;
  type: "rect" | "circle" | "line";
  x: number;
  y: number;
  width: number;
  height: number;
  color: string;
}

// 添加图形
function addShape(shape: Shape) {
  shapes.push([shape]);
}

// 监听变化
shapes.observe(() => {
  // 重绘画布
  redrawCanvas(shapes.toArray());
});

// Canvas 绘制
function redrawCanvas(allShapes: Shape[]) {
  const canvas = document.querySelector("canvas")!;
  const ctx = canvas.getContext("2d")!;
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  for (const shape of allShapes) {
    ctx.fillStyle = shape.color;
    ctx.fillRect(shape.x, shape.y, shape.width, shape.height);
  }
}

项目 4:PartyKit(Cloudflare 实时)

// party/chat.ts
import type * as Party from "partykit/server";

export default class ChatServer implements Party.Server {
  messages: { user: string; text: string; ts: number }[] = [];

  constructor(readonly room: Party.Room) {}

  async onStart() {
    // 加载历史消息
    const stored = await this.room.storage.get("messages");
    if (stored) this.messages = stored as any;
  }

  async onConnect(conn: Party.Connection) {
    // 发送历史
    conn.send(JSON.stringify({ type: "history", messages: this.messages }));
  }

  async onMessage(message: string, sender: Party.Connection) {
    const data = JSON.parse(message);

    if (data.type === "message") {
      const msg = {
        user: data.user,
        text: data.text,
        ts: Date.now(),
      };

      this.messages.push(msg);

      // 持久化
      await this.room.storage.put("messages", this.messages);

      // 广播给所有客户端
      this.room.broadcast(JSON.stringify({ type: "message", message: msg }));
    }
  }
}

```typescript
// app/chat/[room]/page.tsx
"use client";

import PartySocket from "partykit/client";

export default function Chat({ params }: { params: { room: string } }) {
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState("");

  useEffect(() => {
    const socket = new PartySocket({
      host: "my-app.partykit.dev",
      room: params.room,
    });

    socket.addEventListener("message", (event) => {
      const data = JSON.parse(event.data);
      if (data.type === "message") {
        setMessages((prev) => [...prev, data.message]);
      } else if (data.type === "history") {
        setMessages(data.messages);
      }
    });

    return () => socket.close();
  }, [params.room]);

  return (
    <div>
      {messages.map((m, i) => (
        <div key={i}>{m.user}: {m.text}</div>
      ))}
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <button onClick={() => {
        socket.send(JSON.stringify({ type: "message", user: "Alice", text: input }));
        setInput("");
      }}>发送</button>
    </div>
  );
}

选型决策树

场景?
├── 富文本编辑     → Tiptap + Yjs(开源)
│   ├── 自托管    → y-websocket
│   └── 托管      → Liveblocks
├── 看板 / 数据库  → Liveblocks(最佳 Presence)
├── 白板 / 绘图   → Yjs + Canvas / tldraw
├── 实时聊天      → PartyKit(边缘)
├── 离线优先数据库 → Electric SQL
└── AI 协作       → Liveblocks AI Presence

预算?
├── $0          → Yjs 自托管
├── $0-100/月    → PartyKit / Liveblocks 免费层
└── $100+/月   → Liveblocks Pro

5 个常见坑

坑 1:CRDT 内存占用

❌ 一个超大文档(100 万字符)放在 Y.Text
→ Yjs 内部数据结构会膨胀

✅ 拆分多个 Y.Doc(章节级别)

坑 2:没有 IndexedDB 持久化

// ❌ 用户刷新页面 → 丢失所有未同步内容
const doc = new Y.Doc();

// ✅ 添加 IndexedDB
const persistence = new IndexeddbPersistence("room-id", doc);

坑 3:服务器不存储

// ❌ 仅 WebSocket(无持久化)
const provider = new WebsocketProvider(url, room, doc);

// ✅ WebSocket + PostgreSQL
// 每次 update 时同步到 DB
doc.on("update", (update) => {
  saveToDB(room, update);
});

坑 4:历史版本无限增长

// ❌ 保留所有版本
db.snapshots.create(...);  // 永久增长

// ✅ 限制保留时间
// 保留最近 30 天 + 每月 1 个
if (dayOfMonth === 1) saveSnapshot();
// 自动清理 30 天前
db.snapshots.delete("older than 30 days");

坑 5:权限缺失

// ❌ 任何人都能编辑
const provider = new WebsocketProvider(url, room, doc);

// ✅ 权限验证
const token = verifyJWT(req);
if (!token) return res.status(401).send("Unauthorized");

// Liveblocks
session.allow(roomId, session.FULL_ACCESS);

与之前内容的关系

7/22 React 19 RSC       → 单应用
8/5  Motion 动画          → 交互
8/7  Next.js SEO         → SEO
8/13 微前端              → 多应用
8/20 实时协作            → 多用户  ← 今天
→ "单应用 → 交互 → SEO → 多应用 → 多用户"完整前端栈

7 天落地路径

Day 1:选型

基于场景选 Yjs / Liveblocks / PartyKit

Day 2:本地 Yjs + Tiptap

npm install yjs @tiptap/extension-collaboration

Day 3:WebSocket 服务

# y-websocket 服务器
HOST=localhost PORT= 1234 npx y-websocket

Day 4:IndexedDB 持久化

new IndexeddbPersistence(roomId, doc);

Day 5:服务端持久化

// 定期保存到 Postgres

Day 6:Presence + 光标

provider.awareness.setLocalStateField(...)

Day 7:权限 + 生产

// JWT 验证 + 部署

我的看法

实时协作是 2026 年 SaaS 的"标配功能"

  1. 用户期望:Notion / Figma 教育市场
  2. 差异化:竞争必备
  3. AI 协作:未来方向
  4. 技术成熟:Yjs / CRDT 标准化
  5. 开源友好:自托管可行

对独立开发者的建议:

  • MVP:Yjs 自托管(免费)
  • 快速上线:Liveblocks(5 分钟)
  • 边缘实时:PartyKit
  • AI 协作:Liveblocks AI Presence
  • 离线优先:Electric SQL

参考


本文基于 Yjs 14 / Liveblocks AI Presence,2026 年 8 月最新实时协作实战。

🎨 前端 / Web 分类更多