Tiptap、Yjs实现协同编辑

什么是协同编辑

协同编辑的核心挑战是如何处理多个用户同时编辑同一份文档时产生的冲突。目前主要有以下几种解决方案:

1. 操作转换(Operational Transformation, OT)

  • 将用户的编辑操作转换为可以安全应用的操作
  • 需要复杂的转换算法来处理并发操作
  • Google Docs 早期版本使用的技术

2. 无冲突复制数据类型(CRDT)

  • 数据结构本身保证操作的可交换性和结合性
  • 无需中心化的冲突解决机制
  • Yjs 采用的技术方案

3. Yjs 的 CRDT 实现

Yjs 使用了一种称为 "Y.CRDT" 的特殊 CRDT 实现:

makefile 复制代码
用户A: 插入字符 'H' 在位置 0
用户B: 插入字符 'i' 在位置 0

传统方式: 需要协调谁先执行,可能产生冲突
CRDT方式: 每个操作都有唯一标识,最终结果确定且一致

Yjs 协同流程:

  1. 用户进行编辑操作
  2. Yjs 将操作转换为 CRDT 操作
  3. 操作通过网络传输给其他用户
  4. 其他用户接收并应用操作
  5. 所有用户最终达到一致状态

项目初始化

创建项目

sh 复制代码
pnpm create vite@latest

# 运行之后选择 react ts

进入项目目录并安装基础依赖:

sh 复制代码
cd your-project-name
pnpm install

WebSocket协同编辑

安装依赖

安装 tiptap 相关依赖:

sh 复制代码
pnpm add @tiptap/react @tiptap/extension-document @tiptap/extension-paragraph @tiptap/extension-text @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor

安装 yjs WebSocket 相关依赖:

sh 复制代码
pnpm add yjs y-websocket

实现步骤

1. 创建随机用户函数

首先创建一个生成随机用户名和颜色的工具函数:

ts 复制代码
function getRandomUser() {
  const names = [
    'Alice',
    'Bob',
    'Charlie',
    'Diana',
    'Eve',
    'Frank',
    'Grace',
    'Henry',
    'Ivy',
    'Jack'
  ]
  const colors = [
    '#ff6b6b',
    '#4ecdc4',
    '#45b7d1',
    '#96ceb4',
    '#feca57',
    '#ff9ff3',
    '#54a0ff',
    '#5f27cd',
    '#c44569',
    '#f8b500'
  ]

  const randomName = names[Math.floor(Math.random() * names.length)]
  const randomColor = colors[Math.floor(Math.random() * colors.length)]

  return {
    name: randomName,
    color: randomColor
  }
}

2. 创建 YDoc 和 WebSocket Provider 实例

ts 复制代码
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'

const ydoc = new Y.Doc()
const provider = new WebsocketProvider('ws://localhost:1234', 'room', ydoc)

3. 配置编辑器扩展

ts 复制代码
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'

const extensions = [
  Document,
  Text,
  Paragraph,
  Collaboration.configure({
    document: ydoc
  }),
  CollaborationCursor.configure({
    provider,
    user: {
      name: getRandomUser().name,
      color: getRandomUser().color
    },
  })
]

4. 创建组件

使用 EditorProvider 包装器来实现协同编辑:

ts 复制代码
import { EditorProvider } from '@tiptap/react'

export default function TiptapWs() {
  const content = ''  // 初始内容
  return (
    <EditorProvider 
      content={content} 
      extensions={extensions}
    />
  )
}

注意: 在这个实现中,我们使用了 EditorProvider 而不是 useEditor hook,这是一种更简洁的方式来创建协同编辑器。

5. 启动 WebSocket 服务

安装 WebSocket 服务器依赖:

sh 复制代码
pnpm add @y/websocket-server -D

启动 WebSocket 服务:

sh 复制代码
HOST=0.0.0.0 PORT=1234 npx y-websocket

现在多个用户可以通过 WebSocket 连接进行实时协同编辑。

如图:

WebRTC协同编辑

WebRTC 实现的协同编辑不需要中心化的服务器,通过点对点连接实现协同。

安装依赖

安装 tiptap 相关依赖:

sh 复制代码
pnpm add @tiptap/react @tiptap/extension-document @tiptap/extension-paragraph @tiptap/extension-text @tiptap/extension-placeholder @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor

安装 yjs WebRTC 相关依赖:

sh 复制代码
pnpm add yjs y-webrtc

实现步骤

1. 创建 YDoc 和 WebRTC Provider 实例

ts 复制代码
import * as Y from 'yjs'
import { WebrtcProvider } from 'y-webrtc'

const ydoc = new Y.Doc()
const provider = new WebrtcProvider('tiptap-collaboration-cursor-extension', ydoc)

重要说明:

  • 第一个参数是房间名称,相同房间名称的用户会连接到同一个协同会话
  • WebRTC 使用 P2P 连接,不需要中心化服务器

2. 完整组件实现

