使用 Node.js、React 和 Websockets 构建看板 📝 ✨
这篇文章是关于什么的?
在本文中,您将学习如何构建与 JIRA、Monday 和 Trello 中相同的看板。我们将使用 React、Socket.io 和 React 精美的 DND 插件,通过美观的拖放功能来实现。用户将能够登录、创建和更新各种任务以及添加评论。
Novu——第一个开源通知基础设施
简单介绍一下我们。Novu 是第一个开源通知基础设施。我们主要负责管理所有产品通知。通知可以是应用内通知(类似于开发者社区的Websockets中的铃铛图标)、电子邮件、短信等等。
如果你能给我们一颗星,我会非常高兴!这会帮助我每周写更多文章🚀
https://github.com/novuhq/novu
我们还会在 Hacktoberfest 期间发送一些很棒的礼品 😇
什么是 Socket.io?
Socket.io是一个流行的 JavaScript 库,它允许我们在 Web 浏览器和 Node.js 服务器之间创建实时的双向通信。它是一个高性能且可靠的库,经过优化,能够以最小的延迟处理大量数据。它遵循 WebSocket 协议,并提供更强大的功能,例如回退到 HTTP 长轮询或自动重新连接,这使我们能够构建高效的实时应用程序。
如何使用 Socket.io 和 React.js 创建实时连接
在这里,我们将为项目设置项目环境。您还将学习如何将 Socket.io 添加到 React 和 Node.js 应用程序中,以及如何通过 Socket.io 连接两个开发服务器以实现实时通信。
创建包含两个名为客户端和服务器的子文件夹的项目文件夹。
mkdir todo-list
cd todo-list
mkdir client server
通过终端导航到客户端文件夹并创建一个新的 React.js 项目。
cd client
npx create-react-app ./
安装 Socket.io 客户端 API 和 React Router。React Router是一个 JavaScript 库,它使我们能够在 React 应用程序中的页面之间导航。
npm install socket.io-client react-router-dom
从 React 应用中删除多余的文件(例如徽标和测试文件),并更新 App.js
文件以显示如下所示的 Hello World。
function App() {
return (
<div>
<p>Hello World!</p>
</div>
);
}
export default App;
导航到服务器文件夹并创建一个 package.json
文件。
cd server & npm init -y
安装 Express.js、CORS、Nodemon 和 Socket.io 服务器 API。
Express.js 是一个快速、简约的框架,它提供了多种用于在 Node.js 中构建 Web 应用程序的功能。CORS 是一个 Node.js包 ,允许不同域之间进行通信。
Nodemon 是一个 Node.js 工具,它在检测到文件更改后会自动重启服务器,而 Socket.io 允许我们在服务器上配置实时连接。
npm install express cors nodemon socket.io
创建一个 index.js
文件——Web 服务器的入口点。
touch index.js
使用 Express.js 设置一个简单的 Node.js 服务器。下面的代码片段会在浏览器中访问时返回一个 JSON 对象 http://localhost:4000/api
。
//👇🏻index.js
const express = require("express");
const app = express();
const PORT = 4000;
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.get("/api", (req, res) => {
res.json({
message: "Hello world",
});
});
app.listen(PORT, () => {
console.log(`Server listening on ${PORT}`);
});
导入 HTTP 和 CORS 库以允许客户端和服务器域之间进行数据传输。
const express = require("express");
const app = express();
const PORT = 4000;
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
//New imports
const http = require("http").Server(app);
const cors = require("cors");
app.use(cors());
app.get("/api", (req, res) => {
res.json({
message: "Hello world",
});
});
http.listen(PORT, () => {
console.log(`Server listening on ${PORT}`);
});
接下来,将 Socket.io 添加到项目中,以创建实时连接。在 app.get()
代码块之前,复制以下代码。
//New imports
.....
const socketIO = require('socket.io')(http, {
cors: {
origin: "http://localhost:3000"
}
});
//Add this before the app.get() block
socketIO.on('connection', (socket) => {
console.log(`⚡: ${socket.id} user just connected!`);
socket.on('disconnect', () => {
socket.disconnect()
console.log('🔥: A user disconnected');
});
});
从上面的代码片段中,该 socket.io("connection")
函数与 React 应用程序建立连接,然后为每个套接字创建一个唯一的 ID,并在用户访问网页时将该 ID 记录到控制台。
当您刷新或关闭网页时,套接字会触发断开事件,表明用户已与套接字断开连接。
通过将启动命令添加到 package.json
文件中的脚本列表中来配置 Nodemon。下面的代码片段使用 Nodemon 启动服务器。
//In server/package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.js"
},
您现在可以使用以下命令通过 Nodemon 运行服务器。
npm start
构建用户界面
在这里,我们将创建应用程序的用户界面。它分为三个页面:登录页面、任务页面(应用程序的核心部分)以及评论页面(用户可以对每个任务进行评论)。
导航到client/src
包含Login.js
、Task.js
和Comments.js
文件的组件文件夹并创建该文件夹。
cd client/src
mkdir components
cd components
touch Login.js Task.js Comments.js
更新App.js
文件以通过 React Router 在不同的路由上呈现新创建的组件。
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Comments from "./components/Comments";
import Task from "./components/Task";
import Login from "./components/Login";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<Login />} />
<Route path='/task' element={<Task />} />
<Route path='/comments/:category/:id' element={<Comments />} />
</Routes>
</BrowserRouter>
);
}
export default App;
导航到src/index.css
文件并复制以下代码。它包含设计此项目所需的所有 CSS。
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap");
* {
font-family: "Space Grotesk", sans-serif;
box-sizing: border-box;
}
a {
text-decoration: none;
}
body {
margin: 0;
padding: 0;
}
.navbar {
width: 100%;
background-color: #f1f7ee;
height: 10vh;
border-bottom: 1px solid #ddd;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
}
.form__input {
min-height: 20vh;
display: flex;
align-items: center;
justify-content: center;
}
.input {
margin: 0 5px;
width: 50%;
padding: 10px 15px;
}
.addTodoBtn {
width: 150px;
padding: 10px;
cursor: pointer;
background-color: #367e18;
color: #fff;
border: none;
outline: none;
height: 43px;
}
.container {
width: 100%;
min-height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
}
.completed__wrapper,
.ongoing__wrapper,
.pending__wrapper {
width: 32%;
min-height: 60vh;
display: flex;
flex-direction: column;
padding: 5px;
}
.ongoing__wrapper > h3,
.pending__wrapper > h3,
.completed__wrapper > h3 {
text-align: center;
text-transform: capitalize;
}
.pending__items {
background-color: #eee3cb;
}
.ongoing__items {
background-color: #d2daff;
}
.completed__items {
background-color: #7fb77e;
}
.pending__container,
.ongoing__container,
.completed__container {
width: 100%;
min-height: 55vh;
display: flex;
flex-direction: column;
padding: 5px;
border: 1px solid #ddd;
border-radius: 5px;
}
.pending__items,
.ongoing__items,
.completed__items {
width: 100%;
border-radius: 5px;
margin-bottom: 10px;
padding: 15px;
}
.comment {
text-align: right;
font-size: 14px;
cursor: pointer;
color: rgb(85, 85, 199);
}
.comment:hover {
text-decoration: underline;
}
.comments__container {
padding: 20px;
}
.comment__form {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-bottom: 30px;
}
.comment__form > label {
margin-bottom: 15px;
}
.comment__form textarea {
width: 80%;
padding: 15px;
margin-bottom: 15px;
}
.commentBtn {
padding: 10px;
width: 200px;
background-color: #367e18;
outline: none;
border: none;
color: #fff;
height: 45px;
cursor: pointer;
}
.comments__section {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.login__form {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.login__form > label {
margin-bottom: 15px;
}
.login__form > input {
width: 70%;
padding: 10px 15px;
margin-bottom: 15px;
}
.login__form > button {
background-color: #367e18;
color: #fff;
padding: 15px;
cursor: pointer;
border: none;
font-size: 16px;
outline: none;
width: 200px;
}
登录页面
在这里,应用程序接受用户名并将其保存在本地存储中以供识别。
更新Login.js
文件如下:
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
const Login = () => {
const [username, setUsername] = useState("");
const navigate = useNavigate();
const handleLogin = (e) => {
e.preventDefault();
//👇🏻 saves the username to localstorage
localStorage.setItem("userId", username);
setUsername("");
//👇🏻 redirects to the Tasks page.
navigate("/tasks");
};
return (
<div className='login__container'>
<form className='login__form' onSubmit={handleLogin}>
<label htmlFor='username'>Provide a username</label>
<input
type='text'
name='username'
id='username'
required
onChange={(e) => setUsername(e.target.value)}
value={username}
/>
<button>SIGN IN</button>
</form>
</div>
);
};
export default Login;
任务页面
现在,我将指导您创建“任务”页面的网页布局。下图展示了该页面的布局。
将布局分为三个部分,即:Nav.js
-AddTask.js
表单输入部分,以及TasksContainer.js
- 包含任务。
cd src/components
touch Nav.js AddTask.js TasksContainer.js
渲染文件内的组件Task.js
。
import React from "react";
import AddTask from "./AddTask";
import TasksContainer from "./TasksContainer";
import Nav from "./Nav";
import socketIO from "socket.io-client";
/*
👇🏻 Pass Socket.io into the required components
where communications are made with the server
*/
const socket = socketIO.connect("http://localhost:4000");
const Task = () => {
return (
<div>
<Nav />
<AddTask socket={socket} />
<TasksContainer socket={socket} />
</div>
);
};
export default Task;
将下面的代码复制到Nav.js
文件中。
import React from "react";
const Nav = () => {
return (
<nav className='navbar'>
<h3>Team's todo list</h3>
</nav>
);
};
export default Nav;
更新AddTask.js
文件如下:
import React, { useState } from "react";
const AddTask = ({ socket }) => {
const [task, setTask] = useState("");
const handleAddTodo = (e) => {
e.preventDefault();
//👇🏻 Logs the task to the console
console.log({ task });
setTask("");
};
return (
<form className='form__input' onSubmit={handleAddTodo}>
<label htmlFor='task'>Add Todo</label>
<input
type='text'
name='task'
id='task'
value={task}
className='input'
required
onChange={(e) => setTask(e.target.value)}
/>
<button className='addTodoBtn'>ADD TODO</button>
</form>
);
};
export default AddTask;
将以下代码复制到TasksContainer.js
文件中。它将渲染三个父元素,分别对应待处理、正在进行和已完成的任务。
import React from "react";
import { Link } from "react-router-dom";
const TasksContainer = ({ socket }) => {
return (
<div className='container'>
<div className='pending__wrapper'>
<h3>Pending Tasks</h3>
<div className='pending__container'>
<div className='pending__items'>
<p>Debug the Notification center</p>
<p className='comment'>
<Link to='/comments'>2 Comments</Link>
</p>
</div>
</div>
</div>
<div className='ongoing__wrapper'>
<h3>Ongoing Tasks</h3>
<div className='ongoing__container'>
<div className='ongoing__items'>
<p>Create designs for Novu</p>
<p className='comment'>
<Link to='/comments'>Add Comment</Link>
</p>
</div>
</div>
</div>
<div className='completed__wrapper'>
<h3>Completed Tasks</h3>
<div className='completed__container'>
<div className='completed__items'>
<p>Debug the Notification center</p>
<p className='comment'>
<Link to='/comments'>2 Comments</Link>
</p>
</div>
</div>
</div>
</div>
);
};
export default TasksContainer;
恭喜!💃🏻 布局已设置完毕。接下来,让我们为评论页面创建一个简单的模板。
评论页面
将以下代码复制到Comments.js
文件中。它会将评论和用户名记录到控制台。
import React, { useEffect, useState } from "react";
import socketIO from "socket.io-client";
import { useParams } from "react-router-dom";
const socket = socketIO.connect("http://localhost:4000");
const Comments = () => {
const [comment, setComment] = useState("");
const addComment = (e) => {
e.preventDefault();
console.log({
comment,
userId: localStorage.getItem("userId"),
});
setComment("");
};
return (
<div className='comments__container'>
<form className='comment__form' onSubmit={addComment}>
<label htmlFor='comment'>Add a comment</label>
<textarea
placeholder='Type your comment...'
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={5}
id='comment'
name='comment'
required
></textarea>
<button className='commentBtn'>ADD COMMENT</button>
</form>
<div className='comments__section'>
<h2>Existing Comments</h2>
<div></div>
</div>
</div>
);
};
export default Comments;
用户界面现已完成。接下来,让我们将 React Beautiful DND 添加到应用程序中,以启用拖放功能。
如何使用 React Beautiful DND 添加拖放功能
在这里,您将学习如何使用 React Beautiful DND 添加拖放功能并在 React 应用程序和 Socket.io Node.js 服务器之间进行通信。
React Beautiful DND 如何工作?
React Beautiful DND 是一个高性能库,它允许我们选择一个项目并将其从当前位置拖动到页面上的另一个位置。
上图解释了如何设置 React Beautiful DND。你必须将所有可拖放项目包裹在 组件中<DragDropContext/>
。<Droppable/>
组件会保存放置在 组件内的可拖放项目<Draggable/>
。
使用 React Beautiful DND 使任务可拖放
在这里,您将学习如何将 React Beautiful DND 添加到 React 应用程序并使任务从一个类别移动到另一个类别(待定、正在进行和已完成)。
安装 React Beautiful DND 并确保您没有在严格模式下使用 React。(检查src/index.js
)。
npm install react-beautiful-dnd
打开server/index.js
文件并创建一个包含每个任务类别的所有虚拟数据的对象。
//👇🏻 server/index.js
//👇🏻 Generates a random string
const fetchID = () => Math.random().toString(36).substring(2, 10);
//👇🏻 Nested object
let tasks = {
pending: {
title: "pending",
items: [
{
id: fetchID(),
title: "Send the Figma file to Dima",
comments: [],
},
],
},
ongoing: {
title: "ongoing",
items: [
{
id: fetchID(),
title: "Review GitHub issues",
comments: [
{
name: "David",
text: "Ensure you review before merging",
id: fetchID(),
},
],
},
],
},
completed: {
title: "completed",
items: [
{
id: fetchID(),
title: "Create technical contents",
comments: [
{
name: "Dima",
text: "Make sure you check the requirements",
id: fetchID(),
},
],
},
],
},
};
//👇🏻 host the tasks object via the /api route
app.get("/api", (req, res) => {
res.json(tasks);
});
接下来,获取文件中的任务TasksContainer.js
。下面的代码片段在渲染组件之前将任务对象转换为数组。
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
const TasksContainer = () => {
const [tasks, setTasks] = useState({});
useEffect(() => {
function fetchTasks() {
fetch("http://localhost:4000/api")
.then((res) => res.json())
.then((data) => {
console.log(data);
setTasks(data);
});
}
fetchTasks();
}, []);
return (
<div className='container'>
{/*
👇🏻 Returns an array of each tasks (Uncomment to view the data structure)
{Object.entries(tasks).map((task) => console.log(task))} */}
{Object.entries(tasks).map((task) => (
<div
className={`${task[1].title.toLowerCase()}__wrapper`}
key={task[1].title}
>
<h3>{task[1].title} Tasks</h3>
<div className={`${task[1].title.toLowerCase()}__container`}>
{task[1].items.map((item, index) => (
<div
className={`${task[1].title.toLowerCase()}__items`}
key={item.id}
>
<p>{item.title}</p>
<p className='comment'>
<Link to='/comments'>
{item.comments.length > 0 ? `View Comments` : "Add Comment"}
</Link>
</p>
</div>
))}
</div>
</div>
))}
</div>
);
};
export default TasksContainer;
将“react-beautiful-dnd”所需的组件导入到TasksContainer.js
文件中。
//👇🏻 At the top of the TasksContainer.js file
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
更新TaskContainer.js
文件如下:
return (
<div className='container'>
{/** --- 👇🏻 DragDropContext ---- */}
<DragDropContext onDragEnd={handleDragEnd}>
{Object.entries(tasks).map((task) => (
<div
className={`${task[1].title.toLowerCase()}__wrapper`}
key={task[1].title}
>
<h3>{task[1].title} Tasks</h3>
<div className={`${task[1].title.toLowerCase()}__container`}>
{/** --- 👇🏻 Droppable --- */}
<Droppable droppableId={task[1].title}>
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{task[1].items.map((item, index) => (
{/** --- 👇🏻 Draggable --- */}
<Draggable
key={item.id}
draggableId={item.id}
index={index}
>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={`${task[1].title.toLowerCase()}__items`}
>
<p>{item.title}</p>
<p className='comment'>
<Link to={`/comments/${task[1].title}/${item.id}`}>
{item.comments.length > 0
? `View Comments`
: "Add Comment"}
</Link>
</p>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</div>
</div>
))}
</DragDropContext>
</div>
);
- 从上面的代码片段来看:
- 包装
DragDropContext
整个拖放容器, Droppable表示可拖动元素 的父元素 。 - Draggable 和 Droppable 组件接受一个可拖放的 ID。它们也接受一个子元素,
provided
这样我们就可以引用每个元素并将其渲染为可拖放的项目。 - 请随意将代码分成不同的组件,并单击 此处 了解有关 React Beautiful DND 的更多信息。
- 包装
接受DragDropContext
一个 prop onDragEnd
,在拖动元素后立即触发。
//👇🏻 This function is the value of the onDragEnd prop
const handleDragEnd = ({ destination, source }) => {
if (!destination) return;
if (
destination.index === source.index &&
destination.droppableId === source.droppableId
)
return;
socket.emit("taskDragged", {
source,
destination,
});
};
上面的代码片段接受拖动项目的目标和源,检查它是否被拖动到可放置的目的地,以及在通过 Socket.io 向 Node.js 服务器发送消息之前源和目的地是否不同。
taskDragged
在后端创建该事件的监听器。
socketIO.on("connection", (socket) => {
console.log(`⚡: ${socket.id} user just connected!`);
socket.on("taskDragged", (data) => {
console.log(data);
});
socket.on("disconnect", () => {
socket.disconnect();
console.log("🔥: A user disconnected");
});
});
让我们简单检查一下拖动项目后返回的数据:
下面的代码片段显示该项目从“待处理”类别移至“进行中”类别。索引也从 0 更改为 1。
{
source: { index: 0, droppableId: 'pending' },
destination: { droppableId: 'ongoing', index: 1 }
}
接下来,让拖拽元素停留在其目标位置。更新taskDragged
监听器如下:
socket.on("taskDragged", (data) => {
const { source, destination } = data;
//👇🏻 Gets the item that was dragged
const itemMoved = {
...tasks[source.droppableId].items[source.index],
};
console.log("DraggedItem>>> ", itemMoved);
//👇🏻 Removes the item from the its source
tasks[source.droppableId].items.splice(source.index, 1);
//👇🏻 Add the item to its destination using its destination index
tasks[destination.droppableId].items.splice(destination.index, 0, itemMoved);
//👇🏻 Sends the updated tasks object to the React app
socket.emit("tasks", tasks);
/* 👇🏻 Print the items at the Source and Destination
console.log("Source >>>", tasks[source.droppableId].items);
console.log("Destination >>>", tasks[destination.droppableId].items);
*/
});
tasks
为组件内的事件创建一个监听器TasksContainer
。
useEffect(() => {
socket.on("tasks", (data) => setTasks(data));
}, [socket]);
恭喜!🎉 您现在可以将项目从一个类别拖放到另一个类别。
如何创建新任务
在本节中,我将指导您从 React 应用程序创建新任务。
更新AddTask.js
文件以将新任务发送到后端服务器。
import React, { useState } from "react";
const AddTask = ({ socket }) => {
const [task, setTask] = useState("");
const handleAddTodo = (e) => {
e.preventDefault();
//👇🏻 sends the task to the Socket.io server
socket.emit("createTask", { task });
setTask("");
};
return (
<form className='form__input' onSubmit={handleAddTodo}>
<label htmlFor='task'>Add Todo</label>
<input
type='text'
name='task'
id='task'
value={task}
className='input'
required
onChange={(e) => setTask(e.target.value)}
/>
<button className='addTodoBtn'>ADD TODO</button>
</form>
);
};
export default AddTask;
createTask
在后端服务器上为该事件创建一个监听器,并将该项目添加到tasks
对象中。
socketIO.on("connection", (socket) => {
console.log(`⚡: ${socket.id} user just connected!`);
socket.on("createTask", (data) => {
// 👇🏻 Constructs an object according to the data structure
const newTask = { id: fetchID(), title: data.task, comments: [] };
// 👇🏻 Adds the task to the pending category
tasks["pending"].items.push(newTask);
/*
👇🏻 Fires the tasks event for update
*/
socket.emit("tasks", tasks);
});
//...other listeners
});
完成评论页面
在本节中,您将学习如何添加和检索每个任务的评论。
更新Comments.js
文件如下:
import React, { useEffect, useState } from "react";
import socketIO from "socket.io-client";
import { useParams } from "react-router-dom";
const socket = socketIO.connect("http://localhost:4000");
const Comments = () => {
const { category, id } = useParams();
const [comment, setComment] = useState("");
const addComment = (e) => {
e.preventDefault();
/*
👇🏻 sends the comment, the task category, item's id and the userID.
*/
socket.emit("addComment", {
comment,
category,
id,
userId: localStorage.getItem("userId"),
});
setComment("");
};
return (
<div className='comments__container'>
<form className='comment__form' onSubmit={addComment}>
<label htmlFor='comment'>Add a comment</label>
<textarea
placeholder='Type your comment...'
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={5}
id='comment'
name='comment'
required
></textarea>
<button className='commentBtn'>ADD COMMENT</button>
</form>
<div className='comments__section'>
<h2>Existing Comments</h2>
<div></div>
</div>
</div>
);
};
export default Comments;
回想一下,评论页面的路线是/comments/:category/:id
;上面的代码片段从页面的 URL 中检索项目的类别及其 ID,然后将项目的类别、ID、用户 ID 和评论发送到 Node.js 服务器。
接下来,在 Node.js 服务器上创建一个事件监听器,通过其 ID 将评论添加到特定任务。
socket.on("addComment", (data) => {
const { category, userId, comment, id } = data;
//👇🏻 Gets the items in the task's category
const taskItems = tasks[category].items;
//👇🏻 Loops through the list of items to find a matching ID
for (let i = 0; i < taskItems.length; i++) {
if (taskItems[i].id === id) {
//👇🏻 Then adds the comment to the list of comments under the item (task)
taskItems[i].comments.push({
name: userId,
text: comment,
id: fetchID(),
});
//👇🏻 sends a new event to the React app
socket.emit("comments", taskItems[i].comments);
}
}
});
通过 Socket.io 获取评论。
const Comments = () => {
const { category, id } = useParams();
const [comment, setComment] = useState("");
const [commentList, setCommentList] = useState([]);
//👇🏻 Listens to the comments event
useEffect(() => {
socket.on("comments", (data) => setCommentList(data));
}, []);
//...other listeners
return (
<div className='comments__container'>
<form className='comment__form' onSubmit={addComment}>
...
</form>
{/** 👇🏻 Displays all the available comments*/}
<div className='comments__section'>
<h2>Existing Comments</h2>
{commentList.map((comment) => (
<div key={comment.id}>
<p>
<span style={{ fontWeight: "bold" }}>{comment.text} </span>by{" "}
{comment.name}
</p>
</div>
))}
</div>
</div>
);
};
export default Comments;
最后,添加此useEffect
钩子以在页面加载到浏览器时获取评论。
useEffect(() => {
socket.emit("fetchComments", { category, id });
}, [category, id]);
监听后端的事件,触发comments事件,返回符合item ID和分类的评论列表。
socket.on("fetchComments", (data) => {
const { category, id } = data;
const taskItems = tasks[category].items;
for (let i = 0; i < taskItems.length; i++) {
if (taskItems[i].id === id) {
socket.emit("comments", taskItems[i].comments);
}
}
});
恭喜!💃🏻我们已经完成了这个项目。
额外内容:使用 Novu 发送通知
如果您想在用户添加评论或新任务时向应用程序添加通知,您可以使用Nav.js
组件中的 Novu 轻松实现。
Novu 允许您添加各种通知类型,例如电子邮件、短信和应用内通知。
如何将 Novu 添加到 React 和 Node.js 应用程序
要添加应用内通知,请在服务器上安装 Novu Node.js SDK 并在 React 应用程序中安装通知中心。
👇🏻 Install on the client
npm install @novu/notification-center
👇🏻 Install on the server
npm install @novu/node
运行以下代码创建一个 Novu 项目。您可以使用个性化仪表板。
👇🏻 Install on the client
npx novu init
在创建 Novu 项目之前,您需要使用 Github 登录。以下代码片段包含运行后应遵循的步骤 npx novu init
Now let's setup your account and send your first notification
❓ What is your application name? Devto Clone
❓ Now lets setup your environment. How would you like to proceed?
> Create a free cloud account (Recommended)
❓ Create your account with:
> Sign-in with GitHub
❓ I accept the Terms and Condidtions (https://novu.co/terms) and have read the Privacy Policy (https://novu.co/privacy)
> Yes
✔️ Create your account successfully.
We've created a demo web page for you to see novu notifications in action.
Visit: http://localhost:57807/demo to continue
访问演示网页 http://localhost:57807/demo
,复制您的订阅者 ID,然后点击“跳过教程”按钮。我们将在本教程的后续部分使用它。
更新 文件以包含 Novu 及其来自文档components/Nav.js
的应用内通知所需的元素 。
import React from "react";
import {
NovuProvider,
PopoverNotificationCenter,
NotificationBell,
} from "@novu/notification-center";
import { useNavigate } from "react-router-dom";
const Nav = () => {
const navigate = useNavigate();
const onNotificationClick = (notification) =>
navigate(notification.cta.data.url);
return (
<nav className='navbar'>
<h3>Team's todo list</h3>
<div>
<NovuProvider
subscriberId='<SUBSCRIBER_ID>'
applicationIdentifier='<APP_ID>'
>
<PopoverNotificationCenter
onNotificationClick={onNotificationClick}
colorScheme='light'
>
{({ unseenCount }) => (
<NotificationBell unseenCount={unseenCount} />
)}
</PopoverNotificationCenter>
</NovuProvider>
</div>
</nav>
);
};
export default Nav;
上面的代码片段将 Novu 通知铃图标添加到 Nav 组件,使我们能够查看来自应用程序的所有通知。
💡 该 NovuProvider
组件需要您先前复制的订阅者 ID 以及Novu 管理平台http://localhost:57807/demo
API 密钥下的设置部分中提供的应用程序 ID 。
接下来,让我们为应用程序创建工作流程,它描述了您想要添加到应用程序的功能。
从“开发”侧边栏中选择“通知”,然后创建一个通知模板。选择新创建的模板,点击“工作流编辑器”,并确保工作流如下:
从上图可以看出,Novu 在发送应用内通知之前会触发摘要引擎。
Novu Digest 允许我们控制应用程序内通知的发送方式。它会收集多个触发事件,并将它们作为一条消息发送。上图显示每 2 分钟发送一次通知,当您拥有大量用户且更新频繁时,这种方法非常有效。
单击该In-App
步骤并编辑通知模板以包含以下内容。
{{userId}} added a new task.
💡 Novu 允许您使用Handlebars 模板引擎向模板添加动态内容或数据 。用户名变量的数据将作为请求的有效负载插入到模板中。
单击按钮保存模板 Update
并返回代码编辑器。
将 Novu 添加到应用程序
从包中导入 Novu 并使用服务器上的 API 密钥创建实例。
//server/index.js
const { Novu } = require("@novu/node");
const novu = new Novu("<YOUR_API_KEY>");
创建一个通过 Novu 向 React 应用程序发送通知的函数。
const sendNotification = async (user) => {
try {
const result = await novu.trigger(<TEMPLATE_ID>, {
to: {
subscriberId: <SUBSCRIBER_ID>,
},
payload: {
userId: user,
},
});
console.log(result);
} catch (err) {
console.error("Error >>>>", { err });
}
};
//👇🏻 The function is called after a new task is created
socket.on("createTask", (data) => {
const newTask = { id: fetchID(), title: data.task, comments: [] };
tasks["pending"].items.push(newTask);
socket.emit("tasks", tasks);
//👇🏻 Triggers the notification via Novu
sendNotification(data.userId);
});
当新任务添加到应用程序时,上面的代码片段会向所有用户发送通知。
结论
到目前为止,您已经学习了如何在 React 和 Node.js 应用程序中设置 Socket.io,通过 Socket.io 在服务器和客户端之间进行通信,以及如何使用 React Beautiful DND 拖放项目。
这是一个使用 Socket.io 和 React Beautiful DND 构建的演示。您可以随意改进此应用,例如添加身份验证、将任务分配给特定用户的功能以及在用户发表评论时添加通知。
本教程的源代码可以在这里找到:https://github.com/novuhq/blog/tree/main/react-beautiful-dnd-todo-list
感谢您的阅读!
PS: Novu 将在 Hacktoberfest 上送出超棒的礼品!如果您能给我们一颗星,我们将非常高兴!⭐️
https://github.com/novuhq/novu
文章来源:https://dev.to/novu/building-a-beautiful-kanban-board-with-nodejs-react-and-websockets-39dk