使用 Socket.io 和 React 构建类似 Notion 的系统
这篇文章是关于什么的?
我们将构建一个类似 Click-Up 和 Notion 的知识系统。您将能够添加帖子、发表评论、标记其他用户并将其显示在他们的通知中。
在 Notion 中,用户可以实时查看其他用户的活动,而无需刷新页面。这就是我们将使用 Socket.io 的原因。下一篇文章我将介绍 SSE(服务器发送事件)。
什么是 WebSocket?
WebSocket 是一个内置的 Node.js 模块,它使我们能够在客户端和服务器之间建立实时连接,从而允许它们双向发送数据。然而,WebSocket 是底层的,不提供构建复杂的实时应用程序所需的功能;这就是 Socket.io 存在的原因。
Socket.io 是一个流行的 JavaScript 库,它允许我们在软件应用程序和 Node.js 服务器之间创建实时双向通信。它经过优化,能够以最小的延迟处理大量数据,并提供更强大的功能,例如回退到 HTTP 长轮询或自动重新连接。
Novu——第一个开源通知基础设施
简单介绍一下我们。Novu 是第一个开源通知基础设施。我们主要负责管理所有产品通知。通知可以是应用内通知(类似于开发者社区的Websockets中的铃铛图标)、电子邮件、短信等等。
如果您能通过为该库加注星标来帮助我们,我将不胜感激🤩
https://github.com/novuhq/novu
如何使用 React 和 Socket.io 创建实时连接
在这里,我们将为 Notion 应用设置项目环境。您还将学习如何将 Socket.io 添加到 React 和 Node.js 应用程序中,以及如何通过Socket.io连接两个开发服务器进行实时通信。
创建包含两个名为客户端和服务器的子文件夹的项目文件夹。
mkdir notion-platform
cd notion-platform
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;
将 Socket.io 客户端 API 添加到 React 应用程序,如下所示:
import { io } from "socket.io-client";
//👇🏻 http://localhost:4000 is where the server host URL.
const socket = io.connect("http://localhost:4000");
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。
npm install express cors nodemon socket.io
Express.js 是一个快速、简约的框架,它提供了多种用于在 Node.js 中构建 Web 应用程序的功能。CORS 是一个 Node.js包 ,允许不同域之间进行通信。
Nodemon 是一个 Node.js 工具,它在检测到文件更改后会自动重启服务器,而 Socket.io 允许我们在服务器上配置实时连接。
创建一个 index.js 文件 - Web 服务器的入口点。
touch index.js
使用 Express.js 设置 Node.js 服务器。当您 http://localhost:4000/api
在浏览器中访问时,下面的代码片段会返回一个 JSON 对象。
//👇🏻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;
//👇🏻 New imports
const http = require("http").Server(app);
const cors = require("cors");
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
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
、、和文件的组件文件夹Home.js
。CreatePost.js
NotionPost.js
cd client
mkdir components
cd components
touch Login.js Home.js CreatePost.js NotionPost.js
更新 App.js
文件以通过 React Router 在不同的路由上呈现新创建的组件,如下所示:
import { BrowserRouter, Routes, Route } from "react-router-dom";
import NotionPost from "./components/NotionPost";
import CreatePost from "./components/CreatePost";
import Home from "./components/Home";
import Login from "./components/Login";
import { io } from "socket.io-client";
const socket = io.connect("http://localhost:4000");
function App() {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<Login socket={socket} />} />
<Route path='/dashboard' element={<Home socket={socket} />} />
<Route path='/post/create' element={<CreatePost socket={socket} />} />
<Route path='/post/:id' element={<NotionPost socket={socket} />} />
</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");
* {
box-sizing: border-box;
padding: 0;
margin: 0;
font-family: "Space Grotesk", sans-serif;
}
body {
padding: 0;
}
.login {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.login > h2 {
color: #00abb3;
margin-bottom: 30px;
}
.loginForm {
width: 70%;
display: flex;
flex-direction: column;
}
.loginForm > input {
margin: 10px 0;
padding: 10px 15px;
}
.home__navbar {
width: 100%;
height: 10vh;
padding: 20px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #eaeaea;
}
.home__navbar > h2 {
color: #00abb3;
}
.home__buttons {
display: flex;
align-items: center;
justify-content: baseline;
}
.home__createBtn {
padding: 10px;
cursor: pointer;
margin-right: 10px;
background-color: #00abb3;
border: none;
outline: none;
color: #fff;
}
.home__createBtn:hover,
.createForm__button:hover {
background-color: #02595e;
}
.home__notifyBtn {
padding: 10px;
cursor: pointer;
color: #00abb3;
background-color: #fff;
border: 1px solid #3c4048;
outline: none;
width: 100px;
}
.posts__container {
width: 100%;
min-height: 90vh;
padding: 30px 20px;
}
.post {
width: 100%;
min-height: 8vh;
background-color: #00abb3;
border-radius: 5px;
padding: 20px;
display: flex;
color: #eaeaea;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
}
.post__cta {
padding: 10px;
background-color: #fff;
cursor: pointer;
outline: none;
border: none;
border-radius: 2px;
}
.createPost {
min-height: 100vh;
width: 100%;
padding: 30px 20px;
}
.createPost > h2 {
text-align: center;
color: #00abb3;
}
.createForm {
width: 100%;
min-height: 80vh;
padding: 20px;
display: flex;
flex-direction: column;
}
.createForm__title {
padding: 10px;
height: 45px;
margin-bottom: 10px;
text-transform: capitalize;
border: 1px solid #3c4048;
}
.createForm__content {
padding: 15px;
margin-bottom: 15px;
border: 1px solid #3c4048;
}
.createForm__button {
width: 200px;
padding: 10px;
height: 45px;
background-color: #00abb3;
color: #fff;
outline: none;
border: none;
cursor: pointer;
border-radius: 5px;
}
.notionPost {
width: 100%;
min-height: 100vh;
background-color: #eaeaea;
display: flex;
flex-direction: column;
padding: 30px 50px;
}
.notionPost__container {
width: 90%;
min-height: 70vh;
margin-bottom: 30px;
}
.notionPost__author {
color: #00abb3;
}
.notionPost__date {
opacity: 0.4;
font-size: 12px;
}
.notionPost__content {
padding-top: 30px;
line-height: 200%;
}
.comments__container {
min-height: 70vh;
border: 1px solid #3c4048;
padding: 30px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 2px 2px 3px 1px rgba(208, 213, 219, 0.28);
border-radius: 3px;
}
.comments__inputContainer {
display: flex;
align-items: center;
margin: 30px 0;
}
.comments__input {
width: 50%;
padding: 15px;
margin-right: 15px;
}
.comments__cta,
.login__cta {
padding: 15px;
width: 200px;
cursor: pointer;
outline: none;
border: none;
background-color: #00abb3;
color: #fff;
}
.comment {
margin-bottom: 15px;
}
登录页面
在这里,应用程序接受用户名并将其保存在本地存储中以进行用户识别。将以下代码复制到登录组件中。
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();
//The username 👉🏻 console.log({ username });
localStorage.setItem("username", username);
navigate("/dashboard");
};
return (
<div className='login'>
<h2>Sign in to HackNotion</h2>
<form className='loginForm' onSubmit={handleLogin}>
<label htmlFor='username'>Enter your username</label>
<input
name='username'
id='username'
type='text'
value={username}
required
onChange={(e) => setUsername(e.target.value)}
/>
<button className='login__cta'>LOG IN</button>
</form>
</div>
);
};
export default Login;
主页
将以下代码复制到 Home.js
文件中。它代表应用程序的主布局。
import React from "react";
import { useNavigate } from "react-router-dom";
const Home = () => {
const navigate = useNavigate();
const createPostBtn = () => navigate("/post/create");
const readMoreBtn = () => navigate("/post/:id");
return (
<div className='home'>
<nav className='home__navbar'>
<h2>HackNotion</h2>
<div className='home__buttons'>
<button className='home__createBtn' onClick={createPostBtn}>
CREATE POST
</button>
<button className='home__notifyBtn'>NOTIFY</button>
</div>
</nav>
<div className='posts__container'>
<div className='post'>
<h3>How to create a new Socket.io client</h3>
<button className='post__cta' onClick={readMoreBtn}>
READ MORE
</button>
</div>
<div className='post'>
<h3>Creating React Native project with Expo</h3>
<button className='post__cta' onClick={readMoreBtn}>
READ MORE
</button>
</div>
</div>
</div>
);
};
export default Home;
NotionPost 页面
此页面是动态的,通过传入 URL 的 ID 显示每篇帖子的内容。用户可以在这里阅读帖子并添加评论。
将以下代码复制到NotionPost.js
文件中:
import React, { useState } from "react";
const NotionPost = () => {
const [comment, setComment] = useState("");
const handleAddComment = (e) => {
e.preventDefault();
console.log({ comment });
setComment("");
};
return (
<div className='notionPost'>
<div className='notionPost__container'>
<h1>How to create a new React Native project with Expo</h1>
<div className='notionPost__meta'>
<p className='notionPost__author'>By Nevo David</p>
<p className='notionPost__date'>Created on 22nd September, 2022</p>
</div>
<div className='notionPost__content'>
For this article, I will use Puppeteer and ReactJS. Puppeteer is a
Node.js library that automates several browser actions such as form
submission.
</div>
</div>
<div className='comments__container'>
<h2>Add Comments</h2>
<form className='comments__inputContainer' onSubmit={handleAddComment}>
<textarea
placeholder='Type in your comments...'
rows={5}
className='comments__input'
value={comment}
required
onChange={(e) => setComment(e.target.value)}
/>
<button className='comments__cta'>Add Comment</button>
</form>
<div>
<p className='comment'>
<span style={{ fontWeight: "bold" }}>Scopsy Dima</span> - Nice post
fam!❤️
</p>
</div>
</div>
</div>
);
};
export default NotionPost;
创建帖子页面
在这里,我们将创建一个简单的布局,允许用户通过添加标题和内容来创建帖子。用户还可以使用 React Tag标记其他用户。
React Tag 是一个库,允许我们通过单个组件轻松创建标签。它提供了许多功能,例如基于建议列表的自动完成、使用拖放重新排序等等。*
将下面的代码复制到CreatePost.js
文件中。
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
const CreatePost = () => {
const navigate = useNavigate();
const [postTitle, setPostTitle] = useState("");
const [postContent, setPostContent] = useState("");
//...gets the publish date for the post
const currentDate = () => {
const d = new Date();
return `${d.getDate()}-${d.getMonth() + 1}-${d.getFullYear()}`;
};
//...logs the post details to the console
const addPost = (e) => {
e.preventDefault();
console.log({
postTitle,
postContent,
username: localStorage.getItem("username"),
timestamp: currentDate(),
});
navigate("/dashboard");
};
return (
<>
<div className='createPost'>
<h2>Create a new Post</h2>
<form className='createForm' onSubmit={addPost}>
<label htmlFor='title'> Title</label>
<input
type='text'
required
value={postTitle}
onChange={(e) => setPostTitle(e.target.value)}
className='createForm__title'
/>
<label htmlFor='title'> Content</label>
<textarea
required
rows={15}
value={postContent}
onChange={(e) => setPostContent(e.target.value)}
className='createForm__content'
/>
<button className='createForm__button'>ADD POST</button>
</form>
</div>
</>
);
};
export default CreatePost;
将 React Tags 导入到 CreatePost.js
文件中:
import { WithContext as ReactTags } from "react-tag-input";
更新 CreatePost 组件以包含下面的代码片段,用于使用 React Tags 创建标签。
//👇🏻 The suggestion list for autocomplete
const suggestions = ["Tomer", "David", "Nevo"].map((name) => {
return {
id: name,
text: name,
};
});
const KeyCodes = {
comma: 188,
enter: 13,
};
//👇🏻 The comma and enter keys are used to separate each tags
const delimiters = [KeyCodes.comma, KeyCodes.enter];
//...The React component
const CreatePost = () => {
//👇🏻 An array containing the tags
const [tags, setTags] = useState([]);
//...deleting tags
const handleDelete = (i) => {
setTags(tags.filter((tag, index) => index !== i));
};
//...adding new tags
const handleAddition = (tag) => {
setTags([...tags, tag]);
};
//...runs when you click on a tag
const handleTagClick = (index) => {
console.log("The tag at index " + index + " was clicked");
};
return (
<div className='createPost'>
<form>
{/**...below the input fields---*/}
<ReactTags
tags={tags}
suggestions={suggestions}
delimiters={delimiters}
handleDelete={handleDelete}
handleAddition={handleAddition}
handleTagClick={handleTagClick}
inputFieldPosition='bottom'
autocomplete
/>
<button className='createForm__button'>ADD POST</button>
</form>
</div>
);
};
export default CreatePost;
React Tags 还允许我们自定义其元素。将以下代码添加到src/index.css
文件:
/*
You can learn how it's styled here:
https://stackblitz.com/edit/react-tag-input-1nelrc
*/
.ReactTags__tags react-tags-wrapper,
.ReactTags__tagInput {
width: 100%;
}
.ReactTags__selected span.ReactTags__tag {
border: 1px solid #ddd;
background: #00abb3;
color: white;
font-size: 12px;
display: inline-block;
padding: 5px;
margin: 0 5px;
border-radius: 2px;
min-width: 100px;
}
.ReactTags__selected button.ReactTags__remove {
color: #fff;
margin-left: 15px;
cursor: pointer;
background-color: orangered;
padding: 0 10px;
border: none;
outline: none;
}
.ReactTags__tagInput input.ReactTags__tagInputField,
.ReactTags__tagInput input.ReactTags__tagInputField:focus {
margin: 10px 0;
font-size: 12px;
width: 100%;
padding: 10px;
height: 45px;
text-transform: capitalize;
border: 1px solid #3c4048;
}
.ReactTags__selected span.ReactTags__tag {
border: 1px solid #ddd;
background: #63bcfd;
color: white;
font-size: 12px;
display: inline-block;
padding: 5px;
margin: 0 5px;
border-radius: 2px;
}
.ReactTags__selected a.ReactTags__remove {
color: #aaa;
margin-left: 5px;
cursor: pointer;
}
/* Styles for suggestions */
.ReactTags__suggestions {
position: absolute;
}
.ReactTags__suggestions ul {
list-style-type: none;
box-shadow: 0.05em 0.01em 0.5em rgba(0, 0, 0, 0.2);
background: white;
width: 200px;
}
.ReactTags__suggestions li {
border-bottom: 1px solid #ddd;
padding: 5px 10px;
margin: 0;
}
.ReactTags__suggestions li mark {
text-decoration: underline;
background: none;
font-weight: 600;
}
.ReactTags__suggestions ul li.ReactTags__activeSuggestion {
background: #fff;
cursor: pointer;
}
.ReactTags__remove {
border: none;
cursor: pointer;
background: none;
color: white;
}
恭喜!我们已经完成了 Notion 应用程序的布局。接下来,让我们学习如何使用 Socket.io Node.js 服务器添加所有必要的功能。
使用 Socket.io 创建新帖子
在本节中,我将指导您如何使用 Socket.io 创建新帖子并在 React 应用程序上显示它们。
通过 Socket.io 将新创建的帖子发送到服务器来更新组件addPost
内的功能。CreatePost
//👇🏻 Socket.io was passed from the App.js file
const CreatePost = ({ socket }) => {
//...other functions
const addPost = (e) => {
e.preventDefault();
//👇🏻 sends all the post details to the server
socket.emit("createPost", {
postTitle,
postContent,
username: localStorage.getItem("username"),
timestamp: currentDate(),
tags,
});
navigate("/dashboard");
};
return <div className='createPost'>...</div>;
};
在服务器上创建该事件的监听器。
socketIO.on("connection", (socket) => {
console.log(`⚡: ${socket.id} user just connected!`);
socket.on("createPost", (data) => {
/*👇🏻 data - contains all the post details
from the React app
*/
console.log(data);
});
socket.on("disconnect", () => {
socket.disconnect();
console.log("🔥: A user disconnected");
});
});
在后端服务器上创建一个包含所有帖子的数组,并将新帖子添加到列表中。
//👇🏻 generates a random ID
const fetchID = () => Math.random().toString(36).substring(2, 10);
let notionPosts = [];
socket.on("createPost", (data) => {
const { postTitle, postContent, username, timestamp, tags } = data;
notionPosts.unshift({
id: fetchID(),
title: postTitle,
author: username,
createdAt: timestamp,
content: postContent,
comments: [],
});
//👉🏻 We'll use the tags later for sending notifications
//👇🏻 The notionposts are sent back to the React app via another event
socket.emit("updatePosts", notionPosts);
});
通过复制以下代码,通过 useEffect 钩子向 React 应用程序上的概念帖子添加一个监听器:
//👇🏻 Within Home.js file
useEffect(() => {
socket.on("updatePosts", (posts) => console.log(posts));
}, [socket]);
显示帖子
将帖子保存为状态并按如下所示进行呈现:
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
const Home = ({ socket }) => {
const navigate = useNavigate();
const [posts, setPosts] = useState([]);
//👇🏻 Saves the posts into the "posts" state
useEffect(() => {
socket.on("updatePosts", (posts) => setPosts(posts));
}, [socket]);
const createPostBtn = () => navigate("/post/create");
//👇🏻 Navigates to the NotionPost page to view
// all the post contents
const readMoreBtn = (postID) => {
navigate(`/post/${postID}`);
};
return (
<div className='home'>
<nav className='home__navbar'>
<h2>HackNotion</h2>
<div className='home__buttons'>
<button className='home__createBtn' onClick={createPostBtn}>
CREATE POST
</button>
<button className='home__notifyBtn'>NOTIFY</button>
</div>
</nav>
<div className='posts__container'>
{posts?.map((post) => (
<div className='post' key={post.id}>
<h3>{post.title}</h3>
<button className='post__cta' onClick={() => readMoreBtn(post.id)}>
READ MORE
</button>
</div>
))}
</div>
</div>
);
};
export default Home;
到目前为止,我们只能在添加帖子时查看帖子。接下来,让我们能够在加载页面时显示帖子。
在服务器上创建返回概念帖子的路由。
app.get("/api", (req, res) => {
res.json(notionPosts);
});
更新Home.js
文件,获取概念帖子并监听来自服务器的新帖子。
useEffect(() => {
function fetchPosts() {
fetch("http://localhost:4000/api")
.then((res) => res.json())
.then((data) => setPosts(data))
.catch((err) => console.error(err));
}
fetchPosts();
}, []);
useEffect(() => {
socket.on("updatePosts", (posts) => setPosts(posts));
}, [socket]);
完成 Notion Post 组件
在上一节中,您学习了如何创建并向用户显示 Notion 帖子。在这里,您将学习如何在点击“阅读更多”按钮时显示每篇 Notion 帖子的内容。
更新 文件readMoreBtn
中的函数 Home.js
如下:
const readMoreBtn = (postID) => {
socket.emit("findPost", postID);
//👇🏻 navigates to the Notionpost routenavigate(`/post/${postID}`);
};
上面的代码片段获取所选帖子的 ID,并在重定向到帖子路由之前将包含帖子 ID 的 Socket.io 事件发送到服务器。
创建一个 findPost
事件监听器并通过另一个 Socket.io 事件返回帖子详细信息。
socket.on("findPost", (postID) => {
//👇🏻 Filter the notion post via the post ID
let result = notionPosts.filter((post) => post.id === postID);
//👇🏻 Returns a new event containing the post details
socket.emit("postDetails", result[0]);
});
postDetails
使用 组件监听事件 NotionPost
并渲染帖子详情如下:
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
const NotionPost = ({ socket }) => {
//👇🏻 gets the Post ID from its URL
const { id } = useParams();
const [comment, setComment] = useState("");
const [post, setPost] = useState({});
//👇🏻loading state for async request
const [loading, setLoading] = useState(true);
//👇🏻 Gets the post details from the server for display
useEffect(() => {
socket.on("postDetails", (data) => {
setPost(data);
setLoading(false);
});
}, [socket]);
//👇🏻 Function for creating new comments
const handleAddComment = (e) => {
e.preventDefault();
console.log("newComment", {
comment,
user: localStorage.getItem("username"),
postID: id,
});
setComment("");
};
if (loading) {
return <h2>Loading... Please wait</h2>;
}
return (
<div className='notionPost'>
<div className='notionPost__container'>
<h1>{post.title}</h1>
<div className='notionPost__meta'>
<p className='notionPost__author'>By {post.author}</p>
<p className='notionPost__date'>Created on {post.createdAt}</p>
</div>
<div className='notionPost__content'>{post.content}</div>
</div>
<div className='comments__container'>
<h2>Add Comments</h2>
<form className='comments__inputContainer' onSubmit={handleAddComment}>
<textarea
placeholder='Type in your comments...'
rows={5}
className='comments__input'
value={comment}
required
onChange={(e) => setComment(e.target.value)}
/>
<button className='comments__cta'>Add Comment</button>
</form>
<div>
<p className='comment'>
<span style={{ fontWeight: "bold" }}>Scopsy Dima</span> - Nice post
fam!❤️
</p>
</div>
</div>
</div>
);
};
export default NotionPost;
评论部分
在这里,我将指导您向每个概念帖子添加评论并实时显示它们。
更新 组件handleAddComment
内的功能 NotionPost
,将新的评论详情发送到服务器。
const handleAddComment = (e) => {
e.preventDefault();
socket.emit("newComment", {
comment,
user: localStorage.getItem("username"),
postID: id,
});
setComment("");
};
创建一个监听器来监听服务器上的事件,将评论添加到评论列表中。
socket.on("newComment", (data) => {
const { postID, user, comment } = data;
//👇🏻 filters the notion post via its ID
let result = notionPosts.filter((post) => post.id === postID);
//👇🏻 Adds the comment to the comments list
result[0].comments.unshift({
id: fetchID(),
user,
message: comment,
});
//👇🏻 sends the updated details to the React app
socket.emit("postDetails", result[0]);
});
更新 NotionPost.js
文件以显示任何现有评论。
return (
<div className='notionPost'>
<div className='notionPost__container'>
<h1>{post.title}</h1>
<div className='notionPost__meta'>
<p className='notionPost__author'>By {post.author}</p>
<p className='notionPost__date'>Created on {post.createdAt}</p>
</div>
<div className='notionPost__content'>{post.content}</div>
</div>
<div className='comments__container'>
<h2>Add Comments</h2>
<form className='comments__inputContainer' onSubmit={handleAddComment}>
<textarea
placeholder='Type in your comments...'
rows={5}
className='comments__input'
value={comment}
required
onChange={(e) => setComment(e.target.value)}
/>
<button className='comments__cta'>Add Comment</button>
</form>
{/** Displays existing comments to the user */}
<div>
{post.comments.map((item) => (
<p className='comment' key={item.id}>
<span style={{ fontWeight: "bold", marginRight: "15px" }}>
{item.user}
</span>
{item.message}
</p>
))}
</div>
</div>
</div>
);
恭喜!🎊 您现在可以使用 Socket.io 创建帖子并添加评论了。在本教程的剩余部分,我将指导您如何使用 Novu 向帖子中标记的每个用户发送通知。
如何将 Novu 添加到 React 和 Node.js 应用程序
Novu 允许您添加各种通知类型,例如电子邮件、短信和应用内通知。在本教程中,您将学习如何创建 Novu 项目、如何将 Novu 添加到您的 React 和 Node.js 项目中,以及如何使用 Novu 发送应用内通知。
在服务器上安装 Novu Node.js SDK,并在 React 应用程序中安装通知中心。
👇🏻 Install on the client
npm install @novu/notification-center
👇🏻 Install on the server
npm install @novu/node
运行以下代码创建一个 Novu 项目。您可以使用个性化仪表板。
👇🏻 Run 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? Notionging-Platform
❓ 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 添加到您的 React 和 Node.js 项目中。接下来,让我们学习如何在 Notionging 应用程序中添加应用内通知,以便在我们将用户添加到帖子时通知他们。
使用 Novu 添加应用内通知
在文件夹中创建一个Notify.js
文件src/components
,并将以下代码复制到该文件中。它包含文档中应用内通知所需的 元素。
import React from "react";
import {
NovuProvider,
PopoverNotificationCenter,
NotificationBell,
} from "@novu/notification-center";
import { useNavigate } from "react-router-dom";
const Notify = () => {
const navigate = useNavigate();
const onNotificationClick = (notification) =>
navigate(notification.cta.data.url);
return (
<div>
<NovuProvider
subscriberId='<YOUR_SUBSCRIBER_ID>'
applicationIdentifier='<YOUR_APP_ID>'
>
<PopoverNotificationCenter
onNotificationClick={onNotificationClick}
colorScheme='light'
>
{({ unseenCount }) => <NotificationBell unseenCount={unseenCount} />}
</PopoverNotificationCenter>
</NovuProvider>
</div>
);
};
export default Notify;
上面的代码片段将 Novu 通知铃铛图标添加到 Notify 组件,使我们能够查看应用程序内的所有通知。
该
NovuProvider
组件需要您先前复制的订户 ID 以及Novu 管理平台http://localhost:57807/demo
API 密钥下的设置部分中提供的应用程序 ID 。
将Notify组件导入到Home.js
文件中,并显示铃铛图标如下:
return (
<div className='home'>
<nav className='home__navbar'>
<h2>HackNotion</h2>
<div className='home__buttons'>
<button className='home__createBtn' onClick={createPostBtn}>
CREATE POST
</button>
<Notify />
</div>
</nav>
</div>
)
接下来,为应用程序创建工作流,它描述您想要添加到应用程序的功能。
从“开发”侧边栏中选择“通知”,然后创建一个通知模板。点击新创建的模板,然后点击“工作流编辑器”,并确保工作流如下:
从上图可以看出,Novu 在发送应用内通知之前会触发摘要引擎。
Novu Digest 允许我们控制应用程序内通知的发送方式。它会收集多个触发事件,并将它们作为一条消息发送。上图显示每 2 分钟发送一次通知,当您拥有大量用户且更新频繁时,这种方法非常有效。
单击 In-App
上图中的步骤并编辑通知模板以包含以下内容。
{{sender}} tagged you to a post
Novu 允许您使用Handlebars 模板引擎向模板添加动态内容或数据 。变量的数据
sender
将作为来自应用内请求的有效负载插入到模板中。
单击按钮保存模板 Update
并返回代码编辑器。
使用 Novu 发送通知
由于我们希望在将用户标记到帖子时向他们发送通知,因此我们必须将每个用户名存储在服务器上,在 React Tags 提供的建议列表中显示它们,并通过 Novu 向他们发送通知。
更新文件handleLogin
中的功能Login.js
,以便在登录时将用户名发送到服务器。
const handleLogin = (e) => {
e.preventDefault();
//👇🏻 sends the username to the server
socket.emit("addUser", username);
localStorage.setItem("username", username);
navigate("/dashboard");
};
监听事件并将用户名存储在服务器上的数组中。
let allUsers = [];
socket.on("addUser", (user) => {
allUsers.push(user);
});
另外,通过服务器上的另一条路线呈现用户列表。
app.get("/users", (req, res) => {
res.json(allUsers);
});
要在 React 标签建议列表中显示用户,您需要向 API 路由发送请求,获取用户列表并将其传递到列表中。
更新CreatePost
函数以获取用户列表,并在导航post/create
路由之前将其保存在本地存储中。
const createPostBtn = () => {
fetchUser();
navigate("/post/create");
};
const fetchUser = () => {
fetch("http://localhost:4000/users")
.then((res) => res.json())
.then((data) => {
//👇🏻 converts the array to a string
const stringData = data.toString();
//👇🏻 saved the data to local storage
localStorage.setItem("users", stringData);
})
.catch((err) => console.error(err));
};
接下来,从本地存储中检索所有用户,并将它们传递到 React Tags 提供的建议列表中,以便在CreatePost
组件内显示。
const [users, setUsers] = useState([]);
useEffect(() => {
function getUsers() {
const storedUsers = localStorage.getItem("users").split(",");
setUsers(storedUsers);
}
getUsers();
}, []);
const suggestions = users.map((name) => {
return {
id: name,
text: name,
};
});
要通知每个标记的用户,请创建一个循环遍历服务器上的用户并通过 Novu 向他们发送通知的函数。
在服务器上导入并启动 Novu。
const { Novu } = require("@novu/node");
const novu = new Novu("<YOUR_API_KEY>")
更新createPost
后端的监听器,向所有标记的用户发送通知。
//👇🏻 Loops through the tagged users and sends a notification to each one of them
const sendUsersNotification = (users, sender) => {
users.forEach(function (user) {
novuNotify(user, sender);
});
};
//👇🏻 sends a notification via Novu
const novuNotify = async (user, sender) => {
try {
await novu
.trigger("<TEMPLATE_ID>", {
to: {
subscriberId: user.id,
firstName: user.text,
},
payload: {
sender: sender,
},
})
.then((res) => console.log("Response >>", res));
} catch (err) {
console.error("Error >>>>", { err });
}
};
socket.on("createPost", (data) => {
const { postTitle, postContent, username, timestamp, tags } = data;
notionPosts.unshift({
id: fetchID(),
title: postTitle,
author: username,
createdAt: timestamp,
content: postContent,
comments: [],
});
//👇🏻 Calls the function to send a notification to all tagged users
sendUsersNotification(tags, username);
socket.emit("updatePosts", notionPosts);
});
恭喜!💃🏻 我们已经完成了这个项目的代码。
结论
到目前为止,您已经学习了如何在 React 和 Node.js 应用程序中设置 Socket.io、如何在客户端和 Node.js 服务器之间发送消息、如何在 React 和 Node.js 应用程序中添加 Novu 以及如何使用 Novu 发送通知。
本教程演示了如何使用 Socket.io 和 Novu 构建项目。您可以随意改进项目,例如添加身份验证库,并将博客文章保存到支持实时通信的数据库。
本教程的完整代码可以在这里找到:https://github.com/novuhq/blog/tree/main/blogging-platform-with-react-socketIO
感谢您的阅读!
PS:如果您能通过 star 来帮助我们,我将非常感激 🤩
https://github.com/novuhq/novu