You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 06:05:21