如何使用 React、Websockets 和 Novu 构建 dev.to 通知中心🔥

2025-06-10

如何使用 React、Websockets 和 Novu 构建 dev.to 通知中心🔥

这篇文章是关于什么的?

无论你正在构建什么应用程序,你都可能在某个时候向用户发送通知。它可以通过电子邮件、短信、推送通知,或者像你在DEV 社区上那样的通知中心来实现。在本文中,我将向你展示如何实现DEV 社区通知中心。

为了演示,我创建了DEV 社区设计——至少我尝试过。每当有新帖子发布时,我都会通过铃铛图标发送通知。如果 30 秒内收到多条帖子,我会将它们合并为一条通知(摘要/批量)。

通知
你知道每次有新通知时,那种想点击通知按钮的冲动吗?如果你也遇到过,请在评论区留言告诉我👇

这实际上是提高应用参与度的好方法,但那是另一回事了🤯

什么是 Websockets (Socket.io)?

WebSocket 在客户端和服务器之间建立连接,允许它们双向发送数据:客户端-服务器和服务器-客户端。与 HTTP 相比,WebSocket 提供持久的双向客户端-服务器连接,从而可以实时发送和接收消息。

在本文中,我将使用 Socket.io 进行实时通信,因为它遵循 WebSocket 协议并提供了出色的功能,例如回退到 HTTP 长轮询或自动重新连接,这使我们能够构建高效的实时应用程序。

如何将 React 应用连接到 Socket.io 🚀

在这里,我们将为 DEV Community 克隆版设置项目环境。您还将学习如何将 Socket.io 添加到 React 和 Node.js 应用程序中,以及如何通过 Socket.io 连接两个开发服务器以实现实时通信。

创建包含两个名为客户端和服务器的子文件夹的项目文件夹。

mkdir devto-clone
cd devto-clone
mkdir client server
Enter fullscreen mode Exit fullscreen mode

通过终端导航到客户端文件夹并创建一个新的 React.js 项目。

cd client
npx create-react-app ./
Enter fullscreen mode Exit fullscreen mode

安装 Socket.io 客户端 API 和 React Router。React  Router 是一个 JavaScript 库,它使我们能够在 React 应用程序中的页面之间导航。

npm install socket.io-client react-router-dom
Enter fullscreen mode Exit fullscreen mode

从 React 应用中删除多余的文件(例如徽标和测试文件),并更新App.js文件以显示如下所示的 Hello World。

function App() {
    return (
        <div>
            <p>Hello World!</p>
        </div>
    );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

导航到服务器文件夹并创建一个package.json文件。

cd server & npm init -y
Enter fullscreen mode Exit fullscreen mode

安装 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
Enter fullscreen mode Exit fullscreen mode

创建一个index.js文件——Web 服务器的入口点。

touch index.js
Enter fullscreen mode Exit fullscreen mode

使用 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}`);
});
Enter fullscreen mode Exit fullscreen mode

导入 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}`);
});
Enter fullscreen mode Exit fullscreen mode

接下来,将 Socket.io 添加到项目中,以创建实时连接。在app.get()代码块之前,复制以下代码。接下来,将 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', () => {
      console.log('🔥: A user disconnected');
    });
});
Enter fullscreen mode Exit fullscreen mode

从上面的代码片段中,该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"
  },
Enter fullscreen mode Exit fullscreen mode

您现在可以使用以下命令通过 Nodemon 运行服务器。

npm start
Enter fullscreen mode Exit fullscreen mode

打开App.js客户端文件夹中的文件并将 React 应用程序连接到 Socket.io 服务器。

import socketIO from "socket.io-client";
const socket = socketIO.connect("http://localhost:4000");

