基于MERN栈的用户交易平台通知功能(铃铛图标)最佳实践咨询
MERN栈实现网站通知功能的最佳实践
后端实现(MongoDB + Express)
1. 设计通知数据模型
在MongoDB中定义通知集合的Schema,覆盖核心业务需求:
// models/Notification.js const mongoose = require('mongoose'); const notificationSchema = new mongoose.Schema({ recipient: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, triggerType: { type: String, enum: ['new_post', 'comment_on_post', 'reply_to_comment'], required: true }, triggerUser: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, targetId: { // 关联的帖子ID或评论ID type: mongoose.Schema.Types.ObjectId, required: true }, isRead: { type: Boolean, default: false }, createdAt: { type: Date, default: Date.now, expires: 604800 // 可选:7天后自动清理过期通知 } }); // 添加索引提升查询性能 notificationSchema.index({ recipient: 1, isRead: 1 }); notificationSchema.index({ recipient: 1, createdAt: -1 }); module.exports = mongoose.model('Notification', notificationSchema);
2. 触发通知的业务逻辑
在对应业务接口中嵌入通知创建逻辑:
- 被关注用户发布帖子时:
// routes/posts.js const Notification = require('../models/Notification'); const User = require('../models/User'); router.post('/', async (req, res) => { // 创建帖子核心逻辑 const newPost = await Post.create(postData); // 获取当前用户的所有关注者 const user = await User.findById(req.user.id).populate('followers'); // 批量给关注者创建通知 const notificationPromises = user.followers.map(follower => { return Notification.create({ recipient: follower._id, triggerType: 'new_post', triggerUser: req.user.id, targetId: newPost._id }); }); await Promise.all(notificationPromises); res.status(201).json(newPost); });
- 他人评论用户帖子时:
// routes/comments.js router.post('/:postId', async (req, res) => { const post = await Post.findById(req.params.postId); // 创建评论核心逻辑 const newComment = await Comment.create(commentData); // 排除评论者自己,给帖子作者发通知 if (post.author.toString() !== req.user.id) { await Notification.create({ recipient: post.author, triggerType: 'comment_on_post', triggerUser: req.user.id, targetId: newComment._id }); } res.status(201).json(newComment); });
- 他人回复用户评论时:
router.post('/reply/:commentId', async (req, res) => { const parentComment = await Comment.findById(req.params.commentId); // 创建回复核心逻辑 const newReply = await Comment.create(replyData); // 排除回复者自己,给被回复的评论作者发通知 if (parentComment.author.toString() !== req.user.id) { await Notification.create({ recipient: parentComment.author, triggerType: 'reply_to_comment', triggerUser: req.user.id, targetId: newReply._id }); } res.status(201).json(newReply); });
3. 通知管理API
提供获取、标记已读的接口:
// routes/notifications.js router.get('/', async (req, res) => { // 分页获取当前用户的通知,按创建时间倒序 const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 10; const notifications = await Notification.find({ recipient: req.user.id }) .populate('triggerUser', 'username avatar') .sort({ createdAt: -1 }) .skip((page - 1) * limit) .limit(limit); // 获取未读通知数量 const unreadCount = await Notification.countDocuments({ recipient: req.user.id, isRead: false }); res.json({ notifications, unreadCount }); }); // 标记单个通知为已读 router.patch('/:id/read', async (req, res) => { const notification = await Notification.findByIdAndUpdate( req.params.id, { isRead: true }, { new: true } ); res.json(notification); }); // 标记所有通知为已读 router.patch('/all/read', async (req, res) => { await Notification.updateMany( { recipient: req.user.id, isRead: false }, { isRead: true } ); res.json({ message: '所有通知已标记为已读' }); });
前端实现(React)
1. 右上角铃铛组件
实现带未读红点的弹窗式通知列表:
// components/NotificationBell.js import { useState, useEffect } from 'react'; import axios from 'axios'; const NotificationBell = () => { const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [isOpen, setIsOpen] = useState(false); // 获取通知数据 const fetchNotifications = async () => { try { const res = await axios.get('/api/notifications'); setNotifications(res.data.notifications); setUnreadCount(res.data.unreadCount); } catch (err) { console.error(err); } }; useEffect(() => { fetchNotifications(); // 可选:30秒定期刷新,配合实时推送可省略 const interval = setInterval(fetchNotifications, 30000); return () => clearInterval(interval); }, []); // 标记单个通知为已读并跳转 const handleNotificationClick = async (notification) => { try { await axios.patch(`/api/notifications/${notification._id}/read`); // 根据通知类型跳转对应页面 switch (notification.triggerType) { case 'new_post': window.location.href = `/posts/${notification.targetId}`; break; case 'comment_on_post': case 'reply_to_comment': window.location.href = `/posts/${notification.targetId}`; // 需关联评论所属帖子ID break; } fetchNotifications(); setIsOpen(false); } catch (err) { console.error(err); } }; // 标记所有通知为已读 const markAllAsRead = async () => { try { await axios.patch('/api/notifications/all/read'); fetchNotifications(); } catch (err) { console.error(err); } }; return ( <div className="notification-bell"> <button onClick={() => setIsOpen(!isOpen)} className="bell-btn"> <svg className="bell-icon" viewBox="0 0 24 24"> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/> </svg> {unreadCount > 0 && ( <span className="unread-badge">{unreadCount}</span> )} </button> {isOpen && ( <div className="notification-dropdown"> <div className="dropdown-header"> <h3>通知</h3> {unreadCount > 0 && ( <button onClick={markAllAsRead} className="mark-all-btn"> 标记所有已读 </button> )} </div> <div className="notification-list"> {notifications.length === 0 ? ( <p>暂无通知</p> ) : ( notifications.map(notification => ( <div key={notification._id} className={`notification-item ${!notification.isRead ? 'unread' : ''}`} onClick={() => handleNotificationClick(notification)} > <img src={notification.triggerUser.avatar || '/default-avatar.png'} alt={notification.triggerUser.username} className="user-avatar" /> <div className="notification-content"> <p> <strong>{notification.triggerUser.username}</strong> {notification.triggerType === 'new_post' && ' 发布了新帖子'} {notification.triggerType === 'comment_on_post' && ' 评论了你的帖子'} {notification.triggerType === 'reply_to_comment' && ' 回复了你的评论'} </p> <small>{new Date(notification.createdAt).toLocaleString()}</small> </div> </div> )) )} </div> </div> )} </div> ); }; export default NotificationBell;
2. 实时通知推送(Socket.io)
实现即时通知推送,无需页面刷新:
- 后端集成:
// server.js const http = require('http'); const socketIo = require('socket.io'); const express = require('express'); const app = express(); const server = http.createServer(app); const io = socketIo(server, { cors: { origin: process.env.CLIENT_URL } }); // 存储用户ID与Socket连接的映射 const userSockets = new Map(); io.on('connection', (socket) => { // 用户登录后传递userId绑定连接 socket.on('join', (userId) => { userSockets.set(userId, socket.id); }); socket.on('disconnect', () => { for (const [userId, socketId] of userSockets.entries()) { if (socketId === socket.id) { userSockets.delete(userId); break; } } }); }); // 在创建通知后推送实时消息(示例:帖子通知) await Promise.all(notificationPromises); user.followers.forEach(follower => { const socketId = userSockets.get(follower._id.toString()); if (socketId) { io.to(socketId).emit('new-notification', { message: `${req.user.username}发布了新帖子` }); } });
- 前端集成:
// 在App.js中添加Socket监听 import { useEffect } from 'react'; import io from 'socket.io-client'; import { fetchNotifications } from './utils/notificationUtils'; // 复用之前的获取通知函数 const socket = io(process.env.REACT_APP_SERVER_URL); useEffect(() => { const userId = localStorage.getItem('userId'); if (userId) { socket.emit('join', userId); } // 监听新通知事件,刷新通知列表 socket.on('new-notification', () => { fetchNotifications(); }); return () => socket.disconnect(); }, []);
性能与体验优化
- 数据库索引:给通知集合的
recipient、isRead、createdAt字段添加索引,提升查询速度。 - 分页加载:前端获取通知时分页,避免一次性加载大量数据。
- 通知过期:在MongoDB Schema中设置
expires自动清理旧通知,减少数据量。 - 未读数量缓存:前端缓存未读数量,配合实时推送更新,减少不必要的请求。
内容的提问来源于stack exchange,提问作者Mubarak B
相关产品推荐
相关产品推荐

