Webman需依托WebSocket实现协同编辑,而非HTTP控制器;须手动管理文档状态、操作校验与广播、断连恢复,并配合结构化编辑器(如Quill)及中文输入法composition事件处理。

Webman 本身不内置协同编辑能力,也不直接提供富文本编辑器或 OT/CRDT 同步算法。想用它搭出真正可用的多人协作编辑器,核心不是“怎么装 Webman”,而是“怎么把协同逻辑跑在 Webman 的 WebSocket 服务上”。否则只是个单机编辑器套了层 PHP 框架壳。
为什么不能只靠 Webman 的路由和控制器?
协同编辑的本质是:多个客户端对同一份文档做并发修改,且必须保证最终一致、不丢内容、不乱序。这需要:
- 服务端维持文档状态(不能每次请求都查 DB 再覆盖)
- 客户端发送的是「操作」(如“在位置 12 插入文字‘hello’”),不是整段 HTML
- 服务端要按顺序应用操作,并广播给其他客户端
- 中文输入法场景下,要拦截未完成的输入(compositionstart/compositionend)
而 Webman 的 HTTP 控制器是无状态、短连接的,天然不适合这类场景。你得用它的 WebSocket 扩展模块(如 webman/websocket 或基于 workerman 自建 gateway)来长连接通信。
WebSocket 连接必须自己管理文档状态
Webman 的 WebSocket 服务默认不维护任何共享内存或文档快照。你得手动实现:
- 用
static数组或Swoole\Table存每个文档 ID 对应的当前内容 + 操作历史 - 为每个连接分配唯一 client_id,记录它正在编辑哪个 doc_id
- 收到操作时,先校验是否属于该文档、是否带合法 timestamp/revision,再 apply 并广播
- 断连重连时,需下发全量快照 + 断连期间的操作补丁(否则会丢编辑)
示例关键点:
// app/controller/WebSocketController.php
public function onMessage($connection, $data) {
$op = json_decode($data, true);
$doc_id = $op['doc_id'] ?? '';
$client_id = $connection->id;
<pre class="brush:php;toolbar:false;">// 不要用 $_SESSION —— WebSocket 连接不走 PHP-FPM session 机制
if (!isset(self::$docs[$doc_id])) {
self::$docs[$doc_id] = ['content' => '', 'version' => 0];
}
// 简单版本控制(实际需 OT 或 Yjs)
if ($op['version'] != self::$docs[$doc_id]['version']) {
$connection->send(json_encode(['type' => 'rebase', 'snapshot' => self::$docs[$doc_id]['content']]));
return;
}
// 应用操作(此处仅为示意,真实需解析并插入到 DOM 树对应位置)
self::$docs[$doc_id]['content'] = $this->applyInsert(self::$docs[$doc_id]['content'], $op);
self::$docs[$doc_id]['version']++;
// 广播给其他 client_id ≠ 当前连接的用户
foreach (self::$connections as $conn) {
if ($conn->id !== $client_id && $conn->doc_id === $doc_id) {
$conn->send($data);
}
}}
前端不能用 contenteditable 直接发 HTML
直接把 div[contenteditable] 的 innerHTML 发给后端,会导致:
Webman 2.2.0版本强化了 TCP/UDP 服务支持,优化路由组管理,并增强异步任务处理能力。结合协程与连接池技术,Webman 能轻松应对高并发场景,适用于网站、接口服务、即时通讯、物联网及游戏开发,兼具高性能、灵活扩展与稳定可靠,是多场景 PHP 服务开发的理想选择。
- 样式标签(
<span style="color:red"></span>)被不同浏览器渲染不一致 - 换行、空格、零宽字符等不可见字符处理混乱
- 无法做精确光标同步(别人插了一段文字,你的光标可能跳到错误位置)
必须用结构化编辑器,比如:
-
Quill(输出 Delta 格式,轻量、易集成) -
Tiptap(基于 ProseMirror,支持自定义节点和协作扩展) -
Yjs配合y-websocket(自动处理 OT 和离线合并,但需服务端配合 Yjs 协议)
选 Quill 的话,后端只需理解 Delta(数组操作),不用解析 HTML:
// 前端发送
{
"doc_id": "abc123",
"delta": [
{"insert": "Hello "},
{"attributes": {"bold": true}, "insert": "world"},
{"insert": "!"}
],
"version": 5
}
最易踩的坑:没处理中文输入法的 composition 事件
用户按“zhongwen”,还没选词时,浏览器会先触发 compositionstart;选完“中文”后才触发 compositionend。中间若有人插入文字,你的光标位置和输入框内容会错乱。
- 必须监听
compositionstart暂停发送变更 - 在
compositionend后,再把整段输入作为一次操作提交 - 服务端不能在 composition 过程中 apply 其他人的操作到该客户端的编辑器本地状态(否则会覆盖未确认输入)
Quill 默认不处理这个,得自己 patch:
quill.on('text-change', (delta, oldDelta, source) => {
if (source === 'user' && !isComposing) {
sendToWebSocket({ doc_id, delta });
}
});
quill.root.addEventListener('compositionstart', () => isComposing = true);
quill.root.addEventListener('compositionend', () => {
isComposing = false;
// 此时再触发一次 text-change,确保最终内容发出
});
协同编辑真正的复杂点不在框架选型,而在操作语义的精确建模和时序控制。Webman 只是帮你扛住连接和转发,剩下的——文档模型、操作归一、冲突消解、光标同步——每一块都得自己填平。别指望“装个包就协同”,那只会在线上出现“两人打字互相覆盖”或“输入法下文字消失”这种现场翻车。










