使用Node.js,WebSocket和CRDT构建共享代码编辑器
#网络开发人员 #node #mongodb #websocket

如何使用Google文档,CodeShare.io或Excalidraw工作等协作编辑平台?

这些平台提供实时协作,与其他用户同步以及解决冲突的机制。

可以通过使用WebSocket实现实时通信和文档的同步,因为它可以在用户进行更新时即可发送并接收到即时更新。例如,当用户A进行更改时,此信息将通过WebSocket发送到服务器,然后服务器将此更改广播到其他已连接的用户。

困难的部分是解决冲突。您可能会知道解决GIT存储库中合并冲突有多么困难。想象一下自动化此过程ð。

幸运的是,有许多大脑人员正在从事分布式冲突解决算法。

主要是有两种技术可以做到这一点:

  1. 操作转换(OT)
  2. 无冲突的复制数据类型(CRDT)

我会深入研究每种技术的算法细节,但我会引用this博客的几行。

区别在于,使用OT(操作转换),每当您编辑文档时,必须通过单个服务器发送编辑。 Google在Google文档的情况下提供了该服务器。因此,您所有的交流,您的所有协作都必须通过这一台服务器进行。

和CRDT是不同的,因为它们是分散的。他们不需要单个服务器才能工作。但是,您可以通过任何可用的网络同步设备。_

i为此项目选择了CRDT,因为有许多编写文档的开源实现。

鸟类的视野

所有平台都有类似的用户流,看起来像这样:

  1. 用户A创建新房间/文档
  2. 该应用程序提供了唯一的邀请URL /邀请代码< / li>
  3. 用户A向用户B
  4. 共享此邀请代码
  5. 用户b输入应用程序上的邀请代码,并加入房间/文档
  6. 用户A或用户B选择编程语言并开始编码

我将使用以下模式存储有关房间的相关信息:

interface Room {
    roomId: string,
    owner: string,
    dateCreated: Date,
    participants: string[],
    programmingLanguage: string
}

当然,文档的内容也可以存储在数据库中。

现在,众所周知,我们知道需要存储的数据的模式,我们可以为房间操作设计API端点。

API Design

前端反应应用

i使用Vite和二手材料UI V5组件库生成了一个React + Typescript项目。

带有创建房间按钮的基本主页和加入房间输入字段就足够了,如下所示:

Code Companion Home Page

对于代码室页面,我们将使用YJS,这是一个CRDT实现,并具有编辑器绑定的编辑器绑定,例如散文镜,Quill,Quill,Monaco Editor等。

我们将在此应用程序中使用摩纳哥编辑器。因此,我们需要摩纳哥编辑器组件,用于YJS的React和Monaco编辑器结合。我们还需要YJS的WebSocket模块,这将传播到服务器的编辑器中的更改。以下是安装所有这些模块的命令:

npm install monaco-editor yjs y-monaco y-websocket

代码室页面

以下是代码室页面的代码:

在您的房间页面中创建摩纳哥编辑器组件
当编辑器组件安装时,我们创建y.doc(),这是共享数据结构
然后,我们通过y-websocket的WebSocketProvider连接到后端服务器,并与之设置共享数据类型。
最后,我们将摩纳哥编辑器绑定到此共享数据类型

import { useTheme } from "@mui/material";
import { useState, useRef, useEffect } from "react";
import { Editor } from "@monaco-editor/react";
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { MonacoBinding } from 'y-monaco';
import { editor } from "monaco-editor";

const serverWsUrl = import.meta.env.VITE_SERVER_WS_URL;

export default function CodeRoom() {
    const theme = useTheme();

    const editorRef = useRef<editor.IStandaloneCodeEditor>();

    function handleEditorDidMount(editor: editor.IStandaloneCodeEditor) {
        editorRef.current = editor;

        // Initialize yjs
        const doc = new Y.Doc(); // collection of shared objects

        // Connect to peers with WebSocket
        const provider: WebsocketProvider = new WebsocketProvider(serverWsUrl, "roomId", doc);
        const type = doc.getText("monaco");

        // Bind yjs doc to Manaco editor
        const binding = new MonacoBinding(type, editorRef.current!.getModel()!, new Set([editorRef.current!]));

    }

    return (
        <>
        <Editor 
            height="100vh"
            language={"cpp"}
            defaultValue={"// your code here"}
            theme={theme.palette.mode === "dark" ? "vs-dark" : "vs-light"}
            onMount={handleEditorDidMount}
        />
        </>
    );
}

后端Express,Node.js,WebSocketServer

后端由一个简单的Express服务器和WebSocketServer组成,该服务器会倾听连接事件,并使用Y-Websocket模块提供的实用程序文件来设置连接。您可以探索SetupWsconnection文件以深入了解。

import express, { Request, Response } from 'express';
import { createServer } from 'http';
import cors from 'cors';
import { logger } from './logger';
import { WebSocketServer } from 'ws';
const setupWSConnection = require('y-websocket/bin/utils').setupWSConnection;

/**
 * CORSConfiguration
 */
export const allowedOrigins = ['http://localhost:5173'];

/**
 * Server INITIALIZATION and CONFIGURATION
 * CORS configuration
 * Request body parsing
 */
const app = express();
app.use(cors(
  {
    origin: allowedOrigins,
    methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
    allowedHeaders: "Content-Type",
    credentials: true
  }
));
app.use(express.json());


/**
 * Create an http server
 */
export const httpServer = createServer(app);

/**
 * Create a wss (Web Socket Secure) server
 */
export const wss = new WebSocketServer({server: httpServer})

function onError(error: any) {
  logger.info(error);
}

function onListening() {
  logger.info("Listening")
}

httpServer.on('error', onError);
httpServer.on('listening', onListening);

/**
* On connection, use the utility file provided by y-websocket
*/
wss.on('connection', (ws, req) => {
  logger.info("wss:connection");
  setupWSConnection(ws, req);
})

和瞧!您现在有一个共享的代码编辑器。我在这里添加了更多功能:

  1. 在前端添加了编程语言的下拉列表,聆听用户后端上的语言更改事件,然后将其广播给其他同行。使用socket.io服务器和客户端创建了一个单独的WebSocket连接。
  2. 同样,每当新参与者进入房间时,参与者列表都会在前端显示,并且以同样的方式,此信息通过后端传播到其他同行。
  3. 使用MongoDB云实例连接后端服务器,用于存储房间信息。

Extra Features

演示

您可以通过访问此网站来使用此应用程序:

https://code-companion.netlify.app

或观看简短的演示视频:

此应用程序已部署在NetLify和AWS EC2上,并且数据库部署在MongoDB云上。

快乐编码:)