ts 复制代码
import { EditorContent, useEditor } from '@tiptap/react'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Placeholder from '@tiptap/extension-placeholder'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import * as Y from 'yjs'
import { WebrtcProvider } from 'y-webrtc'

function getRandomUser() {
  // ... 与上面相同的实现
}

const ydoc = new Y.Doc()
const provider = new WebrtcProvider('tiptap-collaboration-cursor-extension', ydoc)

export default function TiptapWebRTC() {
  const editor = useEditor({
    extensions: [
      Document,
      Paragraph,
      Text,
      Collaboration.configure({
        document: ydoc
      }),
      CollaborationCursor.configure({
        provider,
        user: {
          name: getRandomUser().name,
          color: getRandomUser().color
        }
      }),
      Placeholder.configure({
        placeholder: 'Write something ... It'll be shared with everyone else looking at this example.'
      })
    ]
  })

  return <EditorContent editor={editor} />
}

如图:

相关样式文件

css 复制代码
.tiptap {
  :first-child {
    margin-top: 0;
  }

  /* Placeholder (at the top) */
  p.is-editor-empty:first-child::before {
    color: var(--gray-4);
    content: attr(data-placeholder);
    float: left;
    height: 0;
    pointer-events: none;
  }

  p {
    word-break: break-all;
  }

  /* Give a remote user a caret */
  .collaboration-cursor__caret {
    border-left: 1px solid #0d0d0d;
    border-right: 1px solid #0d0d0d;
    margin-left: -1px;
    margin-right: -1px;
    pointer-events: none;
    position: relative;
    word-break: normal;
  }

  /* Render the username above the caret */
  .collaboration-cursor__label {
    border-radius: 3px 3px 3px 0;
    color: #0d0d0d;
    font-size: 12px;
    font-style: normal;
    font-weight: 600;
    left: -1px;
    line-height: normal;
    padding: 0.1rem 0.3rem;
    position: absolute;
    top: -1.4em;
    user-select: none;
    white-space: nowrap;
  }
}

WebRTC vs WebSocket 对比

特性 WebRTC WebSocket
连接方式 点对点 (P2P) 客户端-服务器
服务器需求 无需中心化服务器 需要 WebSocket 服务器
延迟 通常更低 取决于服务器性能
可扩展性 受浏览器连接数限制 服务器可处理大量连接
网络穿透 可能需要 STUN/TURN 简单的 HTTP 升级
适用场景 小团队协作 大规模协同编辑

最佳实践和注意事项

1. 用户状态管理

建议为每个用户分配持久化的身份标识:

ts 复制代码
// 可以从 localStorage 或用户登录信息获取
const userId = localStorage.getItem('userId') || generateUserId()
const userName = localStorage.getItem('userName') || 'Anonymous'

CollaborationCursor.configure({
  provider,
  user: {
    name: userName,
    color: getUserColor(userId) // 基于用户ID生成固定颜色
  }
})

2. 错误处理和重连

ts 复制代码
// WebSocket 重连处理
provider.on('status', (event) => {
  console.log('Connection status:', event.status) // 'connecting' | 'connected' | 'disconnected'
})

provider.on('connection-error', (event) => {
  console.error('Connection error:', event)
  // 实现重连逻辑
})

3. 数据持久化

ts 复制代码
// 可以将文档内容持久化到数据库
ydoc.on('update', () => {
  const content = editor.getHTML()
  // 保存到数据库或本地存储
  saveDocument(content)
})

4. 性能优化

  • 使用 Collaboration 扩展的 fragment 选项来指定协同的文档片段
  • 合理设置 CollaborationCursor 的更新频率
  • 对于大型文档,考虑使用文档分片

5. 安全考虑

  • 在生产环境中,WebSocket 服务器应该添加身份验证
  • 考虑添加内容过滤和权限控制
  • WebRTC 连接可能需要配置 STUN/TURN 服务器来处理 NAT 穿透

总结

本文展示了使用 Tiptap 和 Yjs 实现协同编辑的两种方案:

  1. WebSocket 方案:适合需要中心化控制、大规模用户协同的场景
  2. WebRTC 方案:适合小团队、低延迟需求的 P2P 协同场景

两种方案都提供了完整的协同编辑功能,包括实时文本同步、光标位置共享、用户状态显示等。选择哪种方案取决于具体的应用场景和技术要求。

参考资料

相关推荐
摸鱼的春哥6 分钟前
春哥的Agent通关秘籍07:5分钟实现文件归类助手【实战】
前端·javascript·后端
念念不忘 必有回响10 分钟前
viepress:vue组件展示和源码功能
前端·javascript·vue.js
C澒15 分钟前
多场景多角色前端架构方案:基于页面协议化与模块标准化的通用能力沉淀
前端·架构·系统架构·前端框架
崔庆才丨静觅17 分钟前
稳定好用的 ADSL 拨号代理,就这家了!
前端
江湖有缘18 分钟前
Docker部署music-tag-web音乐标签编辑器
前端·docker·编辑器
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端