【精选优质专栏推荐】
- 《AI 技术前沿》 —— 紧跟 AI 最新趋势与应用
- 《网络安全新手快速入门(附漏洞挖掘案例)》 —— 零基础安全入门必看
- 《BurpSuite 入门教程(附实战图文)》 —— 渗透测试必备工具详解
- 《网安渗透工具使用教程(全)》 —— 一站式工具手册
- 《CTF 新手入门实战教程》 —— 从题目讲解到实战技巧
- 《前后端项目开发(新手必知必会)》 —— 实战驱动快速上手
每个专栏均配有案例与图文讲解,循序渐进,适合新手与进阶学习者,欢迎订阅。
文章目录

文章概述
本文介绍了一个采用现代化前后端分离架构的留言板系统的完整设计与实现过程。该系统前端采用React框架配合Vite构建工具,后端使用Python Flask框架提供RESTful API服务,数据持久化采用SQLite轻量级数据库。系统实现了富文本编辑、图片上传、层级化评论等核心功能,并在界面设计上采用灰白系极简风格,体现了现代Web应用的设计理念。文章将从技术选型、架构设计、核心模块实现、数据流转机制等多个维度进行深入剖析,为读者提供一个完整的前后端分离项目实践案例。
引言
在当今互联网应用开发领域,前后端分离架构已成为主流的技术方案。这种架构模式将用户界面与业务逻辑完全解耦,使得前端专注于用户体验优化,后端专注于数据处理与业务逻辑实现,两者通过标准化的HTTP接口进行通信。这种分离不仅提升了开发效率,更为系统的横向扩展和技术栈升级提供了极大的灵活性。
本文所述的留言板系统正是基于这一理念构建的典型案例。系统选用React作为前端框架,其虚拟DOM机制和组件化思想能够有效提升界面渲染性能和代码复用性。后端采用Flask这一轻量级Python Web框架,其简洁的API设计和强大的扩展性使得快速构建RESTful服务成为可能。数据层面选择SQLite嵌入式数据库,避免了复杂的数据库配置,特别适合中小型应用的快速部署。
系统在功能设计上涵盖了现代社交应用的核心要素,包括用户留言发布、富文本内容编辑、多媒体附件上传、层级化评论互动等。在技术实现上充分考虑了跨域资源共享、文件安全存储、数据库事务处理、前端状态管理等工程实践中的关键问题。整个系统从架构设计到代码实现都遵循业界最佳实践,为开发者提供了一个可以直接应用于生产环境的参考模板。
技术方案
整体架构设计
系统采用经典的三层架构模式,分为表现层、业务逻辑层和数据持久层。表现层由React构建的单页应用承担,运行在客户端浏览器环境中,负责用户交互和界面渲染。业务逻辑层由Flask应用实现,部署在服务器端,提供标准的RESTful API接口供前端调用。数据持久层使用SQLite关系型数据库,通过Python内置的sqlite3模块进行操作,实现数据的持久化存储。
在技术选型上,前端使用Vite作为构建工具,相比传统的Webpack,Vite基于ES模块的开发服务器启动速度更快,热模块替换性能更优。React-Quill作为富文本编辑器组件,基于Quill.js封装,提供了丰富的文本格式化功能。Axios作为HTTP客户端,其拦截器机制和Promise API使得异步请求处理更加优雅。后端Flask框架配合Flask-CORS扩展解决跨域问题,Werkzeug工具库提供了安全的文件上传处理机制。
前后端通信机制
系统采用基于HTTP协议的RESTful API进行前后端通信。后端Flask应用监听5000端口,前端Vite开发服务器运行在3000端口。当前端发起API请求时,浏览器会因同源策略限制产生跨域问题,系统通过在Flask应用中配置CORS解决这一问题。具体实现是在Flask应用初始化时调用CORS(app),允许来自所有源的跨域请求。

