灏天阁

基于React+Socket.io实现简易在线文档协作编辑

· Yin灏

许久之前写过写一篇基于 socket.io 和 canvas 实现的共享协作画板 (更新) - 掘金 (juejin.cn)的文章,意犹未尽,重新捡起 socket.io 继续开疆拓土。

本文将介绍如何使用 React 和 Socket.io 实现在线文档协作编辑。在线协作文档编辑是一种非常方便的方式,可以帮助团队在不同的地理位置进行协作工作,提高工作效率。当团队成员在不同的地理位置时,使用在线协作文档编辑工具可以让他们在同一文档中共同编辑文件,同时避免了文件的多个版本,确保了团队成员都在同一页面上工作。

一、 用到的技术栈

该项目中使用的主要技术:

  • React:选择 React 的原因是因为 React 是一个功能强大的 JavaScript 库,可以帮助我们构建可重用的 UI 组件。React 不仅易于学习,而且具有出色的性能和可扩展性。此外,React 还具有丰富的社区支持,有大量的第三方库和工具可供选择,可以帮助我们更快地开发应用程序。
  • TypeScript:TypeScript 是由微软开发的开源编程语言,它添加了静态类型和基于类的面向对象编程概念到 JavaScript 中。TypeScript 可以帮助开发者编写更可维护和可读性更强的代码,并提供更好的自动化工具支持。
  • Socket.io:Socket.io 是一个开源 JavaScript 库,它提供了实时通信和协作编辑功能。它可以让开发者轻松构建具有实时功能的应用程序,例如聊天应用程序或多人协作编辑工具。Socket.io 支持多种传输协议和浏览器,并且具有良好的可扩展性和稳定性。

二、项目运行效果

代码仓库地址:forrestyuan/OnlinedocEditing:(github.com)
如何运行项目:

  1. 克隆项目到本地:git clone <代码仓库>

  2. 安装项目依赖:my-app 目录和 server 下:npm install

  3. 进入 server 目录:node index.js

  4. 进入 my-app 目录:yarn start

  5. 在浏览器打开localhost:3000

demo.gif

三、前端实现

1. 创建一个 React 应用

创建一个新的 React 应用,可以使用 create-react-app 脚手架。create-react-app 是一个自动化工具,可以快速搭建一个基于 React 的项目。它提供了一些预设的配置,如 Babel、Webpack 等,使得你可以更加专注于编写代码而不是繁琐的配置。可以选择使用 TypeScript 或者 JavaScript。可以添加一些常用的库,如 React Router、Redux 等,以实现更多功能。

npx create-react-app my-app --template typescript

2. 安装 Socket.io 客户端

使用 yarn 或 npm 安装 Socket.io 客户端。

yarn add socket.io-client

3. 实现 Socket.io 连接

在 React 组件中使用 Socket.io 客户端连接到服务器端。

import io from "socket.io-client";
const socket = io("http://localhost:3333");

4. 实现协作编辑功能

使用 Socket.io 实现实时通信和协作编辑功能,并使用 React 和 ReactQuill 来实现 UI 界面。其中,ReactQuill 是一个富文本编辑器,可以让用户更方便地进行文本编辑。在这个在线文档协作编辑器中,用户可以在同一文档中共同编辑文件。

import React, { useRef, useState, useEffect } from "react";
import io, { Socket } from "socket.io-client";
import "./App.css";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
import quillEmoji from "quill-emoji";
import "quill-emoji/dist/quill-emoji.css";
import { debounce } from "lodash";

