基于yjs实现协同编辑页面

一、前言

协同编辑已成为现代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-room
    if (!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 = () => {
  // 会自动同步到服务端,变更model
  yarray.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 变化后更新dom
  todoList.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>

### 基于 Canvas-Editor 实现协同编辑的方法 #### 协同编辑的核心原理 Canvas-Editor 编辑器从核心底层就支持协同处理,采用的是数组而非树状的数据结构来存储和同步数据[^2]。这意味着,在实现协同编辑时,开发者可以利用这一特性简化操作逻辑。 #### 使用 Yjs 进行实时协作 为了实现高效的在线文档协同编辑功能,推荐使用 Yjs 库作为后端支持[Yjs官方](https://yjs.dev/)。Yjs 提供了一套完整的冲突解决机制以及丰富的插件生态,能够很好地满足多人同时编辑的需求。特别指出的是,Yjs 的文本原生支持 Delta 数据结构,这与 Quill 富文本编辑器兼容良好,便于集成到现有项目中。 #### 配置步骤 ##### 安装依赖包 首先安装必要的 npm 包: ```bash npm install yjs @hufe921/canvas-editor ``` ##### 初始化 Yjs 和 Canvas-Editor 创建一个新的 JavaScript 文件并引入所需的模块: ```javascript import * as Y from 'yjs'; import Editor from '@hufe921/canvas-editor'; // 创建共享类型实例 const doc = new Y.Doc(); const type = doc.getText('editor'); // 将 Yjs 文档绑定至 WebSocket Provider (假设服务器地址为 ws://localhost:8080) new WebsocketProvider('ws://localhost:8080', 'room-name', doc); // 初始化 Canvas-Editor 并传入 Y.Text 对象用于双向绑定 new Editor( document.querySelector('.canvas-editor'), [{ value: type }], {} ); ``` 上述代码片段展示了如何将 Yjs 与 Canvas-Editor 结合起来,从而实现实时的多人协作编辑体验。通过这种方式,任何对 `type` 变量所做的更改都会自动传播给其他连接上的客户端,并反映在各自的界面之上。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值