数据传输格式根据请求类型有所区别。对于简单的查询操作,前端通过GET请求获取JSON格式数据。对于涉及文件上传的操作,采用multipart/form-data编码格式,将文本字段和文件对象一并提交。后端接收到请求后,通过request.form获取表单字段,通过request.files获取上传文件,处理完成后返回包含success标识和data或error信息的JSON响应。
数据库设计理念
数据库设计采用关系型建模方法,设置messages和comments两张核心表。messages表存储主留言信息,包含自增主键id、用户姓名name、电子邮箱email、富文本内容content、图片路径image_path以及时间戳created_at。comments表存储评论信息,结构与messages表类似,但增加了message_id外键字段用于关联父留言,实现留言与评论的一对多关系。
外键约束设置为ON DELETE CASCADE,确保当主留言被删除时,其关联的所有评论也会被自动删除,维护数据的参照完整性。时间戳字段使用TIMESTAMP类型配合DEFAULT CURRENT_TIMESTAMP约束,实现时间的自动记录。在查询时使用datetime函数配合localtime参数进行时区转换,确保时间显示符合本地时区。
文件存储策略
图片文件采用服务器本地文件系统存储方案。后端在项目根目录下创建uploads目录作为统一的文件存储位置。当用户上传图片时,系统首先验证文件扩展名是否在允许列表内,然后使用uuid.uuid4().hex生成唯一的文件名,避免文件名冲突。文件保存成功后,仅在数据库中记录文件名,而非完整路径,降低系统迁移时的路径依赖问题。

前端访问图片时,通过Flask提供的静态文件路由/uploads/<filename>获取文件内容。Flask内部使用send_from_directory函数安全地返回文件,该函数会验证路径合法性,防止目录遍历攻击。图片URL采用绝对路径形式http://localhost:5000/uploads/文件名,确保在不同环境下都能正确访问。

项目文件结构
A/
├── backend/ # 后端Python服务
│ ├── app.py # Flask主应用
│ ├── requirements.txt # Python依赖配置
│ └── messages.db # SQLite数据库(运行后自动生成)
│
├── frontend/ # 前端React应用
│ ├── src/
│ │ ├── components/ # React组件
│ │ │ ├── MessageForm.jsx # 留言表单组件
│ │ │ ├── MessageForm.css # 表单样式
│ │ │ ├── MessageList.jsx # 留言列表组件
│ │ │ ├── MessageList.css # 列表样式
│ │ │ ├── MessageItem.jsx # 单条留言组件
│ │ │ ├── MessageItem.css # 留言样式
│ │ │ ├── CommentForm.jsx # 评论表单组件
│ │ │ └── CommentForm.css # 评论样式
│ │ ├── App.jsx # 主应用组件
│ │ ├── App.css # 应用样式
│ │ ├── main.jsx # React入口
│ │ └── index.css # 全局样式
│ ├── index.html # HTML模板
│ ├── package.json # 前端依赖配置
│ └── vite.config.js # Vite构建配置
│
├── uploads/ # 图片上传目录
│ └── .gitkeep # 目录占位文件
│
├── README.md # 详细项目文档
└── .gitignore # Git忽略配置
流程介绍
系统启动流程
系统的启动过程分为后端服务初始化和前端应用加载两个阶段。后端服务启动时,Flask应用首先执行init_db函数,该函数连接SQLite数据库并创建必要的数据表结构。若表已存在则跳过创建步骤,实现幂等性保证。随后检查uploads目录是否存在,不存在则自动创建。最后调用app.run方法启动WSGI服务器,监听5000端口并开启调试模式。整个过程在终端输出详细的初始化信息,便于开发者监控服务状态。
前端应用启动时,Vite开发服务器首先解析package.json中的依赖配置,安装必要的npm包。然后读取vite.config.js配置文件,根据插件配置启动React开发服务器。服务器监听3000端口并自动打开浏览器,加载index.html文件。浏览器解析HTML后,通过script标签加载main.jsx入口文件,React执行渲染逻辑,将App组件挂载到DOM的root节点。App组件挂载后立即执行useEffect钩子,发起GET请求获取留言数据,完成首屏渲染。