const { EmojiBlot, ShortNameEmoji, ToolbarEmoji, TextAreaEmoji } = quillEmoji;
ReactQuill.Quill.register(
  {
    "formats/emoji": EmojiBlot,
    "modules/emoji-shortname": ShortNameEmoji,
    "modules/emoji-toolbar": ToolbarEmoji,
    "modules/emoji-textarea": TextAreaEmoji, //复制粘贴组件
  },
  true
);
// 自定义工具栏
const modules = {
  toolbar: {
    container: [
      [{ size: ["small", false, "large", "huge"] }], //字体设置
      ["bold", "italic", "underline", "strike"],
      [
        { list: "ordered" },
        { list: "bullet" },
        { indent: "-1" },
        { indent: "+1" },
      ],
      ["link", "image"], // a链接和图片的显示
      [{ align: [] }],
      [ { background: ["rgb(  0,   0,   0)"]} ],
      [{ color: ["rgb( 61,  20,  10)"] }],
      ["clean"],
      ["emoji"],
    ],
  },
  "emoji-toolbar": true,
  "emoji-textarea": true,
  "emoji-shortname": true,
};
function App() {
  const [text, setText] = useState("");
  const [users, setUsers] = useState<string[]>([]);
  const [editingUser, setEditingUser] = useState("");
  const [curUser, setCurUser] = useState("");

  const socketRef = useRef<Socket>();

  useEffect(() => {
    socketRef.current = io("<http://localhost:3333>");
    socketRef.current.on("text", (newText: string) => {
      setText(newText);
    });
    socketRef.current.on("users", (newUsers: string[]) => {
      setUsers(newUsers);
    });
    socketRef.current.on("editing", (userId: string) => {
      setEditingUser(userId);
    });
    socketRef.current.on("connectUser", (userId: string) => {
      setCurUser(userId);
    });
    return () => {
      socketRef.current?.disconnect();
    };
  }, []);

  const handleTextChange = debounce((newText: string) => {
    socketRef.current?.emit("editing", curUser);
    socketRef.current?.emit("text", newText);
  }, 500);
  const handleBlur = () => {
    setEditingUser("");
    socketRef.current?.emit("editing", "");
  };

  return (
    <div className="app">
      <div className="header">
        <h1>在线文档协作编辑</h1>
        <div>
          <span>作者:forrest</span>&nbsp;&nbsp;&nbsp;&nbsp;
          <span>
            掘金主页:
            <a href="<https://juejin.cn/user/3421335917699335/posts>">
              <https://juejin.cn/user/3421335917699335/posts>
            </a>
          </span>
        </div>
      </div>
      <div className="container">
        <div className="user-list">
          <p>当前在线用户:</p>
          <ul>
            {users.map((user, index) => (
              <li key={index} className={user === editingUser ? "editing" : ""}>
                {user}
              </li>
            ))}
          </ul>
        </div>
        <div className="editor">
          <ReactQuill
            value={text}
            onChange={handleTextChange}
            onBlur={handleBlur}
            modules={modules}
            placeholder="在这里开始你的远程协作办公吧"
            preserveWhitespace
          />
        </div>
      </div>
    </div>
  );
}
export default App;

在代码中Socket 是用于描述 socket.io-client 库中 socket 实例的类型。如果不存在这个类型,可能是因为没有正确地安装 @types/socket.io-client 类型声明文件。可以通过以下命令安装:

npm install @types/socket.io-client

安装完成后,重新编译项目,应该就可以使用Socket 类型了。

5. 美化页面

然后添加以下样式规则:

.app {
  display: flex;
  flex-direction: column;
}
.header {
  text-align: center;
  margin-bottom: 10px;
  background-color: #333;
  color: white;
}
.header a {
  color: #fff;
  text-decoration: none;
}
.container {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  height: 86vh;
}

.editor {
  width: 80%;
  margin: 0 auto;
  padding: 0 10px 0 0;
  box-sizing: border-box;
}
.editor .ql-editor {
  height: 82vh;
}
.user-list {
  width: 20%;
  margin-right: 2rem;
  border-right: solid #ccc 1px;
  padding-left: 10px;
  box-sizing: border-box;
}

.user-list ul {
  list-style: none;
  padding: 0 10px 0 0;
  height: 79vh;
}

.user-list li {
  margin-bottom: 0.5rem;
  font-size: 14px;
  background-color: #444;
  color: #fff;
  padding: 2px 0 2px 5px;
  border-radius: 10px;
}

.user-list .editing::after {
  content: "(正在编辑)";
  color: yellow;
  font-size: 12px;
  margin-left: 5px;
}

