当前位置: 首页 > news >正文

基于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)

包名版本备注
nodev22.13.0
express^5.1.0
ws^8.18.2
y-websocket^3.0.0
yjs^13.6.27

2、数据流程图

client N client 2 client 1 quill(web) y-quill(web) Yjs(web) y-websocket(web) websocket(服务端) markdown编辑器输入:hellow foods 变更数据上下文 数据转换成Yjs数据格式 变更数据上下文(Yjs的数据格式) 变更数据上下文(Yjs的数据格式) 同步client 1中的数据变更 同步client 1中的数据变更 client N client 2 client 1 quill(web) y-quill(web) Yjs(web) y-websocket(web) websocket(服务端)

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>
http://www.xdnf.cn/news/657901.html

相关文章:

  • 学习黑客Metasploit 框架的原理
  • 端午假期 · 粽享欢乐
  • 开源Vue表单设计器 FcDesigner 组件提供的方法详解
  • 《1.1_4计算机网络的分类|精讲篇|附X-mind思维导图》
  • deepseek告诉您http与https有何区别?
  • CQF预备知识:一、微积分 -- 1.4.6 莱布尼茨法则详解
  • Mysql在SQL层面的优化
  • [Java实战]SpringBoot集成SNMP实现OID数据获取:原理、实践与测试(三十三)
  • GitLab 从 17.10 到 18.0.1 的升级指南
  • 动态规划-918.环形子数组的最大和-力扣(LeetCode)
  • SQL Driver
  • 16QAM通信系统设计与实现(上篇)——信号生成与调制技术(python版本)
  • leetcode 525. 连续数组
  • CertiK联创顾荣辉做客纽交所,剖析Bybit与Coinbase事件暴露的Web3安全新挑战
  • 原子操作(C++)
  • 深度体验:海螺 AI,开启智能创作新时代
  • liunx、ubantu22.04安装neo4j数据库并设置开机自启
  • AI工程师跑路了-SpringAi来帮忙
  • 学习路之PHP--easyswoole安装入门
  • LINUX安装运行jeelowcode前端项目
  • SC89171的介绍和使用
  • 炫云云渲染,构筑虚实交融的3D数字新视界
  • AI的“软肋”:架构设计与业务分析的壁垒
  • OpenCV CUDA模块图像过滤------创建一个行方向的一维积分(Sum)滤波器函数createRowSumFilter()
  • 爬虫IP代理效率优化:策略解析与实战案例
  • Neo4j(三) - 使用Java操作Neo4j详解
  • 第12次05: 用户中心-用户基本信息
  • 如何用ChatGPT提升学术长文质量
  • Golang Gin框架基础与实践指南
  • 【学习笔记】GitLab 下载安装与配置