数据交互流程
用户发表留言的完整流程体现了前后端协作的典型模式。用户在MessageForm组件中填写表单并点击提交按钮,触发handleSubmit事件处理函数。该函数首先在客户端进行表单验证,包括必填项检查和邮箱格式验证。验证通过后,构造FormData对象,将文本字段和可选的图片文件添加进去。通过Axios发起POST请求,请求头设置为multipart/form-data,数据通过HTTP请求体发送到后端。
后端Flask应用的create_message函数接收到请求,从request.form中提取name、email、content字段,从request.files中获取image文件对象。执行服务端验证,确认必填字段非空。若上传了图片,生成唯一文件名并保存到uploads目录,记录文件名到image_path变量。构造SQL INSERT语句,使用参数化查询防止SQL注入,将数据插入messages表。事务提交成功后,获取新插入记录的主键id,封装成JSON响应返回前端。
前端收到成功响应后,调用loadMessages函数重新请求留言列表,获取包含新留言的完整数据。React检测到messages状态变化,触发组件重新渲染,MessageList组件遍历messages数组,为每条留言创建MessageItem组件实例。用户界面更新,新发表的留言出现在列表顶部,整个交互过程流畅无感知延迟。
评论互动流程
评论功能的实现展示了状态管理和组件通信的技巧。用户点击某条留言的回复按钮,触发MessageItem组件中的setShowReplyForm状态更新,将回复表单的可见性切换为true。React重新渲染该组件,CommentForm组件被条件渲染挂载到DOM树。用户在CommentForm中填写评论内容,提交时调用onSubmit回调函数,该函数由App组件通过props层层传递。
回调函数handleCommentSubmit在App组件中定义,接收messageId和formData两个参数。函数内部将messageId追加到FormData对象,标识该评论所属的父留言。发起POST请求到/api/comments端点,后端create_comment函数处理请求,执行与留言创建类似的流程,区别在于需要验证message_id字段并插入comments表。数据库通过外键约束确保message_id对应的留言存在,维护数据一致性。
评论创建成功后,前端重新加载留言列表,后端查询时通过子查询关联comments表,将每条留言的评论数组一并返回。MessageItem组件接收到更新后的message对象,包含comments数组。组件遍历该数组,渲染评论列表,每条评论显示作者信息、内容和时间。用户看到自己的评论出现在对应留言下方,并且回复表单自动关闭,通过setShowReplyForm(false)实现状态复位。

核心内容解析
后端API接口设计与实现
后端Flask应用的核心在于RESTful API的设计,遵循资源导向的架构风格。系统定义了messages和comments两个核心资源,分别对应留言和评论实体。对于messages资源,提供GET方法获取资源列表和POST方法创建新资源。GET /api/messages返回所有留言的JSON数组,每个留言对象包含基本字段和嵌套的comments数组,实现一次请求获取完整数据。POST /api/messages接收multipart表单数据,创建新留言并返回生成的id。
评论资源的API设计更加简洁,仅提供POST /api/comments接口用于创建评论。评论作为留言的从属资源,不单独提供查询接口,而是在查询留言时自动关联返回。这种设计减少了API数量,降低了前端调用的复杂度。接口参数中的message_id字段建立了评论与留言的关联关系,后端通过外键约束确保数据完整性。
数据库操作采用参数化查询防止SQL注入攻击,所有用户输入作为查询参数传入,而非直接拼接SQL字符串。连接使用row_factory设置为sqlite3.Row,使得查询结果可以像字典一样通过列名访问,提升代码可读性。事务管理遵循打开连接、执行操作、提交事务、关闭连接的标准流程,异常情况下通过try-except捕获并返回错误信息,避免程序崩溃。

图片上传的安全性处理至关重要,系统通过多层验证机制保障安全。首先检查文件扩展名是否在白名单内,拒绝可执行文件和脚本文件。使用UUID生成随机文件名,防止恶意用户通过特殊文件名进行路径遍历攻击。设置文件大小限制为16MB,通过Flask配置项MAX_CONTENT_LENGTH实现,超过限制的请求会被自动拒绝。文件保存使用Werkzeug的secure_filename函数过滤危险字符,进一步提升安全性。

