支持用户注册和登录、发布动态、点赞、评论、私信等功能的社交媒体平台创建!!!
需要整体源代码的可以在我的代码仓下载https://gitcode.com/speaking_me/social-media-platformTest.git
社交媒体平台
描述:社交媒体平台需要支持用户注册、发布动态、点赞、评论、私信等功能。
技术栈:
- 前端:React, Angular, Vue.js
- 后端:Node.js (Express.js), Python (Django), Ruby on Rails
- 数据库:MySQL, PostgreSQL, Redis
- 消息队列:RabbitMQ, Kafka
- 文件存储:AWS S3, Google Cloud Storage
好的,让我们详细探讨一下社交媒体平台的后端开发。社交媒体平台是一个复杂的系统,涉及到多个方面的技术和架构设计。以下是每个部分的详细说明和建议:
1. 用户注册与登录
功能:
- 用户注册:收集用户的邮箱、用户名、密码等信息。
- 用户登录:验证用户的身份信息。
- 用户注销:允许用户安全地注销账户。
技术栈:
- 认证机制:JWT (JSON Web Tokens) 或者 Session
- 密码加密:使用 bcrypt 或者其他安全的哈希算法
- 邮件验证:使用第三方邮件服务(如 SendGrid、Mailgun)发送验证邮件
2. 发布动态
功能:
- 用户可以发布文字、图片、视频等动态。
- 动态可以包含标签、位置信息等元数据。
技术栈:
- 文件上传:使用 Multer (Node.js) 或者 Django 的文件处理模块
- 文件存储:AWS S3 或 Google Cloud Storage
- 数据库设计:设计合理的表结构来存储动态信息及其关联数据
3. 点赞与评论
功能:
- 用户可以对动态进行点赞和取消点赞。
- 用户可以对动态发表评论,并回复其他用户的评论。
技术栈:
- 数据库设计:设计点赞和评论表,确保高效查询和更新
- 缓存:使用 Redis 缓存热门动态的点赞数和评论数,减少数据库压力
4. 私信功能
功能:
- 用户可以发送私信给其他用户。
- 用户可以查看收到的私信和发送的私信。
技术栈:
- 消息队列:使用 RabbitMQ 或 Kafka 处理消息的异步传递
- 数据库设计:设计私信表,记录发送者、接收者、消息内容等信息
5. 数据库设计
表结构示例:
-
用户表 (users):
id
(主键)username
email
password_hash
created_at
updated_at
-
动态表 (posts):
id
(主键)user_id
(外键,关联用户表)content
(动态内容)image_url
(图片链接)video_url
(视频链接)created_at
updated_at
-
点赞表 (likes):
id
(主键)post_id
(外键,关联动态表)user_id
(外键,关联用户表)created_at
-
评论表 (comments):
id
(主键)post_id
(外键,关联动态表)user_id
(外键,关联用户表)content
(评论内容)created_at
updated_at
-
私信表 (messages):
id
(主键)sender_id
(外键,关联用户表)receiver_id
(外键,关联用户表)content
(消息内容)created_at
is_read
(是否已读)
6. 消息队列
用途:
- 异步处理:处理大量的消息传递,减轻服务器压力。
- 解耦:将发送消息和接收消息的逻辑分离,提高系统的可扩展性和可靠性。
技术栈:
- RabbitMQ:适用于简单的消息队列需求
- Kafka:适用于高吞吐量的消息传递和流处理
7. 文件存储
用途:
- 存储用户上传的图片、视频等文件。
技术栈:
- AWS S3:提供高可用、高持久性的对象存储服务
- Google Cloud Storage:类似 AWS S3 的云存储服务
8. 安全性
功能:
- 输入验证:防止 SQL 注入、XSS 攻击等安全问题。
- 权限管理:确保用户只能访问和操作自己的数据。
- 数据备份:定期备份数据库,防止数据丢失。
9. 性能优化
技术栈:
- 缓存:使用 Redis 缓存常用数据,减少数据库查询次数。
- 负载均衡:使用 Nginx 或其他负载均衡器分散请求压力。
- CDN:使用 CDN 加速静态资源的加载速度。
10. 日志记录
用途:
- 监控系统运行状态:记录系统日志,帮助排查问题。
- 审计:记录用户操作日志,用于审计和回溯。
技术栈:
- ELK Stack:Elasticsearch、Logstash、Kibana 组成的日志管理解决方案
- Graylog:另一个强大的日志管理和分析工具
通过以上各个部分的设计和实现,您可以构建一个功能完善的社交媒体平台。
源代码
前端代码
index.html
头部 (<head>
)
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>社交媒体平台</title> <link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"> </head>
<meta charset="UTF-8">
:设置字符编码为 UTF-8。<meta name="viewport" content="width=device-width, initial-scale=1.0">
:设置视口,使页面在移动设备上正确显示。<title>
:设置页面标题。<link rel="stylesheet" href="styles.css">
:引入自定义的 CSS 文件。<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
:引入 Font Awesome 图标库。
导航栏 (<nav>
)
<nav class="navbar"> <div class="nav-brand">社交平台</div> <div class="nav-menu"> <a href="#" class="nav-item">首页</a> <a href="#" class="nav-item">消息</a> <div class="notification-wrapper"> <a href="#" class="nav-item" id="notification-toggle"> <i class="fas fa-bell"></i> <span class="notification-badge">0</span> </a> <div class="notification-dropdown hidden"> <div class="notification-header"> <h3>通知</h3> <button class="mark-all-read">全部已读</button> </div> <div class="notification-list"> <!-- 通知内容将动态加载 --> </div> </div> </div> <a href="#" class="nav-item">个人中心</a> </div> </nav>
<div class="nav-brand">社交平台</div>
:显示网站的品牌名称。<a href="#" class="nav-item">首页</a>
:首页链接。<a href="#" class="nav-item">消息</a>
:消息链接。<div class="notification-wrapper">
:通知区域,包含通知图标和未读通知数量。<a href="#" class="nav-item" id="notification-toggle">
:点击通知图标会显示通知下拉菜单。<div class="notification-dropdown hidden">
:通知下拉菜单,默认隐藏。<div class="notification-header">
:通知头部,包含标题和“全部已读”按钮。<div class="notification-list">
:通知列表,内容将通过 JavaScript 动态加载。
<a href="#" class="nav-item">个人中心</a>
:个人中心链接。
主要内容 (<main>
)
<main class="container"> <div class="sidebar"> <div class="profile-card"> <img src="avatar.jpg" alt="头像" class="avatar"> <h3>用户名</h3> <p>个人简介</p> </div> </div> <div class="content"> <div class="post-form"> <textarea placeholder="分享新动态..."></textarea> <div class="post-form-actions"> <div class="upload-wrapper"> <input type="file" id="image-upload" accept="image/*" multiple class="file-input"> <label for="image-upload" class="upload-btn"> <i class="fas fa-image"></i> 添加图片 </label> </div> <button class="post-btn">发布</button> </div> <div class="image-preview"></div> </div> <div class="posts"> <article class="post"> <div class="post-header"> <img src="avatar.jpg" alt="用户头像" class="post-avatar"> <div class="post-info"> <h4>用户名</h4> <span class="post-time">2小时前</span> </div> </div> <div class="post-content"> 这是一条示例动态内容 </div> <div class="post-images"> <img src="example1.jpg" alt="发布图片"> </div> <div class="post-actions"> <button class="action-btn"><i class="fas fa-heart"></i> 点赞</button> <button class="action-btn comment-toggle"><i class="fas fa-comment"></i> 评论</button> <button class="action-btn"><i class="fas fa-share"></i> 分享</button> </div> <div class="comments-section hidden"> <div class="comment-form"> <input type="text" placeholder="发表评论..." class="comment-input"> <button class="comment-submit">发送</button> </div> <div class="comments-list"> <div class="comment"> <img src="avatar.jpg" alt="评论者头像" class="comment-avatar"> <div class="comment-content"> <div class="comment-header"> <span class="comment-author">评论用户</span> <span class="comment-time">1小时前</span> </div> <p class="comment-text">这是一条示例评论</p> </div> </div> </div> </div> </article> </div> </div> </main>
<div class="sidebar">
:侧边栏,包含用户个人信息卡片。<div class="profile-card">
:用户个人信息卡片,包含头像、用户名和个人简介。
<div class="content">
:主要内容区域,包含动态发布表单和动态列表。<div class="post-form">
:动态发布表单。<textarea placeholder="分享新动态..."></textarea>
:输入框,用户在此输入动态内容。<div class="post-form-actions">
:表单操作按钮。<div class="upload-wrapper">
:图片上传区域。<input type="file" id="image-upload" accept="image/*" multiple class="file-input">
:文件输入框,用于选择图片文件。<label for="image-upload" class="upload-btn">
:文件上传按钮,点击时触发文件输入框。
<button class="post-btn">发布</button>
:发布按钮,提交表单。
<div class="image-preview"></div>
:图片预览区域,显示用户选择的图片。
<div class="posts">
:动态列表,包含一条示例动态。<article class="post">
:动态项。<div class="post-header">
:动态头部,包含用户头像和信息。<div class="post-content">
:动态内容。<div class="post-images">
:动态图片。<div class="post-actions">
:动态操作按钮,包括点赞、评论和分享。<div class="comments-section hidden">
:评论区域,默认隐藏。<div class="comment-form">
:评论表单,用户在此输入评论内容。<div class="comments-list">
:评论列表,包含一条示例评论。
login.html
头部 (<head>
)
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登录 - 社交媒体平台</title> <link rel="stylesheet" href="styles.css"> </head>
<meta charset="UTF-8">
:设置字符编码为 UTF-8。<meta name="viewport" content="width=device-width, initial-scale=1.0">
:设置视口,使页面在移动设备上正确显示。<title>登录 - 社交媒体平台</title>
:设置页面标题。<link rel="stylesheet" href="styles.css">
:引入自定义的 CSS 文件。
身体 (<body>
)
<body> <div class="auth-container"> <div class="auth-box"> <h2>登录</h2> <form id="loginForm" class="auth-form"> <div class="form-group"> <label for="email">邮箱</label> <input type="email" id="email" required> </div> <div class="form-group"> <label for="password">密码</label> <input type="password" id="password" required> </div> <button type="submit" class="auth-btn">登录</button> </form> <p class="auth-link"> 还没有账号?<a href="register.html">立即注册</a> </p> </div> </div> <script type="module"> import { api } from './api.js'; document.getElementById('loginForm').addEventListener('submit', async (e) => { e.preventDefault(); try { const email = document.getElementById('email').value; const password = document.getElementById('password').value; await api.login(email, password); window.location.href = '/'; } catch (error) { alert(error.message); } }); </script> </body>
<div class="auth-container">
:登录页面的外层容器,用于居中对齐。<div class="auth-box">
:登录表单的容器。<h2>登录</h2>
:标题,显示“登录”。<form id="loginForm" class="auth-form">
:登录表单。<div class="form-group">
:表单组,包含标签和输入框。<label for="email">邮箱</label>
:邮箱输入框的标签。<input type="email" id="email" required>
:邮箱输入框,类型为email
,必填。
<div class="form-group">
:表单组,包含标签和输入框。<label for="password">密码</label>
:密码输入框的标签。<input type="password" id="password" required>
:密码输入框,类型为password
,必填。
<button type="submit" class="auth-btn">登录</button>
:提交按钮,点击时提交表单。
<p class="auth-link">
:提示文本,包含注册链接。<a href="register.html">立即注册</a>
:注册链接,指向注册页面。
register.html
头部 (<head>
)
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>注册 - 社交媒体平台</title> <link rel="stylesheet" href="styles.css"> </head>
<meta charset="UTF-8">
:设置字符编码为 UTF-8。<meta name="viewport" content="width=device-width, initial-scale=1.0">
:设置视口,使页面在移动设备上正确显示。<title>注册 - 社交媒体平台</title>
:设置页面标题。<link rel="stylesheet" href="styles.css">
:引入自定义的 CSS 文件。
身体 (<body>
)
<body> <div class="auth-container"> <div class="auth-box"> <h2>注册账号</h2> <form id="registerForm" class="auth-form"> <div class="form-group"> <label for="username">用户名</label> <input type="text" id="username" required> </div> <div class="form-group"> <label for="email">邮箱</label> <input type="email" id="email" required> </div> <div class="form-group"> <label for="password">密码</label> <input type="password" id="password" required> </div> <button type="submit" class="auth-btn">注册</button> </form> <p class="auth-link"> 已有账号?<a href="login.html">立即登录</a> </p> </div> </div> <script type="module"> import { api } from './api.js'; document.getElementById('registerForm').addEventListener('submit', async (e) => { e.preventDefault(); try { const username = document.getElementById('username').value; const email = document.getElementById('email').value; const password = document.getElementById('password').value; await api.register(username, email, password); window.location.href = '/'; } catch (error) { alert(error.message); } }); </script> </body>
<div class="auth-container">
:注册页面的外层容器,用于居中对齐。<div class="auth-box">
:注册表单的容器。<h2>注册账号</h2>
:标题,显示“注册账号”。<form id="registerForm" class="auth-form">
:注册表单。<div class="form-group">
:表单组,包含标签和输入框。<label for="username">用户名</label>
:用户名输入框的标签。<input type="text" id="username" required>
:用户名输入框,类型为text
,必填。
<div class="form-group">
:表单组,包含标签和输入框。<label for="email">邮箱</label>
:邮箱输入框的标签。<input type="email" id="email" required>
:邮箱输入框,类型为email
,必填。
<div class="form-group">
:表单组,包含标签和输入框。<label for="password">密码</label>
:密码输入框的标签。<input type="password" id="password" required>
:密码输入框,类型为password
,必填。
<button type="submit" class="auth-btn">注册</button>
:提交按钮,点击时提交表单。
<p class="auth-link">
:提示文本,包含登录链接。<a href="login.html">立即登录</a>
:登录链接,指向登录页面。
api.js
1、构造函数 (constructor
)
constructor(baseUrl = 'http://localhost:3000/api') { this.baseUrl = baseUrl; this.token = localStorage.getItem('token'); }
baseUrl
:API 的基础 URL,默认值为http://localhost:3000/api
。token
:从本地存储中获取当前用户的令牌(如果存在)。
2. 设置和清除令牌的方法
setToken(token) { this.token = token; localStorage.setItem('token', token); } clearToken() { this.token = null; localStorage.removeItem('token'); }
setToken(token)
:设置当前用户的令牌,并将其保存到本地存储中。clearToken()
:清除当前用户的令牌,并从本地存储中移除。
3. 发送请求的方法 (request
)
async request(endpoint, options = {}) { const headers = { 'Content-Type': 'application/json', ...options.headers }; if (this.token) { headers.Authorization = `Bearer ${this.token}`; } try { const response = await fetch(`${this.baseUrl}${endpoint}`, { ...options, headers }); if (response.status === 401) { this.clearToken(); window.location.href = '/login.html'; return; } const data = await response.json(); return data; } catch (error) { console.error('API请求失败:', error); throw error; } }
headers
:设置请求头,默认包含Content-Type: application/json
,并根据this.token
添加授权头。fetch
:发送 HTTP 请求到指定的endpoint
,合并options
和headers
。response.status === 401
:如果响应状态码为 401(未授权),清除令牌并重定向到登录页面。response.json()
:解析响应数据为 JSON 格式。catch
:捕获并处理请求过程中发生的错误。
4. 用户认证相关方法
注册 (register
)
async register(username, email, password) { const response = await this.request('/auth/register', { method: 'POST', body: JSON.stringify({ username, email, password }) }); this.setToken(response.token); return response; }
/auth/register
:注册用户。method: 'POST'
:使用 POST 方法。body: JSON.stringify({ username, email, password })
:将用户名、邮箱和密码作为请求体发送。this.setToken(response.token)
:注册成功后,设置令牌。
登录 (login
)
async login(email, password) { const response = await this.request('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) }); this.setToken(response.token); return response; }
/auth/login
:登录用户。method: 'POST'
:使用 POST 方法。body: JSON.stringify({ email, password })
:将邮箱和密码作为请求体发送。this.setToken(response.token)
:登录成功后,设置令牌。
注销 (logout
)
async logout() { this.clearToken(); }
this.clearToken()
:注销用户,清除令牌。
获取当前用户信息 (getCurrentUser
)
async getCurrentUser() { return this.request('/auth/me'); }
/auth/me
:获取当前用户的详细信息。
5. 帖子相关方法
获取帖子列表 (getPosts
)
async getPosts(page = 1) { return this.request(`/posts?page=${page}`); }
/posts?page=${page}
:获取指定页数的帖子列表,默认第一页。
发布新帖子 (createPost
)
async createPost(content, images = []) { return this.request('/posts', { method: 'POST', body: JSON.stringify({ content, images }) }); }
/posts
:发布新帖子。method: 'POST'
:使用 POST 方法。body: JSON.stringify({ content, images })
:将帖子内容和图片作为请求体发送。
添加评论 (addComment
)
async addComment(postId, content) { return this.request(`/posts/${postId}/comments`, { method: 'POST', body: JSON.stringify({ content }) }); }
/posts/${postId}/comments
:向指定帖子添加评论。method: 'POST'
:使用 POST 方法。body: JSON.stringify({ content })
:将评论内容作为请求体发送。
点赞/取消点赞 (toggleLike
)
async toggleLike(postId) { return this.request(`/posts/${postId}/like`, { method: 'POST' }); }
/posts/${postId}/like
:点赞或取消点赞指定帖子。method: 'POST'
:使用 POST 方法。
6. 导出 API 实例
export const api = new Api();
export const api = new Api();
:创建一个Api
类的实例并导出,以便在其他模块中使用。
notifications.js
1. 通知系统 (NotificationSystem
)
构造函数 (constructor
)
constructor() { this.notifications = []; this.unreadCount = 0; this.setupEventListeners(); this.initializeWebSocket(); }
this.notifications
:存储所有通知的数组。this.unreadCount
:未读通知的数量。this.setupEventListeners()
:设置事件监听器。this.initializeWebSocket()
:初始化 WebSocket 连接。
设置事件监听器 (setupEventListeners
)
setupEventListeners() { const notificationToggle = document.getElementById('notification-toggle'); const notificationDropdown = document.querySelector('.notification-dropdown'); const markAllRead = document.querySelector('.mark-all-read'); notificationToggle.addEventListener('click', (e) => { e.preventDefault(); notificationDropdown.classList.toggle('hidden'); }); markAllRead.addEventListener('click', () => { this.markAllAsRead(); }); // 点击外部关闭通知下拉框 document.addEventListener('click', (e) => { if (!e.target.closest('.notification-wrapper')) { notificationDropdown.classList.add('hidden'); } }); }
notificationToggle
:通知图标按钮。notificationDropdown
:通知下拉框。markAllRead
:标记所有通知为已读的按钮。notificationToggle.addEventListener('click', ...)
:点击通知图标时,切换通知下拉框的显示状态。markAllRead.addEventListener('click', ...)
:点击“标记全部为已读”按钮时,调用markAllAsRead
方法。document.addEventListener('click', ...)
:点击页面其他地方时,关闭通知下拉框。
初始化 WebSocket 连接 (initializeWebSocket
)
initializeWebSocket() { this.ws = new WebSocket('ws://your-websocket-server'); this.ws.onmessage = (event) => { const notification = JSON.parse(event.data); this.addNotification(notification); }; }
this.ws = new WebSocket('ws://your-websocket-server')
:连接到 WebSocket 服务器。this.ws.onmessage
:接收 WebSocket 消息时,解析消息并调用addNotification
方法。
添加通知 (addNotification
)
addNotification(notification) { this.notifications.unshift(notification); this.unreadCount++; this.updateNotificationBadge(); this.renderNotification(notification); }
this.notifications.unshift(notification)
:将新通知添加到通知数组的开头。this.unreadCount++
:增加未读通知数量。this.updateNotificationBadge()
:更新通知徽章。this.renderNotification(notification)
:渲染新通知。
渲染通知 (renderNotification
)
renderNotification(notification) { const notificationList = document.querySelector('.notification-list'); const notificationElement = document.createElement('div'); notificationElement.className = 'notification-item unread'; notificationElement.innerHTML = ` <img src="${notification.avatar}" alt="通知头像" class="notification-avatar"> <div class="notification-content"> <p>${notification.message}</p> <span class="notification-time">${this.formatTime(notification.time)}</span> </div> `; notificationList.insertBefore(notificationElement, notificationList.firstChild); }
notificationList
:通知列表的 DOM 元素。notificationElement
:创建一个新的通知项元素。notificationElement.innerHTML
:设置通知项的内容。notificationList.insertBefore(notificationElement, notificationList.firstChild)
:将新通知项插入到通知列表的最前面。
标记所有通知为已读 (markAllAsRead
)
markAllAsRead() { this.unreadCount = 0; this.updateNotificationBadge(); document.querySelectorAll('.notification-item.unread').forEach(item => { item.classList.remove('unread'); }); }
this.unreadCount = 0
:将未读通知数量设置为 0。this.updateNotificationBadge()
:更新通知徽章。document.querySelectorAll('.notification-item.unread').forEach(...)
:移除所有未读通知项的unread
类。
更新通知徽章 (updateNotificationBadge
)
updateNotificationBadge() { const badge = document.querySelector('.notification-badge'); badge.textContent = this.unreadCount; badge.style.display = this.unreadCount > 0 ? 'block' : 'none'; }
badge
:通知徽章的 DOM 元素。badge.textContent = this.unreadCount
:设置徽章的文本内容为未读通知数量。badge.style.display = this.unreadCount > 0 ? 'block' : 'none'
:根据未读通知数量显示或隐藏徽章。
格式化时间 (formatTime
)
formatTime(timestamp) { const date = new Date(timestamp); const now = new Date(); const diff = now - date; if (diff < 60000) return '刚刚'; if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`; return `${Math.floor(diff / 86400000)}天前`; }
date
:通知的时间戳。now
:当前时间。diff
:时间差。if (diff < 60000) return '刚刚'
:如果时间差小于 1 分钟,返回“刚刚”。if (diff < 3600000) return
${Math.floor(diff / 60000)}分钟前`:如果时间差小于 1 小时,返回“多少分钟前”。if (diff < 86400000) return
${Math.floor(diff / 3600000)}小时前`:如果时间差小于 1 天,返回“多少小时前”。return
${Math.floor(diff / 86400000)}天前`:如果时间差大于等于 1 天,返回“多少天前”。
2. 动态加载帖子 (InfiniteScroll
)
构造函数 (constructor
)
constructor() { this.page = 1; this.loading = false; this.hasMore = true; this.setupScrollListener(); }
this.page
:当前加载的页数。this.loading
:是否正在加载。this.hasMore
:是否有更多帖子可以加载。this.setupScrollListener()
:设置滚动事件监听器。
设置滚动事件监听器 (setupScrollListener
)
setupScrollListener() { window.addEventListener('scroll', () => { if (this.loading || !this.hasMore) return; const { scrollTop, scrollHeight, clientHeight } = document.documentElement; if (scrollTop + clientHeight >= scrollHeight - 100) { this.loadMorePosts(); } }); }
window.addEventListener('scroll', ...)
:监听窗口滚动事件。if (this.loading || !this.hasMore) return
:如果正在加载或没有更多帖子,不执行加载操作。scrollTop + clientHeight >= scrollHeight - 100
:当滚动到底部附近时,调用loadMorePosts
方法。
加载更多帖子 (loadMorePosts
)
async loadMorePosts() { this.loading = true; this.showLoader(); try { const response = await fetch(`/api/posts?page=${this.page}`); const data = await response.json(); if (data.posts.length === 0) { this.hasMore = false; return; } this.renderPosts(data.posts); this.page++; } catch (error) { console.error('加载帖子失败:', error); } finally { this.hideLoader(); this.loading = false; } }
this.loading = true
:设置加载状态为true
。this.showLoader()
:显示加载指示器。const response = await fetch(
/api/posts?page=${this.page})
:发送请求获取更多帖子。const data = await response.json()
:解析响应数据。if (data.posts.length === 0) { this.hasMore = false; return; }
:如果没有更多帖子,设置hasMore
为false
并返回。this.renderPosts(data.posts)
:渲染新加载的帖子。this.page++
:增加页数。catch (error) { console.error('加载帖子失败:', error); }
:捕获并处理请求过程中发生的错误。finally { this.hideLoader(); this.loading = false; }
:隐藏加载指示器,设置加载状态为false
。
渲染帖子 (renderPosts
)
renderPosts(posts) { const postsContainer = document.querySelector('.posts'); posts.forEach(post => { const postElement = this.createPostElement(post); postsContainer.appendChild(postElement); }); }
postsContainer
:帖子容器的 DOM 元素。posts.forEach(post => ...)
:遍历帖子数组。const postElement = this.createPostElement(post)
:创建帖子元素。postsContainer.appendChild(postElement)
:将帖子元素添加到帖子容器中。
创建帖子元素 (createPostElement
)
createPostElement(post) { const article = document.createElement('article'); article.className = 'post'; article.innerHTML = ` <div class="post-header"> <img src="${post.avatar}" alt="用户头像" class="post-avatar"> <div class="post-info"> <h4>${post.username}</h4> <span class="post-time">${this.formatTime(post.timestamp)}</span> </div> </div> <div class="post-content">${post.content}</div> ${post.images ? this.renderImages(post.images) : ''} <div class="post-actions"> <button class="action-btn"><i class="far fa-heart"></i> 点赞</button> <button class="action-btn comment-toggle"><i class="far fa-comment"></i> 评论</button> <button class="action-btn"><i class="far fa-share"></i> 分享</button> </div> `; return article; }
article
:创建一个新的article
元素。article.className = 'post'
:设置帖子类名。article.innerHTML
:设置帖子的内容。post.images ? this.renderImages(post.images) : ''
:如果有图片,调用renderImages
方法渲染图片。return article
:返回创建的帖子元素。
渲染图片 (renderImages
)
renderImages(images) { return ` <div class="post-images"> ${images.map(img => `<img src="${img}" alt="发布图片">`).join('')} </div> `; }
images.map(img =>
<img src="${img}" alt="发布图片">)
:将图片数组转换为图片元素的字符串数组。.join('')
:将字符串数组连接成一个字符串。return ...
:返回包含图片的 HTML 字符串。
显示加载指示器 (showLoader
)
showLoader() { const loader = document.querySelector('.loading-spinner'); loader.classList.remove('hidden'); }
loader
:加载指示器的 DOM 元素。loader.classList.remove('hidden')
:显示加载指示器。
隐藏加载指示器 (hideLoader
)
hideLoader() { const loader = document.querySelector('.loading-spinner'); loader.classList.add('hidden'); }
loader
:加载指示器的 DOM 元素。loader.classList.add('hidden')
:隐藏加载指示器。
格式化时间 (formatTime
)
formatTime(timestamp) { return new NotificationSystem().formatTime(timestamp); }
new NotificationSystem().formatTime(timestamp)
:使用通知系统的formatTime
方法格式化时间。
3. 初始化
document.addEventListener('DOMContentLoaded', () => { const notificationSystem = new NotificationSystem(); const infiniteScroll = new InfiniteScroll(); });
document.addEventListener('DOMContentLoaded', ...)
:当文档加载完成后,初始化通知系统和动态加载系统。const notificationSystem = new NotificationSystem()
:创建通知系统的实例。const infiniteScroll = new InfiniteScroll()
:创建动态加载系统的实例。
script.js
1. 基本设置
变量定义
let currentRotation = 0; const carousel = document.querySelector('.carousel'); const prevBtn = document.getElementById('prev'); const nextBtn = document.getElementById('next');
currentRotation
:当前轮播图的旋转角度。carousel
:轮播图的 DOM 元素。prevBtn
和nextBtn
:上一个和下一个按钮的 DOM 元素。
2. 手动旋转功能
旋转函数 (rotateCarousel
)
function rotateCarousel(direction) { currentRotation += direction * 45; carousel.style.transform = `rotateY(${currentRotation}deg)`; }
direction
:旋转方向,1 表示向左旋转,-1 表示向右旋转。currentRotation += direction * 45
:根据方向调整旋转角度。carousel.style.transform =
rotateY(${currentRotation}deg)``:应用新的旋转角度。
按钮事件监听
prevBtn.addEventListener('click', () => { rotateCarousel(1); }); nextBtn.addEventListener('click', () => { rotateCarousel(-1); });
prevBtn.addEventListener('click', ...)
:点击上一个按钮时,调用rotateCarousel(1)
。nextBtn.addEventListener('click', ...)
:点击下一个按钮时,调用rotateCarousel(-1)
。
3. 触摸滑动支持
触摸事件监听
let touchStartX = 0; let touchEndX = 0; document.addEventListener('touchstart', (e) => { touchStartX = e.touches[0].clientX; }); document.addEventListener('touchend', (e) => { touchEndX = e.changedTouches[0].clientX; handleSwipe(); }); function handleSwipe() { const swipeDistance = touchEndX - touchStartX; if (Math.abs(swipeDistance) > 50) { if (swipeDistance > 0) { rotateCarousel(1); } else { rotateCarousel(-1); } } }
touchStartX
和touchEndX
:记录触摸开始和结束的 X 坐标。document.addEventListener('touchstart', ...)
:触摸开始时记录初始位置。document.addEventListener('touchend', ...)
:触摸结束时记录最终位置并调用handleSwipe
。handleSwipe
:计算滑动距离,如果超过阈值(50 像素),根据滑动方向调用rotateCarousel
。
4. 拖动支持
拖动事件监听
let isDragging = false; let startX; let startRotation; carousel.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; startRotation = currentRotation; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', () => { isDragging = false; document.removeEventListener('mousemove', handleMouseMove); }, { once: true }); }); function handleMouseMove(e) { if (!isDragging) return; const deltaX = e.clientX - startX; const sensitivity = 0.5; // 调整灵敏度 const newRotation = startRotation + (deltaX * sensitivity); carousel.style.transform = `rotateY(${newRotation}deg)`; currentRotation = newRotation; }
isDragging
:是否正在拖动。startX
和startRotation
:记录拖动开始时的 X 坐标和旋转角度。carousel.addEventListener('mousedown', ...)
:鼠标按下时开始拖动。document.addEventListener('mousemove', handleMouseMove)
:鼠标移动时调用handleMouseMove
。document.addEventListener('mouseup', ...)
:鼠标释放时结束拖动。handleMouseMove
:计算鼠标移动的距离,根据灵敏度调整旋转角度并应用新的旋转角度。
5. 自动旋转功能
自动旋转函数 (autoRotateCarousel
)
let autoRotate = true; const autoRotateSpeed = 0.5; function autoRotateCarousel() { if (autoRotate && !isDragging) { currentRotation -= autoRotateSpeed; carousel.style.transform = `rotateY(${currentRotation}deg)`; try { if (!isMusicPlaying) { bgMusic.play().catch(error => { console.log('自动播放失败:', error); }); musicToggle.classList.add('playing'); isMusicPlaying = true; } } catch (error) { console.log('音乐播放出错:', error); } } requestAnimationFrame(autoRotateCarousel); } autoRotateCarousel();
autoRotate
:是否启用自动旋转。autoRotateSpeed
:自动旋转的速度。autoRotateCarousel
:自动旋转函数,每帧调用一次。currentRotation -= autoRotateSpeed
:减少旋转角度以实现自动旋转。carousel.style.transform =
rotateY(${currentRotation}deg)``:应用新的旋转角度。try { ... } catch (error) { ... }
:尝试自动播放背景音乐。requestAnimationFrame(autoRotateCarousel)
:递归调用autoRotateCarousel
以实现每帧更新。
6. 背景音乐控制
音乐控制函数 (toggleMusic
)
const bgMusic = document.getElementById('bgMusic'); const musicToggle = document.getElementById('musicToggle'); let isMusicPlaying = false; function toggleMusic() { if (isMusicPlaying) { bgMusic.pause(); musicToggle.classList.remove('playing'); } else { bgMusic.play(); musicToggle.classList.add('playing'); } isMusicPlaying = !isMusicPlaying; } musicToggle.addEventListener('click', toggleMusic);
bgMusic
:背景音乐的音频元素。musicToggle
:音乐控制按钮的 DOM 元素。isMusicPlaying
:是否正在播放音乐。toggleMusic
:切换音乐播放状态。musicToggle.addEventListener('click', toggleMusic)
:点击音乐控制按钮时调用toggleMusic
。
7. 鼠标悬停时暂停自动旋转
carousel.addEventListener('mouseenter', () => { autoRotate = false; if (isMusicPlaying) { bgMusic.pause(); musicToggle.classList.remove('playing'); isMusicPlaying = false; } }); carousel.addEventListener('mouseleave', () => { autoRotate = true; });
carousel.addEventListener('mouseenter', ...)
:鼠标进入轮播图区域时暂停自动旋转和音乐播放。carousel.addEventListener('mouseleave', ...)
:鼠标离开轮播图区域时恢复自动旋转。
8. 评论展开/折叠功能
document.querySelectorAll('.comment-toggle').forEach(button => { button.addEventListener('click', function () { const commentsSection = this.closest('.post').querySelector('.comments-section'); commentsSection.classList.toggle('hidden'); }); });
document.querySelectorAll('.comment-toggle').forEach(...)
:遍历所有评论按钮。button.addEventListener('click', ...)
:点击评论按钮时,切换评论区域的显示状态。
9. 图片上传和预览
document.getElementById('image-upload').addEventListener('change', function (e) { const preview = document.querySelector('.image-preview'); preview.innerHTML = ''; [...e.target.files].forEach(file => { if (file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = function (e) { const img = document.createElement('img'); img.src = e.target.result; preview.appendChild(img); } reader.readAsDataURL(file); } }); });
document.getElementById('image-upload').addEventListener('change', ...)
:监听文件输入框的变化。preview.innerHTML = ''
:清空预览区域。[...e.target.files].forEach(...)
:遍历选择的文件。if (file.type.startsWith('image/'))
:检查文件类型是否为图片。const reader = new FileReader()
:创建文件读取器。reader.onload = function (e) { ... }
:文件读取完成后,创建图片元素并添加到预览区域。reader.readAsDataURL(file)
:读取文件为 Data URL。
10. 评论提交
document.querySelectorAll('.comment-form').forEach(form => { form.addEventListener('submit', function (e) { e.preventDefault(); const input = this.querySelector('.comment-input'); const commentText = input.value.trim(); if (commentText) { const commentsList = this.closest('.comments-section').querySelector('.comments-list'); const newComment = createCommentElement(commentText); commentsList.insertBefore(newComment, commentsList.firstChild); input.value = ''; } }); });
document.querySelectorAll('.comment-form').forEach(...)
:遍历所有评论表单。form.addEventListener('submit', ...)
:监听表单提交事件。e.preventDefault()
:阻止表单默认提交行为。const input = this.querySelector('.comment-input')
:获取评论输入框。const commentText = input.value.trim()
:获取并修剪评论内容。if (commentText)
:如果评论内容不为空,继续处理。const commentsList = this.closest('.comments-section').querySelector('.comments-list')
:获取评论列表。const newComment = createCommentElement(commentText)
:创建新的评论元素。commentsList.insertBefore(newComment, commentsList.firstChild)
:将新评论插入到评论列表的最前面。input.value = ''
:清空评论输入框。
11. 创建新评论元素
function createCommentElement(text) { const comment = document.createElement('div'); comment.className = 'comment'; comment.innerHTML = ` <img src="avatar.jpg" alt="评论者头像" class="comment-avatar"> <div class="comment-content"> <div class="comment-header"> <span class="comment-author">当前用户</span> <span class="comment-time">刚刚</span> </div> <p class="comment-text">${text}</p> </div> `; return comment; }
const comment = document.createElement('div')
:创建一个新的div
元素。comment.className = 'comment'
:设置评论类名。comment.innerHTML
:设置评论的内容。return comment
:返回创建的评论元素。
12. 点赞功能
document.querySelectorAll('.action-btn').forEach(btn => { if (btn.innerHTML.includes('点赞')) { btn.addEventListener('click', function () { const icon = this.querySelector('i'); if (icon.classList.contains('fas')) { icon.classList.replace('fas', 'far'); this.style.color = '#65676b'; } else { icon.classList.replace('far', 'fas'); this.style.color = '#1877f2'; } }); } });
document.querySelectorAll('.action-btn').forEach(...)
:遍历所有操作按钮。if (btn.innerHTML.includes('点赞'))
:检查按钮是否为点赞按钮。btn.addEventListener('click', ...)
:监听按钮点击事件。const icon = this.querySelector('i')
:获取按钮内的图标元素。if (icon.classList.contains('fas'))
:如果图标类名为fas
,表示已点赞,更换为far
类名并改变颜色。else
:如果图标类名为far
,表示未点赞,更换为fas
类名并改变颜色。
styles.css
1. 基本样式重置
* { margin: 0; padding: 0; box-sizing: border-box; }
*
:选择所有元素。margin: 0; padding: 0;
:移除所有元素的默认外边距和内边距。box-sizing: border-box;
:确保元素的总宽度和高度包括内边距和边框。
2. 页面整体样式
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #f0f2f5; }
font-family
:设置字体系列。background-color
:设置背景颜色。
3. 导航栏样式
.navbar { background-color: #ffffff; padding: 1rem 2rem; box-shadow: 0 2px 4px rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center; position: fixed; width: 100%; top: 0; z-index: 100; } .nav-brand { font-size: 1.5rem; font-weight: bold; color: #1877f2; } .nav-menu { display: flex; gap: 1.5rem; } .nav-item { text-decoration: none; color: #65676b; font-weight: 500; }
.navbar
:导航栏的基本样式,包括背景色、内边距、阴影、固定定位等。.nav-brand
:品牌名称的样式。.nav-menu
:导航菜单的样式,使用flex
布局。.nav-item
:导航项的样式,包括文本装饰、颜色和字体粗细。
4. 容器布局
.container { max-width: 1200px; margin: 80px auto 0; padding: 20px; display: grid; grid-template-columns: 300px 1fr; gap: 20px; }
.container
:主内容容器的样式,包括最大宽度、外边距、内边距、网格布局和列间距。
5. 侧边栏样式
.sidebar { position: sticky; top: 100px; } .profile-card { background: white; padding: 20px; border-radius: 8px; text-align: center; } .avatar { width: 100px; height: 100px; border-radius: 50%; margin-bottom: 10px; }
.sidebar
:侧边栏的样式,使用sticky
定位。.profile-card
:个人资料卡片的样式。.avatar
:头像的样式,包括宽度、高度、圆角和底部外边距。
6. 内容区域样式
.content { background: white; border-radius: 8px; padding: 20px; } .post-form { margin-bottom: 20px; } .post-form textarea { width: 100%; height: 100px; padding: 10px; border: 1px solid #ddd; border-radius: 8px; resize: none; margin-bottom: 10px; } .post-btn { background: #1877f2; color: white; border: none; padding: 8px 20px; border-radius: 6px; cursor: pointer; } .post { border-bottom: 1px solid #ddd; padding: 20px 0; } .post-header { display: flex; align-items: center; margin-bottom: 10px; } .post-avatar { width: 40px; height: 40px; border-radius: 50%; margin-right: 10px; } .post-info h4 { margin-bottom: 4px; } .post-time { color: #65676b; font-size: 0.9rem; } .post-content { margin-bottom: 15px; } .post-actions { display: flex; gap: 15px; } .action-btn { background: none; border: none; color: #65676b; cursor: pointer; font-size: 0.9rem; } .action-btn:hover { color: #1877f2; }
.content
:内容区域的基本样式。.post-form
:帖子表单的样式。.post-form textarea
:帖子表单中的文本区域样式。.post-btn
:发布按钮的样式。.post
:每个帖子的样式。.post-header
:帖子头部的样式。.post-avatar
:帖子作者头像的样式。.post-info h4
:帖子作者信息的样式。.post-time
:帖子发布时间的样式。.post-content
:帖子内容的样式。.post-actions
:帖子操作按钮的样式。.action-btn
:操作按钮的样式,包括悬停效果。
7. 响应式设计
@media (max-width: 768px) { .container { grid-template-columns: 1fr; } .sidebar { display: none; } }
@media (max-width: 768px)
:当屏幕宽度小于等于 768px 时,应用以下样式。.container
:容器变为单列布局。.sidebar
:隐藏侧边栏。
8. 图片上传相关样式
.post-form-actions { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .upload-wrapper { position: relative; } .file-input { display: none; } .upload-btn { display: inline-flex; align-items: center; padding: 8px 15px; background: #f0f2f5; border-radius: 6px; cursor: pointer; color: #65676b; } .upload-btn:hover { background: #e4e6eb; } .image-preview { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px; } .image-preview img { width: 100px; height: 100px; object-fit: cover; border-radius: 8px; }
.post-form-actions
:表单操作区域的样式。.upload-wrapper
:上传按钮的容器样式。.file-input
:隐藏文件输入框。.upload-btn
:上传按钮的样式,包括悬停效果。.image-preview
:图片预览区域的样式。.image-preview img
:预览图片的样式。
9. 评论区样式
.comments-section { margin-top: 15px; border-top: 1px solid #ddd; padding-top: 15px; } .comments-section.hidden { display: none; } .comment-form { display: flex; gap: 10px; margin-bottom: 15px; } .comment-input { flex: 1; padding: 8px 12px; border: 1px solid #ddd; border-radius: 20px; outline: none; } .comment-submit { background: #1877f2; color: white; border: none; padding: 8px 15px; border-radius: 20px; cursor: pointer; } .comment { display: flex; gap: 10px; margin-bottom: 12px; } .comment-avatar { width: 32px; height: 32px; border-radius: 50%; } .comment-content { background: #f0f2f5; padding: 8px 12px; border-radius: 12px; flex: 1; } .comment-header { display: flex; justify-content: space-between; margin-bottom: 4px; } .comment-author { font-weight: 500; } .comment-time { color: #65676b; font-size: 0.8rem; }
.comments-section
:评论区域的样式。.comments-section.hidden
:隐藏评论区域。.comment-form
:评论表单的样式。.comment-input
:评论输入框的样式。.comment-submit
:评论提交按钮的样式。.comment
:每个评论的样式。.comment-avatar
:评论者头像的样式。.comment-content
:评论内容的样式。.comment-header
:评论头部的样式。.comment-author
:评论者的名称样式。.comment-time
:评论时间的样式。
10. 图片展示样式
.post-images { margin: 10px 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; } .post-images img { width: 100%; border-radius: 8px; object-fit: cover; }
.post-images
:帖子图片区域的样式。.post-images img
:帖子图片的样式。
11. 加载动画样式
.loading-spinner { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.8); display: flex; justify-content: center; align-items: center; z-index: 1000; } .loading-spinner.hidden { display: none; } .spinner { width: 50px; height: 50px; border: 5px solid #f3f3f3; border-top: 5px solid #1877f2; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.loading-spinner
:加载动画的容器样式。.loading-spinner.hidden
:隐藏加载动画。.spinner
:加载动画的样式。@keyframes spin
:定义加载动画的旋转效果。
12. 通知样式
.notification-wrapper { position: relative; } .notification-badge { position: absolute; top: -5px; right: -5px; background: #ff4444; color: white; border-radius: 50%; padding: 2px 6px; font-size: 0.7rem; } .notification-dropdown { position: absolute; top: 100%; right: 0; width: 300px; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-top: 10px; z-index: 1000; } .notification-dropdown.hidden { display: none; } .notification-header { padding: 15px; border-bottom: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center; } .mark-all-read { background: none; border: none; color: #1877f2; cursor: pointer; } .notification-list { max-height: 400px; overflow-y: auto; } .notification-item { padding: 12px 15px; border-bottom: 1px solid #f0f2f5; display: flex; align-items: center; gap: 10px; cursor: pointer; } .notification-item:hover { background: #f0f2f5; } .notification-item.unread { background: #e7f3ff; }
.notification-wrapper
:通知图标容器的样式。.notification-badge
:未读通知徽章的样式。.notification-dropdown
:通知下拉菜单的样式。.notification-dropdown.hidden
:隐藏通知下拉菜单。.notification-header
:通知头部的样式。.mark-all-read
:标记全部已读按钮的样式。.notification-list
:通知列表的样式。.notification-item
:每个通知项的样式。.notification-item:hover
:通知项悬停效果。.notification-item.unread
:未读通知项的样式。
13. 无限滚动加载指示器
.infinite-scroll-loader { text-align: center; padding: 20px; display: none; } .infinite-scroll-loader.active { display: block; }
.infinite-scroll-loader
:无限滚动加载指示器的样式。.infinite-scroll-loader.active
:显示加载指示器。
14. 认证页面样式
.auth-container
:认证页面的容器样式。.auth-box
:认证框的样式。.auth-box h2
:认证标题的样式。.auth-form
:认证表单的样式。.form-group
:表单组的样式。.form-group label
:表单标签的样式。.form-group input
:表单输入框的样式。.auth-btn
:认证按钮的样式,包括悬停效果。.auth-link
:链接的样式。.auth-link a
:链接的样式,包括悬停效果。
后端代码(服务器)
server.js
1. 引入依赖
const express = require('express'); const WebSocket = require('ws'); const cors = require('cors'); const bodyParser = require('body-parser');
express
:用于构建 Web 服务器。WebSocket
:用于实时通信。cors
:用于处理跨域请求。bodyParser
:用于解析请求体。
2. 初始化 Express 应用
const app = express(); const port = 3000;
app
:Express 应用实例。port
:服务器监听的端口。
3. 配置中间件
app.use(cors()); app.use(bodyParser.json()); app.use(express.static('public'));
cors()
:允许跨域请求。bodyParser.json()
:解析 JSON 格式的请求体。express.static('public')
:提供静态文件服务,路径为public
目录。
4. 创建 WebSocket 服务器
const wss = new WebSocket.Server({ port: 8080 }); const clients = new Set(); wss.on('connection', (ws) => { clients.add(ws); ws.on('close', () => { clients.delete(ws); }); });
wss
:WebSocket 服务器实例。clients
:存储所有连接的客户端。wss.on('connection', ...)
:监听新的 WebSocket 连接,并将客户端添加到clients
集合中。ws.on('close', ...)
:监听客户端断开连接,并从clients
集合中移除。
5. 模拟数据库
let posts = []; let notifications = [];
posts
:存储帖子的数据。notifications
:存储通知的数据。
6. API 路由
获取帖子列表
app.get('/api/posts', (req, res) => { try { const page = parseInt(req.query.page) || 1; const pageSize = 10; const start = (page - 1) * pageSize; const end = start + pageSize; const paginatedPosts = posts.slice(start, end); res.json({ posts: paginatedPosts, hasMore: end < posts.length }); } catch (error) { res.status(500).json({ error: '获取帖子失败', details: error.message }); } });
/api/posts
:GET 请求,用于获取帖子列表。page
:当前页码,默认为 1。pageSize
:每页显示的帖子数量,默认为 10。start
和end
:计算分页的起始和结束索引。paginatedPosts
:分页后的帖子列表。hasMore
:是否还有更多帖子。
创建新帖子
app.post('/api/posts', async (req, res) => { try { const { content, images } = req.body; if (!content) { return res.status(400).json({ error: '内容不能为空' }); } const newPost = { id: Date.now(), content, images: images || [], username: '当前用户', avatar: 'avatar.jpg', timestamp: new Date(), likes: 0, comments: [] }; posts.unshift(newPost); const notification = { type: 'new_post', message: `${newPost.username} 发布了新动态`, timestamp: new Date(), avatar: newPost.avatar }; broadcastNotification(notification); res.status(201).json(newPost); } catch (error) { res.status(500).json({ error: '发布失败', details: error.message }); } });
/api/posts
:POST 请求,用于创建新帖子。content
和images
:从请求体中获取帖子内容和图片。newPost
:新创建的帖子对象。posts.unshift(newPost)
:将新帖子添加到帖子列表的开头。broadcastNotification
:向所有连接的客户端广播新帖子的通知。res.status(201).json(newPost)
:返回新创建的帖子。
添加评论
app.post('/api/posts/:postId/comments', (req, res) => { try { const { postId } = req.params; const { content } = req.body; if (!content) { return res.status(400).json({ error: '评论内容不能为空' }); } const post = posts.find(p => p.id === parseInt(postId)); if (!post) { return res.status(404).json({ error: '帖子不存在' }); } const newComment = { id: Date.now(), content, username: '当前用户', avatar: 'avatar.jpg', timestamp: new Date() }; post.comments.push(newComment); const notification = { type: 'new_comment', message: `${newComment.username} 评论了你的动态`, timestamp: new Date(), avatar: newComment.avatar }; broadcastNotification(notification); res.status(201).json(newComment); } catch (error) { res.status(500).json({ error: '评论失败', details: error.message }); } });
/api/posts/:postId/comments
:POST 请求,用于添加评论。postId
:从 URL 参数中获取帖子 ID。content
:从请求体中获取评论内容。post
:找到对应的帖子。newComment
:新创建的评论对象。post.comments.push(newComment)
:将新评论添加到帖子的评论列表中。broadcastNotification
:向所有连接的客户端广播新评论的通知。res.status(201).json(newComment)
:返回新创建的评论。
点赞/取消点赞
app.post('/api/posts/:postId/like', (req, res) => { try { const { postId } = req.params; const post = posts.find(p => p.id === parseInt(postId)); if (!post) { return res.status(404).json({ error: '帖子不存在' }); } post.likes = post.likes + 1; res.json({ likes: post.likes }); } catch (error) { res.status(500).json({ error: '操作失败', details: error.message }); } });
/api/posts/:postId/like
:POST 请求,用于点赞。postId
:从 URL 参数中获取帖子 ID。post
:找到对应的帖子。post.likes = post.likes + 1
:增加帖子的点赞数。res.json({ likes: post.likes })
:返回更新后的点赞数。
7. 广播通知
function broadcastNotification(notification) { clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(notification)); } }); }
broadcastNotification
:函数用于向所有连接的客户端广播通知。clients.forEach(client => ...)
:遍历所有客户端。client.readyState === WebSocket.OPEN
:检查客户端是否处于打开状态。client.send(JSON.stringify(notification))
:将通知发送给客户端。
8. 错误处理中间件
app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: '服务器错误', details: process.env.NODE_ENV === 'development' ? err.message : '请稍后重试' }); });
app.use((err, req, res, next) => ...)
:全局错误处理中间件。console.error(err.stack)
:记录错误堆栈。res.status(500).json(...)
:返回 500 状态码和错误信息。
9. 启动服务器
app.listen(port, () => { console.log(`服务器运行在 http://localhost:${port}`); });
app.listen(port, ...)
:启动 Express 服务器,监听指定端口。console.log(...)
:打印服务器启动信息。
auth.js
1. 引入依赖
const express = require('express'); const router = express.Router(); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs');
express
:用于构建 Web 服务器。express.Router()
:创建一个路由对象。jwt
:用于生成和验证 JSON Web Tokens (JWT)。bcryptjs
:用于密码加密和验证。
2. 模拟用户数据库
const users = new Map(); const JWT_SECRET = 'your-secret-key'; // 实际应用中应该使用环境变量
users
:使用Map
数据结构来存储用户数据,键为用户 ID,值为用户对象。JWT_SECRET
:用于生成和验证 JWT 的密钥,实际应用中应使用环境变量来管理。
3. 注册路由
router.post('/register', async (req, res) => { try { const { username, password, email } = req.body; // 验证输入 if (!username || !password || !email) { return res.status(400).json({ error: '所有字段都是必填的' }); } // 检查用户是否已存在 if (Array.from(users.values()).some(user => user.email === email)) { return res.status(400).json({ error: '该邮箱已被注册' }); } // 加密密码 const hashedPassword = await bcrypt.hash(password, 10); // 创建新用户 const newUser = { id: Date.now().toString(), username, email, password: hashedPassword, avatar: `https://api.multiavatar.com/${username}.png` }; users.set(newUser.id, newUser); // 生成 JWT const token = jwt.sign({ userId: newUser.id }, JWT_SECRET, { expiresIn: '24h' }); res.status(201).json({ token, user: { id: newUser.id, username: newUser.username, email: newUser.email, avatar: newUser.avatar } }); } catch (error) { res.status(500).json({ error: '注册失败', details: error.message }); } });
/register
:POST 请求,用于用户注册。username
、password
和email
:从请求体中获取用户信息。if (!username || !password || !email)
:验证输入字段是否为空。Array.from(users.values()).some(user => user.email === email)
:检查邮箱是否已注册。await bcrypt.hash(password, 10)
:使用 bcrypt 加密密码。newUser
:创建新的用户对象。users.set(newUser.id, newUser)
:将新用户添加到用户数据库中。jwt.sign(...)
:生成 JWT。res.status(201).json(...)
:返回注册成功的信息,包括 JWT 和用户信息。
4. 登录路由
router.post('/login', async (req, res) => { try { const { email, password } = req.body; // 查找用户 const user = Array.from(users.values()).find(u => u.email === email); if (!user) { return res.status(401).json({ error: '用户名或密码错误' }); } // 验证密码 const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) { return res.status(401).json({ error: '用户名或密码错误' }); } // 生成 JWT const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '24h' }); res.json({ token, user: { id: user.id, username: user.username, email: user.email, avatar: user.avatar } }); } catch (error) { res.status(500).json({ error: '登录失败', details: error.message }); } });
/login
:POST 请求,用于用户登录。email
和password
:从请求体中获取用户信息。Array.from(users.values()).find(u => u.email === email)
:查找用户。await bcrypt.compare(password, user.password)
:验证密码。jwt.sign(...)
:生成 JWT。res.json(...)
:返回登录成功的信息,包括 JWT 和用户信息。
5. 验证中间件
const authMiddleware = (req, res, next) => { try { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: '未授权' }); } const decoded = jwt.verify(token, JWT_SECRET); req.userId = decoded.userId; next(); } catch (error) { res.status(401).json({ error: '无效的令牌' }); } };
authMiddleware
:验证 JWT 的中间件。req.headers.authorization?.split(' ')[1]
:从请求头中提取 JWT。jwt.verify(token, JWT_SECRET)
:验证 JWT。req.userId = decoded.userId
:将用户 ID 添加到请求对象中。next()
:调用下一个中间件或路由处理器。
6. 获取当前用户信息
router.get('/me', authMiddleware, (req, res) => { const user = users.get(req.userId); if (!user) { return res.status(404).json({ error: '用户不存在' }); } res.json({ id: user.id, username: user.username, email: user.email, avatar: user.avatar }); });
/me
:GET 请求,用于获取当前用户信息。authMiddleware
:使用验证中间件确保请求已授权。users.get(req.userId)
:从用户数据库中获取用户信息。res.json(...)
:返回用户信息。
7. 导出模块
module.exports = { router, authMiddleware };
module.e
xports
:导出路由对象和验证中间件,以便在其他文件中使用。
根目录
.env
环境变量配置
PORT=3000 WS_PORT=8080 JWT_SECRET=your-secret-key-here NODE_ENV=development
变量详解
-
PORT
- 值:
3000
- 用途:定义 HTTP 服务器监听的端口号。在这个例子中,HTTP 服务器将在
3000
端口上运行。
- 值:
-
WS_PORT
- 值:
8080
- 用途:定义 WebSocket 服务器监听的端口号。在这个例子中,WebSocket 服务器将在
8080
端口上运行。
- 值:
-
JWT_SECRET
- 值:
your-secret-key-here
- 用途:用于生成和验证 JSON Web Tokens (JWT) 的密钥。这个密钥应该是保密的,不应该硬编码在代码中,而应该通过环境变量传递。
- 值:
-
NODE_ENV
- 值:
development
- 用途:定义应用程序的运行环境。常见的值有
development
(开发环境)、production
(生产环境)和test
(测试环境)。不同的环境可能会有不同的配置和行为
- 值:
package.json
基本信息
{ "name": "social-media-platform", "version": "1.0.0", "description": "社交媒体平台", "main": "server.js" }
name
:项目的名称,这里是social-media-platform
。version
:项目的版本号,这里是1.0.0
。description
:项目的描述,这里是社交媒体平台
。main
:项目的入口文件,这里是server.js
。
脚本命令
"scripts": { "start": "node server.js", "dev": "nodemon server.js" }
start
:运行node server.js
命令启动服务器。dev
:运行nodemon server.js
命令启动服务器,并在文件更改时自动重启服务器。nodemon
是一个开发工具,用于自动重启 Node.js 应用程序。
依赖包
"dependencies": { "express": "^4.17.1", "ws": "^8.2.3", "cors": "^2.8.5", "body-parser": "^1.19.0", "jsonwebtoken": "^8.5.1", "bcryptjs": "^2.4.3", "cookie-parser": "^1.4.5" }
express
:一个流行的 Node.js 框架,用于构建 Web 应用程序。ws
:一个 WebSocket 库,用于实现实时通信。cors
:一个中间件,用于处理跨域请求。body-parser
:一个中间件,用于解析请求体。jsonwebtoken
:一个库,用于生成和验证 JSON Web Tokens (JWT)。bcryptjs
:一个库,用于密码加密和验证。cookie-parser
:一个中间件,用于解析 Cookie。
开发依赖包
"devDependencies": { "nodemon": "^2.0.15" }
nodemon
:一个开发工具,用于监视 Node.js 应用程序的文件变化并自动重启服务器。