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

许久之前写过写一篇基于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

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

三、前端实现

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实现在线文档协作编辑是一种非常实用的技术,可以极大地提高工作效率和协作效果。

原文链接:https://juejin.cn/post/7218109174085173306 作者:forrest酱

(0)
上一篇 2023年4月5日 上午10:05
下一篇 2023年4月5日 上午10:15

相关推荐

发表评论

登录后才能评论