交互体验
在任意一侧输入或拖选,观察另一侧的变化。也可以试试独立撤销、只读和断线重连。
示例使用真实 Yjs,通过同页内存通道同步。刷新或离开页面会清空内容;“已同步”不表示已保存。完整示例可在“接入代码”中查看。 下方“协作记录”由同页中继运行的服务端扩展确认后写入:作者取自连接身份,连续输入合并成一条,撤销等操作单独记一条; 记录只保留最近 200 条、改动片段保留 1 天。“历史版本”由服务端存档时生成,每 20 秒最多一个,也可以保存命名版本; 以参与者 A 的身份恢复后,B 的文档同步变化,协作记录写入“恢复了版本”。接入方式见 MD Editor 协作记录与历史版本。
接入方式
协同目前是实验能力,需要时再安装。默认的 Core、StarterKit、Vue、React 和 MD Editor 不包含协同依赖。
1. 安装依赖
使用 Hocuspocus 时安装两个包;自建传输只需要第一个。
pnpm add @quirekit/msfe-collaboration @quirekit/msfe-collaboration-hocuspocus
共享内容是 Markdown 源码。模式、主题和滚动各自独立;图片上传由应用提供稳定地址。
2. 创建会话
用文档 ID、服务地址和应用凭证创建 session:
import { sharedSource, mountSharedSource } from './collaboration/session'
const session = sharedSource('document-123', {
url: 'wss://your-service.example/collaboration',
sessionEndpoint: 'https://your-service.example/api/collaboration/open',
token: () => applicationAccessToken(),
})
url 用于 WebSocket 同步,sessionEndpoint 是应用的会话接口,返回文档身份、epoch、格式和权限。只提供 WebSocket 地址还不能接入;请替换示例地址和凭证函数。
3. 准备并挂载
继续在同一文件中挂载编辑器,parent 是挂载元素。辅助函数会先准备会话;路由离开时可通过取消信号中止准备。
const controller = new AbortController()
const editor = await mountSharedSource(parent, session, controller.signal)
将下面的文件放在 collaboration/session.ts,与上面的调用代码配合使用。
import { createMsfeEditor, type MsfeEditor } from "@quirekit/msfe-core";
import { StarterKit } from "@quirekit/msfe-starter-kit";
import {
Collaboration,
createCollaborationSession,
type CollaborationSession,
} from "@quirekit/msfe-collaboration";
import {
hocuspocusProvider,
type HocuspocusProviderOptions,
} from "@quirekit/msfe-collaboration-hocuspocus";
/**
* @param documentId - Existing application document.
* @param service - Application admission and transport endpoints.
* @returns Host-owned session, retained even if preparation fails.
*/
export function sharedSource(
documentId: string,
service: HocuspocusProviderOptions,
): CollaborationSession {
return createCollaborationSession({ documentId, provider: hocuspocusProvider(service) });
}
/**
* @param parent - Editor mount.
* @param session - Host-owned source and history.
* @param signal - Route cancellation.
* @returns Mounted editor after authoritative preparation.
*/
export async function mountSharedSource(
parent: HTMLElement,
session: CollaborationSession,
signal: AbortSignal,
): Promise<MsfeEditor> {
// Finish admission and loading before allocating an editor.
await session.prepare({ signal });
signal.throwIfAborted();
// Session owns initial content and identity; extensions keep planning source edits.
return createMsfeEditor({
parent,
extensions: [StarterKit, Collaboration.configure({ session })],
});
}
新文档的创建和初始内容由服务负责。不要在每次连接时重新插入初始 Markdown,现存空文档也应保持为空。
通过原有 extensions 装配
已有编辑器添加 Collaboration.configure({ session }),省略初始 markdown。MD Editor、Vue 和 React 均使用原有扩展入口:
<MdEditor :extensions="[Collaboration.configure({ session })]" />
- 准备后挂载:session 准备完成后再渲染组件;同一 session 同时只绑定一个编辑器。
- 保留会话:组件卸载只解除绑定;再次挂载可继续使用正文和本地撤销历史。
- 避免深响应式:Vue 中用普通变量、
shallowRef或markRaw保存 session。
可选光标与协作者
需要姓名光标、选区和在线成员时,再添加 CollaborationPresence。两个扩展使用同一个 session:
import { Collaboration } from '@quirekit/msfe-collaboration'
import { CollaborationPresence } from '@quirekit/msfe-collaboration/presence'
const extensions = [
Collaboration.configure({ session }),
CollaborationPresence.configure({
session,
user: { id: 'alice', name: 'Alice', color: '#2563eb' },
onParticipants: peers => { /* 更新宿主的成员列表 */ },
}),
]
onParticipants 返回远端连接的 id、user 和 activity(editing / viewing)。可以使用示例中的 QuireAvatar 或自己的成员 UI;光标移动不会刷新成员列表,同一用户的多个连接分别显示。
- 当前广播主选区。隐藏源码或组件内部的位置显示为带
◇的虚线边界光标。 - 失焦后保留在线成员;断线时清理,重连后重新加入。远端更新不会移动本机焦点、选区或滚动,也不展开源码。
- presence 不进入正文、撤销或保存记录。只安装正文协同时,不分配 Awareness 或挂载光标层。
同步、保存与恢复
同步成功与保存成功是两个状态。 服务支持 checkpoint 时,调用 await session.save(),收到持久化回执后再显示“已保存”。
| 要做什么 | 使用方式 |
|---|---|
| 显示连接与保存状态 | 读取 session.state,通过 session.subscribe() 更新 UI。 |
| 断线重连 | 调用 session.prepare(),重新取得身份与权限。 |
| 禁止离线编辑 | 设置 offlineEditing: 'readonly';默认允许在内存中编辑。 |
| 撤销自己的改动 | 使用 editor.history;远端修改不进入本地撤销栈。 |
| 离开文档 | 取消未完成的准备,再销毁编辑器并关闭会话。 |
controller.abort()
editor.destroy()
await session.close({ mode: 'persist' })
关闭或保存失败时保留 session,提供重试或恢复副本入口。没有持久化服务时,使用 checkpoint 关闭方式;只有用户明确放弃内容才使用 discard。
| 字段 | 含义 |
|---|---|
lifecycle | 准备、可用、故障、待恢复或已关闭。 |
connection | 连接中、已连接或断开。 |
permission | 服务授予的读写权限。 |
synced | 传输已确认当前待同步更新。 |
durability | 本地修改的持久化状态;无保存能力时为 unsupported。 |
- 保存期间继续输入会保留 dirty 状态,删除文本也需要保存。不要用同步事件或延时器代替持久化回执。
- epoch 改变,或未保存分支被撤销写权限时,会话进入 recovery。保留
session.checkpoint(),再决定如何对比或导入;当前没有浏览器持久离线缓存。 close({ mode: 'checkpoint', persistRecovery })等待应用保存包含二进制身份与源码的恢复副本。组件卸载不会自动放弃内容。- 工具栏可读取
editor.history.canUndo、canRedo,用subscribe()订阅变化;命令、快捷键和浏览器撤销使用同一历史。 - 换文档时重新装配实例;显式
documentId必须与会话一致。replaceDocument()返回source-provider-locked,追加内容继续使用正常命令。 defineState().invert目前不能与协同历史组合,会报告unsupported-history-effect。不要把onChange再接到全文覆盖或另一套 Yjs 绑定。
自建传输与当前边界
服务负责鉴权、写权限、房间限制和持久化。示例中的只读开关、成员姓名与颜色只用于客户端交互,不能代替服务端校验。
自建传输实现 CollaborationProviderFactory:打开权威 manifest、同步 session 的 Y.Doc、报告连接与权限状态,并在断开时移除监听。保存能力 saveCheckpoint 可选。
光标通道另通过 acquirePresence() 返回 CollaborationPresenceTransport:clientId、getStates()、publish()、subscribe() 和 release()。未实现该能力时不要安装 presence;示例的 presence.ts 提供完整内存通道。Hocuspocus 会按挂载和销毁租用、撤回通道并清理定时器。
adoptCollaborationSession() 可借用已准备的 Y.Doc 与静态 manifest。宿主负责初始化、连接、权限及释放,session 不销毁借用的文档;动态连接报告和保存回执应通过 provider 工厂接入。
当前已验证 Chromium 下的双人编辑与远端呈现。系统输入法、Safari/Firefox、多参与者规模和生产数据库故障恢复仍待发布验收。