使用 ReactJS、Socket.io 和 NodeJS 构建协作白板🤝
各位读者好!希望你们一切顺利。在本文中,我们将构建一个类似于 Google Meet 白板的协作白板。我们将使用HTML Canvas和ReactJS创建白板,并使用socket.io实现实时通信。
项目实施后的样子如下。我们开始吧。
在项目目录中,我们将创建文件夹,并使用命令创建前端npm create vite@latest
并选择“react”和“typescript”。
创建画布
在前端创建一个 Board 组件。该组件将包含我们的 Canvas 及其功能。
import React, { useRef } from 'react';
const Board = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
return (
<canvas
ref={canvasRef}
width={600}
height={400}
style={{ backgroundColor: 'white' }}
/>
);
};
export default Board;
在这里,我们使用useRef hook(现在称为canvasRef )创建画布当前状态的引用。我们将背景颜色保持为白色,以创建白板环境,它可以是任何您想要的颜色,也可以完全没有颜色。
在画布上绘画
现在,我们创建了画布,让我们看看如何在其上绘图/书写。
useEffect(() => {
// Variables to store drawing state
let isDrawing = false;
let lastX = 0;
let lastY = 0;
const startDrawing = (e: { offsetX: number; offsetY: number; }) => {
isDrawing = true;
[lastX, lastY] = [e.offsetX, e.offsetY];
};
// Function to draw
const draw = (e: { offsetX: number; offsetY: number; }) => {
if (!isDrawing) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
}
[lastX, lastY] = [e.offsetX, e.offsetY];
};
// Function to end drawing
const endDrawing = () => {
isDrawing = false;
};
const canvas: HTMLCanvasElement | null = canvasRef.current;
const ctx = canvasRef.current?.getContext('2d');
// Set initial drawing styles
if (ctx) {
ctx.strokeStyle = black;
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
}
// Event listeners for drawing
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', endDrawing);
canvas.addEventListener('mouseout', endDrawing);
return () => {
// Clean up event listeners when component unmounts
canvas.removeEventListener('mousedown', startDrawing);
canvas.removeEventListener('mousemove', draw);
canvas.removeEventListener('mouseup', endDrawing);
canvas.removeEventListener('mouseout', endDrawing);
};
}, []);
我们使用 JavaScript 事件监听器来管理画布上的各种鼠标事件。鼠标右键单击画布后,立即执行startDrawing()函数;鼠标移动后,将调用draw()函数;鼠标离开画布后,将调用endDrawing()函数。让我们详细了解一下。
我们使用该ctx
变量获取当前画布引用并对其执行操作。我们保留了ctx变量的一些默认属性,例如strokeStyle为黑色、lineWidth为 5、lineCap为圆角、lineJoin为圆形。
我们在 Canvas 上设置了事件监听器,并在组件卸载或在下一个效果执行前运行后清理事件监听器。我们使用isDrawing
、lastX
和lastY
变量来控制绘制状态。最初, isDrawing 设置为 false,直到鼠标按下。它将isDrawing的状态更改为true
。当鼠标移动时,该ctx
变量会根据光标在画布上的位置创建笔触。当我们停止右键单击或将光标移出画布时,将调用endDrawing()函数,将isDrawing设置为 false。这样,我们就可以在画布上自由绘图了 :)
后端
现在,让我们添加允许多个用户同时使用白板的功能。进入后端目录,首先执行命令,然后在index.jsnpm install socket.io
文件中编写以下内容。
const { Server } = require('socket.io');
const io = new Server({
cors: "http://localhost:5173/"
})
io.on('connection', function (socket) {
socket.on('canvasImage', (data) => {
socket.broadcast.emit('canvasImage', data);
});
});
io.listen(5000);
这里,我们初始化了一个名为 io 的新 Socket.IO 服务器实例。cors 选项设置为允许来自指定 URL(http://localhost:5173/)的连接,该 URL 是我们的前端 URL。如果不设置该选项,前端将无法与后端共享资源。
io.on(‘connection’)
每当客户端连接到 Socket.IO 服务器时,都会设置一个事件监听器。在这个监听器中,我们还有另一个事件监听器,用于监听用户从前端发出的“canvasImage”事件。当
用户从前端发出事件时,该事件将包含画布的最新副本。我们将使用此事件接收数据,然后将接收到的canvasImagesocket.broadcast.emit(‘canvasImage’, data)
数据广播给所有连接的客户端(发送原始“canvasImage”事件的客户端除外)。最后,同样重要的是,我们使用在端口5000上运行的命令启动服务器。node index.js
客户端套接字连接
在Board.tsx中,我们现在将进行一些更改。在此之前,我们将在前端目录中安装“socket.io-client”包。
import React, { useRef, useEffect, useState } from 'react';
import io from 'socket.io-client';
const [socket, setSocket] = useState(null);
useEffect(() => {
const newSocket = io('http://localhost:5000');
console.log(newSocket, "Connected to socket");
setSocket(newSocket);
}, []);
useEffect(() => {
if (socket) {
// Event listener for receiving canvas data from the socket
socket.on('canvasImage', (data) => {
// Create an image object from the data URL
const image = new Image();
image.src = data;
const canvas = canvasRef.current;
// eslint-disable-next-line react-hooks/exhaustive-deps
const ctx = canvas.getContext('2d');
// Draw the image onto the canvas
image.onload = () => {
ctx.drawImage(image, 0, 0);
};
});
}
}, [socket]);
我们将创建一个名为 socket 的新 useState 钩子来管理套接字。const newSocket = io('http://localhost:5000')
与服务器和客户端建立连接。
为了获取所有使用白板的用户的更新,我们使用 useEffect 钩子监听来自服务器的“canvasImage”事件。一旦收到事件,我们就会在当前画布上绘制该图像。
那么,我们每次绘制时都要将图像发送到服务器吗?不,我们在绘制时(例如,当我们抬起右键时)才将图像发送到服务器。以下是更新后的endDrawing()函数。
const endDrawing = () => {
const canvas = canvasRef.current;
const dataURL = canvas.toDataURL(); // Get the data URL of the canvas content
// Send the dataURL or image data to the socket
// console.log('drawing ended')
if (socket) {
socket.emit('canvasImage', dataURL);
console.log('drawing ended')
}
isDrawing = false;
};
简单!这样,协作白板就完成了。不过,如果你想要调整画笔的颜色和大小,请稍等,我们接下来会讲到。
调整颜色和大小
我们将使用两种状态来控制这两个画笔属性。在App.tsx中,我们将对输入使用onChange()函数来更新每个状态。
import { useEffect, useState } from 'react';
import './App.css';
import Board from './component/Board';
const CanvasDrawing = () => {
const [brushColor, setBrushColor] = useState('black');
const [brushSize, setBrushSize] = useState<number>(5);
return (
<div className="App" >
<h1>Collaborative Whiteboard</h1>
<div>
<Board brushColor={brushColor} brushSize={brushSize} />
<div className='tools' >
<div>
<span>Color: </span>
<input type="color" value={brushColor} onChange={(e) => setBrushColor(e.target.value)} />
</div>
<div>
<span>Size: </span>
<input type="range" color='#fac176'
min="1" max="100" value={brushSize} onChange={(e) => setBrushSize(Number(e.target.value))} />
<span>{brushSize}</span>
</div>
</div>
</div>
</div>
);
};
export default CanvasDrawing;
让我们在 App.css 中添加一些 CSS,使其看起来更漂亮。
.App {
background-color: #4158D0;
background-image: linear-gradient(43deg, #4158D0 0%, #C850C0 46%, #FFCC70 100%);
height: 100vh;
width: 100%;
padding: 3em 1em;
display: flex;
flex-direction: column;
align-items: center;
}
.tools {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
width: 100%;
padding: 1em;
background-color: black;
color: white;
}
h1 {
margin-bottom: 1rem;
text-align: center;
}
input {
margin: 0 5px;
}
span {
font-size: 1.3em;
}
@media screen and (max-width: 600px) {
.tools {
flex-direction: column;
align-items: start;
}
}
索引.css
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Roboto', sans-serif;
}
我们将brushSize和brushColor状态作为道具传递给 Board 组件。
interface MyBoard {
brushColor: string;
brushSize: number;
}
const Board: React.FC<MyBoard> = (props) => {
const { brushColor, brushSize } = props;
useEffect(() => {
// Variables to store drawing state
let isDrawing = false;
let lastX = 0;
let lastY = 0;
const startDrawing = (e: { offsetX: number; offsetY: number; }) => {
isDrawing = true;
console.log(`drawing started`, brushColor, brushSize);
[lastX, lastY] = [e.offsetX, e.offsetY];
};
// Function to draw
const draw = (e: { offsetX: number; offsetY: number; }) => {
if (!isDrawing) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
}
[lastX, lastY] = [e.offsetX, e.offsetY];
};
// Function to end drawing
const endDrawing = () => {
const canvas = canvasRef.current;
const dataURL = canvas.toDataURL(); // Get the data URL of the canvas content
// Send the dataURL or image data to the socket
// console.log('drawing ended')
if (socket) {
socket.emit('canvasImage', dataURL);
console.log('drawing ended')
}
isDrawing = false;
};
const canvas: HTMLCanvasElement | null = canvasRef.current;
const ctx = canvasRef.current?.getContext('2d');
// Set initial drawing styles
if (ctx) {
ctx.strokeStyle = brushColor;
ctx.lineWidth = brushSize;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
}
// Event listeners for drawing
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', endDrawing);
canvas.addEventListener('mouseout', endDrawing);
return () => {
// Clean up event listeners when component unmounts
canvas.removeEventListener('mousedown', startDrawing);
canvas.removeEventListener('mousemove', draw);
canvas.removeEventListener('mouseup', endDrawing);
canvas.removeEventListener('mouseout', endDrawing);
};
}, [brushColor, brushSize, socket]);
在 useEffect 中,我们正在进行更改ctx.strokeStyle = brushColor;
并ctx.lineWidth = brushSize;
在依赖数组中添加 brushColor、brushSize、socket。
响应式画布
我们将使用一些 JavaScript 代码来实现画布的响应式显示。window 对象会获取设备的宽度和高度,并据此显示画布。
const [windowSize, setWindowSize] = useState([
window.innerWidth,
window.innerHeight,
]);
useEffect(() => {
const handleWindowResize = () => {
setWindowSize([window.innerWidth, window.innerHeight]);
};
window.addEventListener('resize', handleWindowResize);
return () => {
window.removeEventListener('resize', handleWindowResize);
};
}, []);
return (
<canvas
ref={canvasRef}
width={windowSize[0] > 600 ? 600 : 300}
height={windowSize[1] > 400 ? 400 : 200}
style={{ backgroundColor: 'white' }}
/>
);
我们使用“resize”事件监听器来更新 windowSize 的宽高状态。至此,我们的 Web 应用就完成了。
希望你喜欢这个项目。
感谢阅读!
如果你有任何疑问或者想查看源代码,可以在这里查看。
与我联系-
鏂囩珷鏉ユ簮锛�https://dev.to/fidalmathew/building-a-collaborative-whiteboard-app-using-reactjs-socketio-and-nodejs-2o71