function App() {
    return (
        <div>
            <p>Hello World!</p>
        </div>
    );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

启动 React.js 服务器。

npm start
Enter fullscreen mode Exit fullscreen mode

检查服务器运行的终端;React.js 客户端的 ID 应该出现在终端上。

恭喜🥂,React 应用程序已成功通过 Socket.io 连接到服务器。

💡 在本文的剩余部分,我将指导您创建应用程序的页面。我们将创建一个主页(用户登录应用程序的地方)、一个 404 页面(用于未经身份验证的用户)以及一个受保护的帖子页面(仅对经过身份验证的用户可见),用户可以在其中创建、查看和回复帖子。

创建应用程序的主页

在这里,我们将为接受用户名并将其保存到本地存储以供识别的应用程序创建主页。

在文件夹中创建一个名为 components 的文件夹client/src。然后,创建主页组件。

cd src
mkdir components & cd components
touch Home.js
Enter fullscreen mode Exit fullscreen mode

将以下代码复制到Home.js文件中。该代码片段显示一个表单输入,该输入接受用户名并将其存储在本地存储中。

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";

const Home = () => {
    const [username, setUsername] = useState("");
    const navigate = useNavigate();

    const handleSignIn = (e) => {
        e.preventDefault();
        localStorage.setItem("_username", username);
        setUsername("");
        navigate("/post");
    };
    return (
        <main className='home'>
            <h2>Sign in to Dev.to</h2>
            <form className='home__form' onSubmit={handleSignIn}>
                <label htmlFor='username'>Your Username</label>
                <input
                    type='text'
                    id='username'
                    name='username'
                    value={username}
                    onChange={(e) => setUsername(e.target.value)}
                />
                <button className='home__cta'>SIGN IN</button>
            </form>
        </main>
    );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

配置 React Router 以启用应用程序页面之间的导航。将以下代码复制到src/App.js文件中并创建引用的组件。

import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import socketIO from "socket.io-client";

import PostPage from "./components/PostPage";
import Home from "./components/Home";
import NullPage from "./components/NullPage";

const socket = socketIO.connect("http://localhost:4000");

const App = () => {
    return (
        <BrowserRouter>
            <div>
                <Routes>
                    <Route path='/post' element={<PostPage socket={socket} />} />
                    <Route path='/' element={<Home />} />
                    <Route path='*' element={<NullPage />} />
                </Routes>
            </div>
        </BrowserRouter>
    );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

代码片段使用 React Router v6 为主页、帖子页面和空页面分配不同的路由,并将 Socket.io 库传递到PostPage组件中。

导航到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;
    font-family: "Space Grotesk", sans-serif;
}

body {
    margin: 0;
    padding: 0;
}
.home {
    width: 100%;
    min-height: 100vh;
    background-color: #cfd2cf;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}
.home__form {
    display: flex;
    flex-direction: column;
    width: 60%;
}
.home__cta {
    padding: 10px;
    font-size: 16px;
    cursor: pointer;
    border: 1px solid #333;
    outline: none;
    width: 200px;
}
.navbar {
    display: flex;
    justify-content: space-between;
    padding: 20px;
    align-items: center;
    height: 10vh;
    position: sticky;
    top: 0;
    border-bottom: 1px solid #ddd;
}
.logo {
    height: 7vh;
    width: 50px;
    border-radius: 5px;
}
.notification__container {
    display: flex;
    align-items: center;
}
.notification__container > div {
    margin-right: 15px;
}
.logOutBtn {
    padding: 10px;
    width: 150px;
    color: red;
    border: 1px solid #333;
    background-color: #fff;
    cursor: pointer;
}
.input__container {
    width: 100%;
    min-height: 50vh;
    padding: 15px;
}
.input__form {
    display: flex;
    width: 70%;
    flex-direction: column;
    margin: 0 auto;
}
label {
    font-size: 18px;
    margin-bottom: 10px;
}
input {
    margin-bottom: 15px;
    padding: 10px;
    font-size: 16px;
    outline: none;
    border: 1px solid #ddd;
}
textarea {
    font-size: 16px;
    padding: 10px;
    width: 100%;
    border-radius: 10px;
    outline: none;
    border: 1px solid #ddd;
    margin-bottom: 15px;
}
.sendBtn {
    padding: 10px;
    font-size: 16px;
    height: 45px;
    width: 200px;
    cursor: pointer;
    border: 1px solid #333;
    outline: none;
}
.sendBtn:hover {
    color: #fff;
    background-color: #333;
}
.articles__container {
    width: 100%;
    padding: 20px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
}
.article {
    width: 70%;
    min-height: 300px;
    padding: 15px;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 1px 1px 3px 1px rgba(208, 213, 219, 0.28);
    margin-bottom: 30px;
    background-color: #fcfffc;
}
.article__content {
    word-spacing: 3px;
}
.likeBtn__container {
    display: flex;
    align-items: center;
}
.likeBtn {
    color: #fff;
    cursor: pointer;
    font-size: 30px;
    margin-right: 10px;
}
@media screen and (max-width: 768px) {
    .article {
        width: 100%;
    }
    .input__form {
        width: 100%;
    }
}
Enter fullscreen mode Exit fullscreen mode

我们已经创建了 DEV Community 克隆版的主页。接下来,让我们设计帖子路由的用户界面。

为未经身份验证的用户创建 404 页面

在本节中,我们将为未经身份验证的用户或不在应用程序的任何定义路由上的用户创建一个简单的 404 页面。

导航到components/NullPage.js并粘贴以下代码:

import React from "react";
import { Link } from "react-router-dom";

const NullPage = () => {
    return (
        <div style={{ padding: "20px" }}>
            <h3>
                Seems you are lost, head back to the <Link to='/'>home page</Link>
            </h3>
        </div>
    );
};

export default NullPage;
Enter fullscreen mode Exit fullscreen mode

创建受保护的帖子页面

在本节中,我们将创建PostPage并使其仅对经过身份验证的用户可见。用户可以创建、查看和回复帖子,并在用户创建帖子时收到通知。

帖子页面

从上图可以看出,帖子页面分为三个部分:

  • Nav组件 - 包含 DEV 社区徽标、铃铛图标和注销按钮
  • CreatePost组件 - 包含表单输入和按钮
  • Post包含已创建帖子的组件。

由于我们已经能够定义帖子页面的布局,因此我们现在可以为设计创建组件。

将以下代码复制到PostPage.js文件中。您需要创建 Nav、CreatePost 和 Posts 组件。

import React from "react";
import CreatePost from "./CreatePost";
import Nav from "./Nav";
import Posts from "./Posts";
import NullPage from "./NullPage";

const PostPage = () => {
    return (
        <div>
            {localStorage.getItem("_username") ? (
                <>
                    <Nav />
                    <CreatePost />
                    <Posts />
                </>
            ) : (
                <NullPage />
            )}
        </div>
    );
};

export default PostPage;
Enter fullscreen mode Exit fullscreen mode

上面的代码片段在显示帖子页面的内容之前检查用户是否已登录;否则,它将呈现 404 页面(空页面)。

构建导航组件

将下面的代码复制到Nav.js文件中。

import React from "react";
import { useNavigate } from "react-router-dom";

const Nav = () => {
    const navigate = useNavigate();

    const handleLogOut = () => {
        localStorage.removeItem("_username");
        navigate("/");
    };

    return (
        <nav className='navbar'>
            <div>
                <img
                    src='https://res.cloudinary.com/practicaldev/image/fetch/s--R9qwOwpC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/78hs31fax49uwy6kbxyw.png'
                    alt='Dev.to'
                    className='logo'
                />
            </div>
            <div className='notification__container'>
                <div>
                    <button>BELL ICON </button>
                </div>
                <button className='logOutBtn' onClick={handleLogOut}>
                    LOG OUT
                </button>
            </div>
        </nav>
    );
};

export default Nav;
Enter fullscreen mode Exit fullscreen mode

上面的代码片段显示了 DEV 社区的徽标以及两个分别代表注销和通知图标的按钮。注销按钮用于注销用户并将其重定向到主页。

构建 CreatePost 组件

在这里,我们将创建一个允许用户创建博客文章的表单。下面的代码片段接受博客文章的标题和内容,并将它们记录到控制台。

import React, { useState } from "react";

const CreatePost = () => {
    const [title, setTitle] = useState("");
    const [content, setContent] = useState("");

    function addNewPost(e) {
        e.preventDefault();
        console.log({ title, content });
        setContent("");
        setTitle("");
    }

    return (
        <div className='input__container'>
            <form className='input__form' onSubmit={addNewPost}>
                <label htmlFor='title'>Title</label>
                <input
                    name='title'
                    type='text'
                    id='title'
                    value={title}
                    onChange={(e) => setTitle(e.target.value)}
                    required
                />

                <textarea
                    name='content'
                    id='content'
                    rows='7'
                    placeholder='Write the contents'
                    value={content}
                    required
                    onChange={(e) => setContent(e.target.value)}
                ></textarea>
                <div>
                    <button className='sendBtn'>SEND POST</button>
                </div>
            </form>
        </div>
    );
};

export default CreatePost;
Enter fullscreen mode Exit fullscreen mode

构建 Posts 组件

复制以下代码。博客文章目前为虚拟文章。

import React from "react";

const Posts = () => {
    const posts = [
        {
            id: 1,
            title: "What is Novu?",
            content:
                "is the first open-source notification infrastructure that manages all forms of communication from email to SMS, Push notifications, etc.",
        },
        {
            id: 2,
            title: "What is Websocket?",
            content:
                "WebSockets are used to create a connection between a client and a server, allowing them to send data both ways; client-server and server-client.",
        },
    ];
    return (
        <div className='articles__container'>
            <h1>Recent Articles</h1>

            {posts.map((post) => (
                <div className='article' key={post.id}>
                    <h2>{post.title}</h2>
                    <p className='article__content'>{post.content}</p>
                    <div className='likeBtn__container'>
                        <p className='likeBtn'>
                            <span role='img' aria-label='like'>
                                👍
                            </span>
                        </p>
                        <p>1</p>
                    </div>
                </div>
            ))}
        </div>
    );
};

export default Posts;
Enter fullscreen mode Exit fullscreen mode

恭喜!💃🏻 我们已经完成了 DEV Community 克隆版的用户界面。接下来,让我们创建所需的功能。

如何在 React 应用和 Socket.io 服务器之间进行通信

在本节中,您将学习如何通过 Socket.io 将消息从 React 应用程序发送到 Node.js 服务器,反之亦然。

App.js文件中,将 Socket.io 传递到PostPage组件中 - 将通过 Web 套接字与服务器通信。

import React from "react";
import socketIO from "socket.io-client";
import PostPage from "./components/PostPage";
import { Route, Routes } from "react-router-dom";
import Home from "./components/Home";
import NullPage from "./components/NullPage";

const socket = socketIO.connect("http://localhost:4000");

const App = () => {
    return (
        <div>
            <Routes>
                <Route path='/post' element={<PostPage socket={socket} />} />
                <Route path='/' element={<Home />} />
                <Route path='*' element={<NullPage />} />
            </Routes>
        </div>
    );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

创建博客文章

在这里,我们将使用户能够创建博客文章。

更新PostPage.js文件以接受 Socket.io 作为 prop 并将其传递到CreatePost组件中。

import React from "react";
import CreatePost from "./CreatePost";
import Nav from "./Nav";
import Posts from "./Posts";
import NullPage from "./NullPage";

const PostPage = ({ socket }) => {
    return (
        <div>
            {localStorage.getItem("_username") ? (
                <>
                    <Nav />
                    <CreatePost socket={socket} />
                    <Posts />
                </>
            ) : (
                <NullPage />
            )}
        </div>
    );
};

export default PostPage;
Enter fullscreen mode Exit fullscreen mode

更新CreatePost组件以将帖子发送到后端 Node.js 服务器。

import React, { useState } from "react";

const CreatePost = ({ socket }) => {
    const [title, setTitle] = useState("");
    const [content, setContent] = useState("");

    const addNewPost = (e) => {
        e.preventDefault();
//sends the post details to the backend via Socket.io
        socket.emit("newPost", {
            id: Math.random(),
            title,
            content,
            likes: 0,
            username: localStorage.getItem("_username"),
        });

        setContent("");
        setTitle("");
    };

    return (
        <div className='input__container'>
            <form className='input__form' onSubmit={addNewPost}>
                <label htmlFor='title'>Title</label>
                <input
                    name='title'
                    type='text'
                    id='title'
                    value={title}
                    onChange={(e) => setTitle(e.target.value)}
                    required
                />

                <textarea
                    name='content'
                    id='content'
                    rows='7'
                    placeholder='Write the contents'
                    value={content}
                    required
                    onChange={(e) => setContent(e.target.value)}
                ></textarea>
                <div>
                    <button className='sendBtn'>SEND POST</button>
                </div>
            </form>
        </div>
    );
};

export default CreatePost;
Enter fullscreen mode Exit fullscreen mode

从上面的代码片段中,该addNewPost函数发出一条带有标签的消息,newPost其中包含随机 ID、标题、内容、用户名和帖子的初始点赞数。

通过在 Socket.io 块中复制以下代码,在后端创建事件监听器

socketIO.on("connection", (socket) => {
    console.log(`⚡: ${socket.id} user just connected!`);

    //Event Listener for new posts
    socket.on("newPost", (data) => {
        //logs the newly created posts to the terminal
        console.log(data);
    });

    socket.on("disconnect", () => {
        socket.disconnect();
    });
});
Enter fullscreen mode Exit fullscreen mode

向用户显示博客文章

在上一节中,我们成功地将帖子发送到了后端。在这里,我们将帖子发送回 React 应用进行显示。

在中创建一个数组server/index.js,并将新创建的帖子添加到该数组中。

let posts = [];

socketIO.on("connection", (socket) => {
    console.log(`⚡: ${socket.id} user just connected!`);

    //Event Listener for new posts
    socket.on("newPost", (data) => {
        //adds every new post as the first element in the array
        posts.unshift(data);
    });

    socket.on("disconnect", () => {
        socket.disconnect();
    });
});
Enter fullscreen mode Exit fullscreen mode

创建另一个事件,将帖子数组发送到 React 应用程序。

let posts = [];

socketIO.on("connection", (socket) => {
    console.log(`⚡: ${socket.id} user just connected!`);

    //Event Listener for new posts
    socket.on("newPost", (data) => {
        //adds every new post as the first element in the array
        posts.unshift(data);
        //sends the array of posts to the React app
        socket.emit("posts", posts);
    });

    socket.on("disconnect", () => {
        socket.disconnect();
    });
});
Enter fullscreen mode Exit fullscreen mode

返回PostPage.js文件,创建“post”事件的监听器,并将详细信息传递到Posts组件中进行显示。

import React, { useState, useEffect } from "react";
import CreatePost from "./CreatePost";
import Nav from "./Nav";
import Posts from "./Posts";
import NullPage from "./NullPage";

const PostPage = ({ socket }) => {
    const [posts, setPosts] = useState([]);

    useEffect(() => {
        socket.on("posts", (data) => setPosts(data));
    }, []);

    return (
        <div>
            {localStorage.getItem("_username") ? (
                <>
                    <Nav />
                    <CreatePost socket={socket} />
                    <Posts posts={posts} />
                </>
            ) : (
                <NullPage />
            )}
        </div>
    );
};

export default PostPage;
Enter fullscreen mode Exit fullscreen mode

通过组件渲染帖子Posts。下面的代码片段在将数据传递到用户界面之前检查帖子数组是否为空。

import React from "react";

const Posts = ({ posts }) => {
    return (
        <div className='articles__container'>
            {posts[0] && <h1>Recent Articles</h1>}
            {posts.length > 0 &&
                posts.map((post) => (
                    <div className='article' key={post.id}>
                        <h2>{post.title}</h2>
                        <p className='article__content'>{post.content}</p>
                        <div className='likeBtn__container'>
                            <p className='likeBtn'>
                                <span role='img' aria-label='like'>
                                    👍
                                </span>
                            </p>
                            <p>{post.likes}</p>
                        </div>
                    </div>
                ))}
        </div>
    );
};

export default Posts;
Enter fullscreen mode Exit fullscreen mode

接下来,让我们让用户喜欢他们最喜欢的帖子,并相应地更新喜欢的数量。

创建“点赞帖子”功能

在本节中,我将引导您向应用程序添加“喜欢帖子”功能,使用户能够对他们选择的任何帖子做出反应。

将 Socket.io 传递到Posts组件中并创建一个postLiked函数,该函数触发一个事件,将用户喜欢的帖子的 id 发送到 Node.js 服务器。

import React from "react";

const Posts = ({ posts, socket }) => {
    //Sends the id of the selected post via a Socket.io event
    const postLiked = (id) => socket.emit("postLiked", id);

    return (
        <div className='articles__container'>
            {posts[0] && <h1>Recent Articles</h1>}
            {posts.length > 0 &&
                posts.map((post) => (
                    <div className='article' key={post.id}>
                        <h2>{post.title}</h2>
                        <p className='article__content'>{post.content}</p>
                        <div className='likeBtn__container'>

                            {/* The postLiked function runs after clicking on the like emoji*/}
                            <p className='likeBtn' onClick={() => postLiked(post.id)}>
                                <span role='img' aria-label='like'>
                                    👍
                                </span>
                            </p>
                            <p>{post.likes > 0 && post.likes}</p>

                        </div>
                    </div>
                ))}
        </div>
    );
};

export default Posts;
Enter fullscreen mode Exit fullscreen mode

在 Node.js 服务器上创建一个监听器,接受帖子 ID,更新点赞数,并将帖子及其最新更新的点赞数发送回 React 应用程序。

/*
The increaseLikes function loops through the array of posts,
fetches for a post with the same ID, and
updates the number of likes
*/
const increaseLikes = (postId, array) => {
    for (let i = 0; i < array.length; i++) {
        if (array[i].id === postId) {
            array[i].likes += 1;
        }
    }
};

socketIO.on("connection", (socket) => {
    console.log(`⚡: ${socket.id} user just connected!`);

    socket.on("newPost", (data) => {
        posts.unshift(data);
        socket.emit("posts", posts);
    });
    socket.on("postLiked", (postId) => {
        //Function accepts the post ID and post array
        increaseLikes(postId, posts);
        //Sends the newly updated array to the React app
        socket.emit("posts", posts);
    });
    socket.on("disconnect", () => {
        socket.disconnect();
    });
});
Enter fullscreen mode Exit fullscreen mode

恭喜!🔥🎉申请即将完成。

我们已经能够设置 React 应用和 Node.js 服务器之间的通信通道。接下来,让我们学习如何在新博客文章创建或点赞时向用户显示通知。

如何将 Novu 添加到 React 和 Node.js 应用

在本节中,您将学习如何将 Novu 添加到 DEV Community 克隆,以使我们能够将通知从 React 应用程序发送到 Socket.io 服务器。

Novu——第一个开源通知基础设施

简单介绍一下我们。Novu 是第一个开源通知基础设施。我们主要负责管理所有产品通知。通知可以是应用内通知(类似于开发者社区的Websockets中的铃铛图标)、电子邮件、短信等等。

我们每周都会推出精彩内容!
关注我们的推特,就能看到我们这里没有的内容!
https://twitter.com/novuhq

诺武

导航到客户端文件夹并通过运行以下代码创建一个 Novu 项目。

cd client
npx novu init
Enter fullscreen mode Exit fullscreen mode

在创建 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
Enter fullscreen mode Exit fullscreen mode

访问演示网页http://localhost:57807/demo,复制您的订阅者 ID,然后点击“跳过教程”按钮。我们将在本教程的后续部分使用它。

演示

在您的 React 项目中安装 Novu Notification 包作为依赖项。

npm install @novu/notification-center
Enter fullscreen mode Exit fullscreen mode

更新components/Nav.js文件以包含 Novu 及其 文档中所需的元素。

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);

    const handleLogOut = () => {
        localStorage.removeItem("_username");
        navigate("/");
    };

    return (
        <nav className='navbar'>
            <div>
                <img
                    src='https://res.cloudinary.com/practicaldev/image/fetch/s--R9qwOwpC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/78hs31fax49uwy6kbxyw.png'
                    alt='Dev.to'
                    className='logo'
                />
            </div>
            <div className='notification__container'>
                <div>
                    <NovuProvider
                        subscriberId='<YOUR_SUBSCRIBER_ID>'
                        applicationIdentifier='<YOUR_APP_ID>'
                    >
                        <PopoverNotificationCenter
                            onNotificationClick={onNotificationClick}
                            colorScheme='light'
                        >
                            {({ unseenCount }) => (
                                <NotificationBell unseenCount={unseenCount} />
                            )}
                        </PopoverNotificationCenter>
                    </NovuProvider>
                </div>
                <button className='logOutBtn' onClick={handleLogOut}>
                    LOG OUT
                </button>
            </div>
        </nav>
    );
};

export default Nav;
Enter fullscreen mode Exit fullscreen mode

上面的代码片段将 Novu 的通知铃铛图标添加到 Nav 组件,使我们能够查看应用程序中的所有通知。

💡 该NovuProvider组件需要您先前复制的订阅者 ID以及Novu 管理平台http://localhost:57807/demoAPI 密钥下的设置部分中提供的应用程序 ID 

NovuProvider

导航到服务器文件夹并安装适用于 Node.js 的 Novu SDK。

cd server
npm install @novu/node
Enter fullscreen mode Exit fullscreen mode

从包中导入 Novu 并使用您的 API 密钥创建实例。

//server/index.js

const { Novu } = require("@novu/node");
const novu = new Novu("<YOUR_API_KEY>");
Enter fullscreen mode Exit fullscreen mode

导入 Novu

在服务器上创建一个新的 POST 路由 - Novu 将从该路由发送通知。

app.post("/notify", async (req, res) => {
    const { username } = req.body;
    console.log({ username });
});
Enter fullscreen mode Exit fullscreen mode

使用 Novu 发送应用内通知

在上一节中,我指导您在 React 和 Node.js 服务器上设置 Novu。在这里,我将指导您在 Web 应用程序中通过 Novu 发送通知。

在浏览器中打开 Novu Manage 平台;通知模板位于“通知”选项卡中。

选择模板,点击工作流编辑器,确保工作流如下:
工作流编辑器

Novu Digest允许您控制应用中通知的发送方式。它会收集多个触发事件,并将它们作为一条消息发送。

单击该In-App步骤并编辑模板以包含以下内容

{{username}} just added a new post!
Enter fullscreen mode Exit fullscreen mode

💡 Novu 允许您使用Handlebars 模板引擎向模板添加动态内容或数据 。username向 Novu 发出 POST 请求时,变量的数据将传递到模板中。

单击按钮保存模板Update,然后返回代码编辑器,并更新上一节中的 POST 路由,如下所示:

app.post("/notify", async (req, res) => {
    const { username } = req.body;
    await novu
        .trigger("<NOTIFICATION_TEMPLATE_IDENTIFIER>", {
            to: {
                subscriberId: "<YOUR_SUBSCRIBER_ID>",
            },
            payload: {
                username,
            },
        })
        .catch((err) => console.error(err));
});
Enter fullscreen mode Exit fullscreen mode

上面的代码片段通过其 ID 触发通知模板,并提供所需的数据 -username作为模板的有效负载。

在服务器上配置 Novu 并创建发送通知的路由后,发送请求以在前端获取通知。

更新CreatePost.js文件如下:

import React, { useState } from "react";

const CreatePost = ({ socket }) => {
    const [title, setTitle] = useState("");
    const [content, setContent] = useState("");

    function addNewPost(e) {
        e.preventDefault();
        socket.emit("newPost", {
            id: Math.random(),
            title,
            content,
            likes: 0,
            username: localStorage.getItem("_username"),
        });
        /* 
            Calls the sendNotification function immediately 
            after creating a new post 
        */
        sendNotification();
        setContent("");
        setTitle("");
    }

    /*
        The sendNotification function makes a post request to the server
        containing the username saved in the local storage.
    */
    async function sendNotification() {
        try {
            const sendNotification = await fetch("http://localhost:4000/notify", {
                method: "POST",
                body: JSON.stringify({
                    username: localStorage.getItem("_username"),
                }),
                headers: {
                    Accept: "application/json",
                    "Content-Type": "application/json",
                },
            });
            const data = await sendNotification.json();
            console.log(data);
        } catch (err) {
            console.error(err);
        }
    }

    return (
        <div className='input__container'>
            <form className='input__form' onSubmit={addNewPost}>
                ...
            </form>
        </div>
    );
};

export default CreatePost;
Enter fullscreen mode Exit fullscreen mode

恭喜!💃🏻 我们已完成此项目的代码。您可以点击导航栏中的通知铃来查看通知。

恭喜

结论

到目前为止,您已经学习了如何将 Novu 添加到 React 和 Node.js 应用程序中,使用 Novu 发送通知,在 React 和 Node.js 应用程序中设置 Socket.io,以及在客户端和 Node.js 服务器之间发送消息。

本文演示了如何使用 Socket.io 和 Novu 构建项目。欢迎通过以下方式改进项目:

  • 添加身份验证库
  • 将博客文章保存到支持实时通信的数据库中
  • 添加对每篇博客文章进行评论的功能
  • 当用户对博客文章做出反应和评论时,通过 Novu 发送通知。

本教程的完整代码可以在这里找到:https://github.com/novuhq/blog/tree/main/devto-notifications-novu

感谢您的阅读!

附言:我们每周都会发布精彩内容!
关注我们的推特,就能看到我们这里没有的内容!
https://twitter.com/novuhq

叽叽喳喳

链接链接:https://dev.to/novu/i-implemented-the-dev-community-notification-center-with-react-novu-and-websockets-7fk
PREV
使用 React 和 NodeJS 的杂货店应用程序的通知系统
NEXT
如何打造一场黑客马拉松