基于yjs实现协同编辑页面
目录
- 一、前言
- 二、YJS简介
- 三、yjs+quill+vue实现协作markdown编辑器
- 1、依赖第三方包列表
- 2、数据流程图
- 3、功能实现
- 四、yjs+vue实现协作ToDoList
- 1、功能实现
一、前言
协同编辑已成为现代Web应用的核心功能之一,它允许多用户实时协作,共同修改同一份文档或数据。然而,实现这一功能往往涉及复杂的冲突解决、状态同步和实时通信问题。
Yjs作为一款高效的开源协同编辑框架,为开发者提供了简洁而强大的解决方案。它基于CRDT(无冲突复制数据类型)技术,确保数据一致性,同时支持与主流前端框架无缝集成。
本文将介绍如何利用Yjs快速构建一个协同编辑页面,涵盖从基础原理到实际实现的完整流程。无论你是初次接触协同技术,还是希望优化现有系统,本文都能提供清晰的指引和实用的代码示例。
二、YJS简介
Yjs是一个支持实时协作的CRDT框架,用于构建多人协同编辑的应用程序。
官网:Yjs官网地址
三、yjs+quill+vue实现协作markdown编辑器
1、依赖第三方包列表
【1】客户端(VUE)
包名 | 版本 | 备注 |
---|---|---|
vue | ^3.5.13 | |
yjs | ^13.6.27 | |
y-quill | ^1.0.0 | |
quill-cursors | ^1.0.0 | |
y-websocket | ^3.0.0 |
【2】服务端(Node)
包名 | 版本 | 备注 |
---|---|---|
node | v22.13.0 | |
express | ^5.1.0 | |
ws | ^8.18.2 | |
y-websocket | ^3.0.0 | |
yjs | ^13.6.27 |
2、数据流程图
3、功能实现
【1】服务端
- 创建一个websocket服务
// 引入必要的模块
const http = require('http');
const WebSocket = require('ws');
const { applyUpdate, encodeStateAsUpdate, encodeStateVector } = require('yjs');
const Y = require('yjs');// 创建一个 HTTP 服务器
const server = http.createServer((req, res) => {// 处理普通的 HTTP 请求(可选)res.writeHead(200, { 'Content-Type': 'text/plain' });res.end('WebSocket Server Running\n');
});// 创建 WebSocket 服务器,但不自动处理所有连接
const wss = new WebSocket.Server({ noServer: true });// 自定义升级逻辑:只允许路径为 /my-room 的连接
wss.on('headers', (headers, req) => {// 添加额外的响应头(可选)headers.push('X-Custom-Header: Hello');
});
// 存储房间与对应的 Y.Doc 实例
const docs = {};
wss.on('connection', (ws, request) => {const url = new URL(request.url, `http://${request.headers.host}`);const room = url.pathname; // 获取路径名,例如:/my-roomif (!docs[room]) {docs[room] = new Y.Doc();}const ydoc = docs[room];const stateConnection = encodeStateAsUpdate(ydoc);applyUpdate(ydoc, stateConnection);ws.on('message', (data) => {try {const state = encodeStateAsUpdate(ydoc);applyUpdate(ydoc, state);// 广播更新给其他所有连接到同一房间的客户端wss.clients.forEach(client => {if (client !== ws && client.readyState === WebSocket.OPEN) {client.send(data);}});} catch (error) {console.error('处理更新时出错:', error);}});ws.on('close', () => {console.log(`客户端断开连接:${room}`);});
});
// 拦截升级请求,根据路径决定是否允许升级为 WebSocket
server.on('upgrade', (request, socket, head) => {const pathname = new URL(request.url, `http://${request.headers.host}`).pathname;if (pathname === '/my-room') {wss.handleUpgrade(request, socket, head, (ws) => {wss.emit('connection', ws, request);});} else if (pathname === '/todo-room') {wss.handleUpgrade(request, socket, head, (ws) => {wss.emit('connection', ws, request);});} else {console.log(`拒绝连接:路径不匹配 (${pathname})`);socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');socket.destroy();}
});
// 让服务器监听特定端口
const PORT = process.env.PORT || 1234;
server.listen(PORT, () => {console.log(`WebSocket 服务器正在运行于 ws://localhost:${PORT}/my-room`);
});
【2】web代码
- 创建一个QuillEditor类,用于二次封装quill组件
- 创建Collaboration服务类,用于封装yjs、y-websocket、y-quill、quill的绑定
1)QuillEditor.ts
import Quill from 'quill';
import QuillCursors from 'quill-cursors';
import 'quill/dist/quill.snow.css';
// 将 QuillCursors 模块注册到 Quill 编辑器中,以便在编辑器中使用光标模块。
Quill.register('modules/cursors', QuillCursors);export class QuillEditor {quillInstance: any;constructor(editorContainer: any) {this.quillInstance = new Quill(editorContainer, {theme: 'snow',modules: {cursors: true}});}getInstance() {return this.quillInstance;}
}
2)collaborationService.ts
import { QuillBinding } from 'y-quill';
import { WebsocketProvider } from 'y-websocket';
import * as Y from 'yjs';
// import Quill from 'quill';
export class CollaborationService {wsUrl: string;constructor(wsUrl: string) {this.wsUrl = wsUrl;}setupCollaboration(docId: string, quill: any) {const ydoc = new Y.Doc();const provider = new WebsocketProvider(this.wsUrl, docId, ydoc);const ytext = ydoc.getText('quill');// 实现quill和ytext的绑定new QuillBinding(ytext, quill, provider.awareness);provider.on('status', event => {console.log('y-js的status',event.status);});ydoc.on('update', update => {//通知服务端更新文档Y.applyUpdate(ydoc, update)});return { ydoc, provider };}
}
3)APP.vue
<script setup lang="ts">
import { onMounted, ref, onUnmounted } from 'vue'
import { CollaborationService } from './components/collaboration/collaborationService.ts';
import { QuillEditor } from '@/common/utils/QuillEditor.ts';
const wsUrl = `ws://${location.hostname}:1234`;
const docId = 'my-room';
const editorContainer = ref(null);
onMounted(() => {const quillEditor = new QuillEditor(editorContainer.value);const collaborationService = new CollaborationService(wsUrl);collaborationService.setupCollaboration(docId, quillEditor.getInstance());
})
</script><template><div id="app"><div ref="editorContainer" class="quill-editor"></div></div>
</template><style scoped>
#app {max-width: 800px;margin: 50px auto;
}
.quill-editor {height: 400px;
}
</style>
四、yjs+vue实现协作ToDoList
1、功能实现
【1】服务端
参考yjs+quill+vue实现协作markdown编辑器 的服务端代码
【2】web端
- 创建一个TODOLIST组件,
1) ToDoList.vue
<template><div ><div><input v-model="newTodo" type="text" /><button @click="addTodoList">添加</button></div><ul><li v-for="(item, index) in todoList" :key="index">{{ item }}</li></ul></div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket';
const props = defineProps({wsUrl: {type: String,required: true},docId: {type: String,required: true}
})
const {wsUrl, docId} = props
const ydoc = new Y.Doc()
const yarray = ydoc.getArray('todoList')
const todoList = ref<any[]>([])
const newTodo = ref<string>('')
const addTodoList = () => {// 会自动同步到服务端,变更modelyarray.push([newTodo.value]);
}
const provider = new WebsocketProvider(wsUrl, docId, ydoc);
provider.on('status', event => {console.log('y-js的status',event.status);
});yarray.observe((yarrayEvent: any) => {// yarray 变化后更新domtodoList.value = yarray.toArray()
})
</script>
2)App.vue
<script setup lang="ts">
import ToDoList from './components/ToDoList.vue';
const wsUrl = `ws://${location.hostname}:1234`;
const todoListID = 'todo-room'
</script>
<template><div id="app"><ToDoList :wsUrl="wsUrl" :docId="todoListID"/></div>
</template><style scoped>
#app {max-width: 800px;margin: 50px auto;
}
</style>