前端组件化架构与状态管理
前端采用React函数式组件和Hooks进行开发,充分利用了React的组件化思想。App作为根组件,负责全局状态管理和子组件协调。组件内部使用useState钩子管理三个核心状态,messages数组存储留言数据,loading布尔值表示加载状态,error字符串记录错误信息。这些状态的变化会触发组件重新渲染,实现用户界面的响应式更新。
useEffect钩子在组件挂载时执行,其依赖数组为空数组,确保仅执行一次。钩子内部调用loadMessages函数,该函数使用async/await语法处理异步请求。请求成功时更新messages状态,失败时设置error状态,finally块中更新loading状态。这种模式将异步逻辑封装在独立函数中,提升了代码的可维护性和可测试性。
组件间通信采用props传递和回调函数模式。App组件将handleMessageSubmit和handleCommentSubmit作为props传递给子组件,子组件通过调用这些函数触发父组件的状态更新。这种单向数据流模式使得数据流转路径清晰,便于追踪状态变化。MessageItem组件接收message对象和onCommentSubmit回调,内部维护showReplyForm局部状态,控制回复表单的显示隐藏,体现了状态的分层管理原则。
表单处理逻辑集中在MessageForm和CommentForm组件中,两者结构相似但功能独立。组件使用受控组件模式,表单元素的value属性绑定到组件状态,onChange事件更新状态,确保React状态与DOM状态保持同步。表单验证在客户端执行,包括必填项检查、邮箱格式验证、图片大小和类型验证。验证失败时通过setError更新错误状态,界面显示错误提示,阻止表单提交。验证通过后构造FormData对象,调用父组件传入的onSubmit回调函数,实现数据提交。
富文本编辑器的集成使用React-Quill组件,该组件封装了Quill.js编辑器。通过modules属性配置工具栏,定义可用的格式化选项。编辑器内容变化时触发onChange回调,传入HTML格式的内容字符串,组件将其存储到content状态。提交时content作为表单字段发送到后端,后端直接存储HTML内容。
样式系统与视觉设计
样式系统采用CSS Modules模式,每个组件对应一个CSS文件,样式作用域限定在组件内部。全局样式定义在index.css中,包含重置样式、通用类和动画定义。组件样式使用BEM命名规范,类名结构清晰,避免样式冲突。样式属性使用现代CSS特性,如Flexbox和Grid布局,实现响应式设计。
配色方案严格遵循灰白系定义,主背景色使用#f5f5f5浅灰,营造柔和视觉效果。卡片背景使用纯白#ffffff,与背景形成对比突出内容。文字颜色采用#2c2c2c炭黑作为主色,#666666深灰作为次要色,#999999中灰作为辅助色,形成清晰的视觉层次。边框和分隔线使用#e0e0e0浅灰,保持界面简洁统一。
交互反馈通过过渡动画实现,所有可交互元素设置transition属性,时长为0.3秒,缓动函数为ease。按钮悬停时改变背景色和添加阴影,视觉上提示可点击。加载状态使用旋转动画,通过CSS animation实现无限循环旋转。淡入动画应用于留言卡片,通过keyframes定义从透明到不透明、从下往上移动的效果,提升用户体验。
响应式设计通过媒体查询实现,断点设置在768px。移动端视图调整容器内边距、字体大小、布局方向等,确保在小屏幕上的可读性和可操作性。图片设置max-width为100%,自适应容器宽度。表单在移动端采用单列布局,桌面端采用双列布局,充分利用屏幕空间。
实践代码
后端核心代码实现
"""
Flask后端应用核心实现
展示数据库初始化、API路由定义和请求处理逻辑
"""
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
import sqlite3
import os
import uuid
# 创建Flask应用实例
app = Flask(__name__)
# 配置CORS允许跨域请求,这是前后端分离架构的必要配置
CORS(app)
# 配置文件上传相关参数
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'uploads')
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 限制最大16MB
# 数据库文件路径定义
DATABASE = os.path.join(os.path.dirname(__file__), 'messages.db')
def init_db():
"""
数据库初始化函数
创建messages和comments两张表,建立外键关联
使用IF NOT EXISTS确保幂等性,多次执行不会报错
"""
conn = sqlite3.connect(DATABASE)
cursor = conn.cursor()
# 创建留言主表,包含自增主键和时间戳字段
cursor.execute('''
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL,
content TEXT NOT NULL,
image_path TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 创建评论表,通过message_id外键关联留言表
# ON DELETE CASCADE确保删除留言时自动删除相关评论
cursor.execute('''
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_id INTEGER NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
content TEXT NOT NULL,
image_path TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
)
''')
conn.commit()
conn.close()
def get_db():
"""
获取数据库连接的工厂函数
设置row_factory使结果可以通过列名访问,提升代码可读性
"""
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
return conn
@app.route('/api/messages', methods=['GET'])
def get_messages():
"""
获取所有留言的API接口
采用嵌套查询方式,为每条留言关联其评论列表
返回JSON格式数据,包含success标识和data数组
"""
try:
conn = get_db()
cursor = conn.cursor()
# 查询所有留言,使用datetime函数转换时间为本地时区
cursor.execute('''
SELECT id, name, email, content, image_path,
datetime(created_at, 'localtime') as created_at
FROM messages
ORDER BY created_at DESC
''')
messages = cursor.fetchall()
result = []
# 为每条留言查询其关联的评论
for message in messages:
message_dict = dict(message)
cursor.execute('''
SELECT id, name, email, content, image_path,
datetime(created_at, 'localtime') as created_at
FROM comments
WHERE message_id = ?
ORDER BY created_at ASC
''', (message['id'],))
comments = cursor.fetchall()
# 将评论列表添加到留言对象中,实现一次查询返回完整数据
message_dict['comments'] = [dict(comment) for comment in comments]
result.append(message_dict)
conn.close()
return jsonify({'success': True, 'data': result})
except Exception as e:
# 异常处理,返回500状态码和错误信息
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/messages', methods=['POST'])
def create_message():
"""
创建新留言的API接口
接收multipart/form-data格式数据,包含文本字段和可选图片文件
使用参数化查询防止SQL注入,使用UUID生成唯一文件名
"""
try:
# 从表单中提取字段,request.form用于文本字段
name = request.form.get('name')
email = request.form.get('email')
content = request.form.get('content')
# 服务端验证必填字段
if not all([name, email, content]):
return jsonify({'success': False, 'error': '姓名、邮箱和内容不能为空'}), 400
image_path = None
# 处理文件上传,request.files用于文件对象
if 'image' in request.files:
file = request.files['image']
if file and file.filename:
# 验证文件扩展名
ext = file.filename.rsplit('.', 1)[1].lower()
if ext in ALLOWED_EXTENSIONS:
# 生成UUID作为文件名,避免冲突和安全风险
filename = f"{uuid.uuid4().hex}.{ext}"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
image_path = filename
# 插入数据库,使用占位符?防止SQL注入
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO messages (name, email, content, image_path)
VALUES (?, ?, ?, ?)
''', (name, email, content, image_path))
conn.commit()
# 获取插入记录的主键id
message_id = cursor.lastrowid
conn.close()
return jsonify({'success': True, 'message_id': message_id})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/uploads/<filename>')
def uploaded_file(filename):
"""
静态文件服务接口
使用send_from_directory安全返回文件,防止路径遍历攻击
"""
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
if __name__ == '__main__':
# 确保上传目录存在
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
# 初始化数据库
init_db()
# 启动Flask开发服务器
app.run(host='0.0.0.0', port=5000, debug=True)
前端核心代码实现
/**
* App主组件实现
* 演示React状态管理、异步数据获取和组件通信
*/
import React, { useState, useEffect } from 'react'
import MessageForm from './components/MessageForm'
import MessageList from './components/MessageList'
import axios from 'axios'
import './App.css'
// 配置后端API基础URL
const API_BASE_URL = 'http://localhost:5000/api'
function App() {
// 使用useState钩子定义组件状态
// messages存储留言数组,loading表示加载状态,error存储错误信息
const [messages, setMessages] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
/**
* 异步函数,从后端获取留言数据
* 使用try-catch处理异常,finally确保loading状态更新
*/
const loadMessages = async () => {
try {
setLoading(true)
setError(null)
// 发起GET请求,axios返回Promise对象
const response = await axios.get(`${API_BASE_URL}/messages`)
if (response.data.success) {
// 更新状态触发组件重新渲染
setMessages(response.data.data)
} else {
setError('加载留言失败')
}
} catch (err) {
console.error('加载留言错误:', err)
setError('无法连接到服务器,请确保后端服务已启动')
} finally {
// 无论成功失败都要关闭加载状态
setLoading(false)
}
}
/**
* useEffect钩子,在组件挂载时执行
* 依赖数组为空,确保仅执行一次
*/
useEffect(() => {
loadMessages()
}, [])
/**
* 处理留言提交的回调函数
* 接收FormData对象,发起POST请求
* 返回包含success标识的对象,供子组件判断
*/
const handleMessageSubmit = async (formData) => {
try {
// 设置请求头为multipart/form-data
const response = await axios.post(`${API_BASE_URL}/messages`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
if (response.data.success) {
// 提交成功后重新加载数据
await loadMessages()
return { success: true }
} else {
return { success: false, error: response.data.error }
}
} catch (err) {
console.error('提交留言错误:', err)
return { success: false, error: '提交失败,请稍后重试' }
}
}
/**
* 处理评论提交的回调函数
* 接收留言ID和FormData,将ID添加到表单数据中
*/
const handleCommentSubmit = async (messageId, formData) => {
try {
// 将留言ID追加到FormData对象
formData.append('message_id', messageId)
const response = await axios.post(`${API_BASE_URL}/comments`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
if (response.data.success) {
await loadMessages()
return { success: true }
} else {
return { success: false, error: response.data.error }
}
} catch (err) {
console.error('提交评论错误:', err)
return { success: false, error: '提交失败,请稍后重试' }
}
}
return (
<div className="app">
<header className="app-header">
<div className="container">
<h1 className="app-title">AI留言板</h1>
<p className="app-subtitle">分享你的想法,与他人互动交流</p>
</div>
</header>
<main className="app-main">
<div className="container">
{/* 留言表单组件,通过props传递提交回调 */}
<section className="form-section">
<h2 className="section-title">发表留言</h2>
<MessageForm onSubmit={handleMessageSubmit} />
</section>
<section className="messages-section">
<h2 className="section-title">
全部留言 {!loading && `(${messages.length})`}
</h2>
{/* 条件渲染:根据loading和error状态显示不同内容 */}
{loading && (
<div className="loading-container">
<div className="loading-spinner"></div>
<p>加载中...</p>
</div>
)}
{error && (
<div className="error-message">
<p>{error}</p>
<button onClick={loadMessages} className="retry-button">
重试
</button>
</div>
)}
{/* 留言列表组件,传递数据和回调函数 */}
{!loading && !error && (
messages.length > 0 ? (
<MessageList
messages={messages}
onCommentSubmit={handleCommentSubmit}
/>
) : (
<div className="empty-state">
<p>还没有留言,快来发表第一条吧!</p>
</div>
)
)}
</section>
</div>
</main>
<footer className="app-footer">
<div className="container">
<p>© 2026 AI留言板系统 - 简约而不简单</p>
</div>
</footer>
</div>
)
}
export default App
/**
* MessageItem组件实现
* 展示单条留言及其评论,管理回复表单状态
*/
import React, { useState } from 'react'
import CommentForm from './CommentForm'
import './MessageItem.css'
const IMAGE_BASE_URL = 'http://localhost:5000/uploads'
function MessageItem({ message, onCommentSubmit }) {
// 局部状态控制回复表单显示
const [showReplyForm, setShowReplyForm] = useState(false)
/**
* 时间格式化函数
* 将ISO格式时间字符串转换为本地化显示格式
*/
const formatDate = (dateString) => {
if (!dateString) return ''
try {
const date = new Date(dateString)
// 使用toLocaleString实现国际化时间显示
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).replace(/\//g, '-')
} catch (e) {
return dateString
}
}
/**
* 评论提交处理函数
* 调用父组件传入的回调,成功后关闭表单
*/
const handleCommentSubmit = async (formData) => {
const result = await onCommentSubmit(message.id, formData)
if (result.success) {
setShowReplyForm(false)
}
return result
}
return (
<div className="message-item fade-in">
<div className="message-header">
<div className="message-author">
{/* 头像显示姓名首字母,使用toUpperCase转大写 */}
<div className="author-avatar">
{message.name.charAt(0).toUpperCase()}
</div>
<div className="author-info">
<h3 className="author-name">{message.name}</h3>
<p className="author-email">{message.email}</p>
</div>
</div>
<div className="message-time">
{formatDate(message.created_at)}
</div>
</div>
{/* 使用dangerouslySetInnerHTML渲染HTML内容 */}
<div
className="message-content"
dangerouslySetInnerHTML={{ __html: message.content }}
/>
{/* 条件渲染图片,使用lazy loading优化性能 */}
{message.image_path && (
<div className="message-image">
<img
src={`${IMAGE_BASE_URL}/${message.image_path}`}
alt="留言图片"
loading="lazy"
/>
</div>
)}
<div className="message-footer">
<button
className="reply-button"
onClick={() => setShowReplyForm(!showReplyForm)}
>
{showReplyForm ? '取消回复' : `💬 回复 ${message.comments?.length > 0 ? `(${message.comments.length})` : ''}`}
</button>
</div>
{/* 条件渲染回复表单,由showReplyForm状态控制 */}
{showReplyForm && (
<div className="reply-form-container">
<CommentForm onSubmit={handleCommentSubmit} />
</div>
)}
{/* 遍历渲染评论列表 */}
{message.comments && message.comments.length > 0 && (
<div className="comments-section">
<h4 className="comments-title">
评论 ({message.comments.length})
</h4>
<div className="comments-list">
{message.comments.map((comment) => (
<div key={comment.id} className="comment-item">
<div className="comment-header">
<div className="comment-author">
<div className="comment-avatar">
{comment.name.charAt(0).toUpperCase()}
</div>
<div className="comment-info">
<span className="comment-name">{comment.name}</span>
<span className="comment-time">
{formatDate(comment.created_at)}
</span>
</div>
</div>
</div>
<div
className="comment-content"
dangerouslySetInnerHTML={{ __html: comment.content }}
/>
{comment.image_path && (
<div className="comment-image">
<img
src={`${IMAGE_BASE_URL}/${comment.image_path}`}
alt="评论图片"
loading="lazy"
/>
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
)
}
export default MessageItem
文件调用关系说明
系统的运行机制依赖于多个文件之间的协作调用,形成完整的数据流转链路。启动阶段,后端的app.py作为入口文件被Python解释器执行。文件顶部的import语句导入Flask框架和相关依赖,创建Flask应用实例。程序执行到if __name__ == '__main__'判断,确认为直接运行而非被导入,随后调用init_db函数初始化数据库,最后调用app.run启动WSGI服务器。
前端的启动从index.html开始,浏览器加载HTML文件后解析其中的script标签,发现type="module"属性,识别为ES模块并加载src指向的main.jsx文件。main.jsx作为前端入口,首先导入React库和ReactDOM库,然后导入App组件和全局样式文件index.css。通过ReactDOM.createRoot方法将App组件挂载到id为root的DOM节点,React开始渲染过程。
App组件的渲染涉及多个子组件的导入和组合。文件顶部通过import语句导入MessageForm和MessageList两个子组件,以及axios HTTP客户端库和App.css样式文件。组件函数体内定义了三个核心函数,loadMessages负责数据获取,handleMessageSubmit和handleCommentSubmit作为回调传递给子组件。useEffect钩子在组件挂载时调用loadMessages,触发对后端/api/messages接口的GET请求。
后端接收到请求后,Flask路由系统根据URL路径和HTTP方法匹配到get_messages函数。函数内部调用get_db建立数据库连接,执行SQL查询获取留言数据。对每条留言,再次执行子查询获取关联的评论,将评论列表添加到留言对象的comments属性。查询完成后,使用jsonify函数将Python字典序列化为JSON格式,通过HTTP响应返回前端。
前端axios收到响应后,解析JSON数据并传递给loadMessages函数的then回调。函数调用setMessages更新状态,React检测到状态变化触发重新渲染。App组件重新执行,根据新的messages状态渲染MessageList组件。MessageList组件文件被导入,组件接收messages数组作为props,通过map方法遍历数组,为每个留言对象创建MessageItem组件实例。
MessageItem组件渲染时,根据message对象的属性显示留言内容。如果存在image_path属性,构造完整的图片URL并设置到img标签的src属性。浏览器解析img标签发起对http://localhost:5000/uploads/文件名的GET请求。后端Flask路由匹配到uploaded_file函数,调用send_from_directory安全返回文件内容,浏览器接收文件数据并渲染图片。
用户点击回复按钮触发MessageItem组件的onClick事件处理函数,调用setShowReplyForm切换状态。React重新渲染组件,条件判断showReplyForm为true,渲染CommentForm组件。CommentForm文件被动态导入,组件挂载到DOM树。用户填写表单点击提交,触发handleSubmit函数,该函数构造FormData并调用props传入的onSubmit回调。
回调链向上传递,MessageItem的handleCommentSubmit被调用,继续调用App组件的handleCommentSubmit。该函数将留言ID添加到FormData并发起POST请求到/api/comments。后端create_comment函数接收请求,处理数据并插入数据库。操作成功后返回JSON响应,前端调用loadMessages重新获取数据,整个流程形成闭环。
样式文件的加载采用级联机制,index.css作为全局样式首先被main.jsx导入,定义了基础样式和CSS变量。各组件的CSS文件在组件文件中被导入,Vite构建系统将CSS文件编译并注入到HTML的style标签中。CSS选择器根据类名匹配元素,浏览器渲染引擎应用样式计算最终显示效果。当用户交互触发状态变化,React重新渲染组件,浏览器根据新的DOM结构重新应用CSS样式,实现动态的视觉反馈。
使用说明
系统的部署和运行需要满足特定的环境依赖。开发环境要求安装Python 3.8或更高版本,用于运行Flask后端服务。Node.js 16或更高版本是前端开发的必要条件,提供npm包管理器和JavaScript运行时。
后端启动需要打开命令行工具,使用cd命令进入A/backend目录,执行pip install命令安装Flask、Flask-CORS等依赖包。安装完成后执行python app.py,终端输出数据库初始化信息和API文档,表示服务启动成功。

前端启动需要另开一个终端窗口,进入A/frontend目录,执行npm install安装依赖,该过程会下载React、Vite等包到node_modules目录。依赖安装完成后执行npm run dev,Vite启动开发服务器并自动打开浏览器访问http://localhost:3000。

系统首次运行时会自动创建messages.db数据库文件和uploads目录,无需手动创建。若遇到端口占用问题,可以修改配置文件。后端端口在app.py文件的app.run方法中定义,修改port参数即可。前端端口在vite.config.js的server.port配置项中定义。修改端口后需要同步更新前端代码中的API_BASE_URL常量,确保请求能够正确发送到后端服务。
开发过程中可以利用浏览器开发者工具进行调试。Network标签显示所有HTTP请求,可以查看请求头、响应数据和状态码。Console标签显示JavaScript日志和错误信息,帮助定位前端代码问题。后端日志直接输出到启动终端,Flask的debug模式提供详细的错误堆栈信息,便于问题排查。
总结
本文详细介绍了一个基于React和Flask的前后端分离留言板系统的完整实现过程,从架构设计到代码实现,从技术选型到工程实践,全面展示了现代Web应用开发的核心技术和最佳实践。系统采用的前后端分离架构充分体现了职责分离和模块化设计的优势,前端专注于用户体验和界面交互,后端专注于业务逻辑和数据处理,两者通过标准化的HTTP接口实现松耦合协作。
技术栈的选择兼顾了开发效率和运行性能。React框架的虚拟DOM机制和声明式编程范式显著降低了UI开发的复杂度,组件化思想促进了代码复用和维护。Vite构建工具的即时服务器启动和快速热更新极大提升了开发体验。Flask框架的简洁设计和强大扩展性使得API开发变得高效直观,RESTful架构风格确保了接口的标准化和可维护性。SQLite数据库的零配置特性和完善的SQL支持满足了中小型应用的数据存储需求。
341



被折叠的 条评论
为什么被折叠?