这将为 .container 添加一个居中的样式,并将 .editor.user-list 分别设置为文本编辑器和在线用户列表的容器,并设置用户列表的样式。同时,设置文本编辑器的样式,以适应页面布局。

四、后端实现

1. 创建一个 Node.js 应用

使用 Node.js 创建一个新的应用。新建一个 server 文件夹,cd 到 server 文件夹下面,执行以下命令

npm init -y

然后创建一个 index.js 文件,用于编写服务端代码。

2. 安装 Socket.io 服务端

使用 yarn 或 npm 安装 Socket.io 服务端。

npm install socket.io

3. 实现 Socket.io 连接

在 Node.js 应用中使用 Socket.io 服务端建立连接并监听客户端事件。

http模块创建了一个 httpServer 实例,并使用 socket.io 将其升级为 WebSocket 服务器。创建一个 users 集合用于存储所有已连接的用户的 socket.id。当有用户连接时,将其 socket.id 添加到 users 集合中,并向此用户发送一个 connectUser 事件,并向所有已连接的用户广播一个 users 事件,以更新在线用户列表。

当有用户断开连接时,它会从 users 集合中删除该用户的 socket.id,并向所有已连接的用户广播一个 users 事件,以更新在线用户列表。

const httpServer = require("http").createServer();
const io = require("socket.io")(httpServer, {
  cors: {
    origin: "*",
  },
});

const users = new Set();
let existingText = "";
let editingUser = "";

io.on("connection", (socket) => {
  console.log("a user connected");

  users.add(socket.id);
  socket.emit("connectUser", socket.id);
  socket.emit("text", existingText);
  io.emit("users", Array.from(users));

  socket.on("text", (newText) => {
    existingText = newText;
    io.emit("text", newText);
  });

  socket.on("editing", (userId) => {
    editingUser = userId;
    io.emit("editing", userId);
  });

  socket.on("disconnect", () => {
    console.log("user disconnected");
    users.delete(socket.id);
    if (editingUser === socket.id) {
      editingUser = "";
      io.emit("editing", "");
    }
    io.emit("users", Array.from(users));
  });
});

httpServer.listen(3333, () => {
  console.log("listening on *:3333");
});

socket.emit 是用于发送事件到指定的 socket 对象,只有该 socket 对象会收到该事件,其他任何 socket 对象都不会收到该事件。

io.emit 是用于向所有连接到服务器的 socket 对象广播事件,所有连接到服务器的 socket 对象都会收到该事件。

在实现在线文档协作编辑的场景中,使用 io.emit 可以确保所有连接到服务器的用户都能够收到其他用户所做的更改,从而保证协作效率。

五、待补充功能

当前实现的这个版本功能尚比较简单,要实现类似于飞书那样的在线文档协作,需要考虑更多功能。

  • 实现协作文档的分享功能,可以将文档分享给其他用户或团队成员
  • 实现文档的自动保存功能,避免因意外断电或网络故障导致数据丢失
  • 实现多种编辑模式,例如代码模式、预览模式、全屏模式等
  • 实现协作文档的导出功能,可以将文档导出为多种格式,例如 PDF、DOC、HTML 等
  • 实现文档的标签管理功能,可以为文档添加标签,方便进行分类和检索
  • 实现文档的评论和反馈功能,方便用户之间进行交流和留言
  • 实现多语言支持,使得用户可以选择自己熟悉的语言进行协作编辑

才思如泉涌的你有兴趣完善吗。

六、写在最后

本文介绍了如何使用 React 和 Socket.io 实现在线文档协作编辑。在实现这一功能的过程中,我们还需要注意一些事项。例如,我们需要考虑到不同用户之间的权限和编辑范围,以防止误操作和不必要的冲突。此外,我们还可以添加一些额外的功能,比如历史记录和版本控制,以便于用户进行版本管理和文档追溯。总之,使用 React 和 Socket.io 实现在线文档协作编辑是一种非常实用的技术,可以极大地提高工作效率和协作效果。

- Book Lists -