基于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)
如何运行项目:
-
克隆项目到本地:
git clone <代码仓库>
-
安装项目依赖:my-app 目录和 server 下:
npm install
-
进入 server 目录:
node index.js
-
进入 my-app 目录:
yarn start
-
在浏览器打开
localhost:3000
三、前端实现
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>
<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 实现在线文档协作编辑是一种非常实用的技术,可以极大地提高工作效率和协作效果。