当前位置: 首页 > news >正文

从零到一:打造现代化全栈个人博客系统

经过一个月的全栈开发,我的个人博客系统 v1.0 正式完成。该系统基于 Node.js + Express + MySQL + Vue 3 + TypeScript 开发

📖 目录

  • 项目概述
  • 技术选型
  • 系统架构
  • 数据库设计
  • 后端实现
  • 前端实现
  • 核心功能实现
  • 技术亮点
  • 性能优化
  • 安全措施
  • 部署指南
  • 总结与展望

🎯 项目概述

为什么要做这个博客

  • 想要「可控、可扩展、可维护」的内容平台,而非受限于现成平台
  • 以真实项目串联学习 Vue 3 + TypeScript、Node.js + Express、MySQL 全栈能力

核心特性

前后端完全分离:RESTful API 设计,前后端独立开发部署
企业级架构:Sequelize ORM + 数据库连接池 + 统一错误处理
完整的内容管理:文章、分类、标签、评论、媒体文件一应俱全
用户认证系统:JWT Token + 角色权限控制(RBAC)
富媒体支持:图片上传、压缩、裁剪、预览
现代化前端:Vue 3 Composition API + TypeScript + Pinia 状态管理
响应式设计:支持桌面端、平板、移动端
Markdown 编辑器:支持实时预览、语法高亮
评论系统:支持匿名评论、嵌套回复、审核机制
数据安全:密码加密、SQL 注入防护、XSS 防护


🛠 技术选型

后端技术栈

运行环境: Node.js 18+
Web 框架: Express 4.x
ORM 框架: Sequelize 6.x
数据库: MySQL 8.0+
认证方案: JWT (jsonwebtoken)
密码加密: bcryptjs
文件上传: Multer 1.4.x
图片处理: Sharp 0.33.x
数据验证: express-validator

前端技术栈

框架: Vue 3.3+
开发语言: TypeScript 5.x
构建工具: Vite 5.x
状态管理: Pinia 2.x
UI 组件库: Element Plus 2.x
路由管理: Vue Router 4.x
HTTP 客户端: Axios
Markdown: marked + highlight.js
工具库: dayjs, dompurify

为什么选择这些技术?

1. Node.js + Express

  • 轻量灵活,易于上手
  • 丰富的中间件生态
  • 非阻塞 I/O,性能优异
  • JavaScript 全栈开发,降低学习成本

2. Sequelize + MySQL

  • ORM 抽象层,避免手写 SQL
  • 自动处理 SQL 注入防护
  • 支持事务、关联查询
  • MySQL 成熟稳定,社区活跃

3. Vue 3 + TypeScript

  • Composition API 代码组织更清晰
  • TypeScript 类型检查提升开发体验
  • Vite 构建速度快,开发体验好
  • Element Plus 组件丰富,开箱即用

🏗 系统架构

整体架构图

┌─────────────────────────────────────────────┐
│              前端层 (Vue 3)                  │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  │
│  │ 博客前台  │  │ 后台管理  │  │ 用户中心  │  │
│  └──────────┘  └──────────┘  └──────────┘  │
└─────────────────────────────────────────────┘↓ HTTP/REST API
┌─────────────────────────────────────────────┐
│           后端层 (Node.js + Express)         │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  │
│  │ 路由层    │  │ 业务逻辑  │  │ 数据访问  │  │
│  └──────────┘  └──────────┘  └──────────┘  │
└─────────────────────────────────────────────┘↓ Sequelize ORM
┌─────────────────────────────────────────────┐
│              数据层 (MySQL)                  │
│  articles | categories | tags | comments    │
│  users | media (媒体文件)                    │
└─────────────────────────────────────────────┘↓ 文件存储
┌─────────────────────────────────────────────┐
│          存储层 (本地/对象存储)               │
│       本地磁盘 / 阿里云OSS / 腾讯云COS       │
└─────────────────────────────────────────────┘

项目目录结构

后端目录结构
backend/
├── src/
│   ├── config/            # 配置文件
│   │   ├── database.js    # 数据库配置
│   │   └── app.js         # 应用配置
│   ├── models/            # Sequelize 模型
│   │   ├── index.js       # 模型关联
│   │   ├── User.js        # 用户模型
│   │   ├── Article.js     # 文章模型
│   │   ├── Category.js    # 分类模型
│   │   ├── Tag.js         # 标签模型
│   │   ├── Comment.js     # 评论模型
│   │   └── Media.js       # 媒体模型
│   ├── controllers/       # 业务逻辑控制器
│   │   ├── articleController.js
│   │   ├── categoryController.js
│   │   ├── tagController.js
│   │   ├── commentController.js
│   │   ├── authController.js
│   │   └── mediaController.js
│   ├── routes/            # 路由定义
│   │   ├── index.js
│   │   ├── articles.js
│   │   ├── categories.js
│   │   ├── tags.js
│   │   ├── comments.js
│   │   ├── auth.js
│   │   └── media.js
│   ├── middlewares/       # 中间件
│   │   ├── auth.js        # JWT 认证
│   │   ├── validator.js   # 数据验证
│   │   ├── upload.js      # 文件上传
│   │   └── errorHandler.js # 错误处理
│   └── utils/             # 工具函数
│       ├── response.js    # 统一响应格式
│       ├── pagination.js  # 分页工具
│       └── fileHelper.js  # 文件处理
├── uploads/               # 文件上传目录
├── server.js              # 服务器入口
├── package.json
└── .env                   # 环境变量
前端目录结构
frontend/
├── src/
│   ├── assets/            # 静态资源
│   ├── components/        # 公共组件
│   │   ├── article/       # 文章组件
│   │   ├── category/      # 分类组件
│   │   ├── comment/       # 评论组件
│   │   └── common/        # 通用组件
│   ├── views/             # 页面视图
│   │   ├── Home.vue       # 首页
│   │   ├── ArticleDetail.vue
│   │   └── admin/         # 后台管理
│   │       ├── Dashboard.vue
│   │       ├── ArticleList.vue
│   │       └── ArticleEdit.vue
│   ├── stores/            # Pinia 状态管理
│   │   ├── user.ts
│   │   ├── article.ts
│   │   └── index.ts
│   ├── router/            # 路由配置
│   │   ├── index.ts
│   │   ├── frontend.ts
│   │   └── admin.ts
│   ├── api/               # API 接口
│   │   ├── request.ts     # Axios 封装
│   │   ├── article.ts
│   │   ├── category.ts
│   │   └── comment.ts
│   ├── types/             # TypeScript 类型
│   │   └── index.ts
│   ├── App.vue
│   └── main.ts
├── index.html
├── vite.config.ts
└── package.json

🗄 数据库设计

ER 图关系

User (用户)↓ 1:N                        ↓ 1:N
Article (文章) ←──── N:N ────→ Tag (标签)    Media (媒体)↓ N:1                      (ArticleTag 中间表)
Category (分类)↓ 1:N
Comment (评论)

核心数据表设计

1. 用户表 (users)
字段名类型说明
idINT PK用户ID
usernameVARCHAR(50)用户名
emailVARCHAR(100)邮箱
passwordVARCHAR(255)密码(bcrypt加密)
avatarVARCHAR(255)头像URL
roleENUM(‘admin’,‘user’)角色
created_atDATETIME创建时间
2. 文章表 (articles)
字段名类型说明
idINT PK文章ID
titleVARCHAR(200)标题
summaryTEXT摘要
contentLONGTEXT内容(Markdown)
cover_imageVARCHAR(255)封面图
author_idINT FK作者ID
category_idINT FK分类ID
statusENUM(‘draft’,‘published’)状态
viewsINT浏览次数
is_topBOOLEAN是否置顶
published_atDATETIME发布时间
3. 分类表 (categories)
字段名类型说明
idINT PK分类ID
nameVARCHAR(50)分类名称
descriptionTEXT描述
sort_orderINT排序
4. 标签表 (tags)
字段名类型说明
idINT PK标签ID
nameVARCHAR(50)标签名称
colorVARCHAR(20)标签颜色
5. 评论表 (comments)
字段名类型说明
idINT PK评论ID
article_idINT FK文章ID
user_idINT FK用户ID
parent_idINT FK父评论ID
nicknameVARCHAR(50)昵称
emailVARCHAR(100)邮箱
contentTEXT评论内容
is_approvedBOOLEAN是否审核
6. 媒体文件表 (media)
字段名类型说明
idINT PK媒体ID
filenameVARCHAR(255)原始文件名
stored_nameVARCHAR(255)存储文件名(UUID)
file_urlVARCHAR(500)访问URL
file_sizeINT文件大小(字节)
mime_typeVARCHAR(100)MIME类型
widthINT图片宽度
heightINT图片高度
uploader_idINT FK上传者ID

Sequelize 模型关联

// models/index.js
const { sequelize } = require('../config/database');// 导入所有模型
const User = require('./User')(sequelize);
const Article = require('./Article')(sequelize);
const Category = require('./Category')(sequelize);
const Tag = require('./Tag')(sequelize);
const Comment = require('./Comment')(sequelize);
const Media = require('./Media')(sequelize);// 定义关联关系
// 用户 -> 文章 (1:N)
User.hasMany(Article, { foreignKey: 'author_id', as: 'articles' });
Article.belongsTo(User, { foreignKey: 'author_id', as: 'author' });// 分类 -> 文章 (1:N)
Category.hasMany(Article, { foreignKey: 'category_id', as: 'articles' });
Article.belongsTo(Category, { foreignKey: 'category_id', as: 'category' });// 文章 <-> 标签 (N:N)
Article.belongsToMany(Tag, { through: 'article_tags', as: 'tags',foreignKey: 'article_id'
});
Tag.belongsToMany(Article, { through: 'article_tags', as: 'articles',foreignKey: 'tag_id'
});// 文章 -> 评论 (1:N)
Article.hasMany(Comment, { foreignKey: 'article_id', as: 'comments' });
Comment.belongsTo(Article, { foreignKey: 'article_id', as: 'article' });// 用户 -> 媒体 (1:N)
User.hasMany(Media, { foreignKey: 'uploader_id', as: 'media' });
Media.belongsTo(User, { foreignKey: 'uploader_id', as: 'uploader' });module.exports = {sequelize,User,Article,Category,Tag,Comment,Media
};

🔧 后端实现

1. 数据库连接配置

// src/config/database.js
require('dotenv').config();
const { Sequelize } = require('sequelize');const sequelize = new Sequelize(process.env.DB_NAME,process.env.DB_USER,process.env.DB_PASSWORD,{host: process.env.DB_HOST,port: process.env.DB_PORT || 3306,dialect: 'mysql',// UTF8MB4 字符集,支持 emojidialectOptions: {charset: 'utf8mb4',collate: 'utf8mb4_unicode_ci'},// 连接池配置pool: {max: 10,        // 最大连接数min: 0,         // 最小连接数acquire: 30000, // 获取连接超时idle: 10000     // 空闲超时},timezone: '+08:00',logging: process.env.NODE_ENV === 'development' ? console.log : false}
);module.exports = { sequelize };

2. JWT 认证中间件

// src/middlewares/auth.js
const jwt = require('jsonwebtoken');
const { sendError } = require('../utils/response');/*** JWT 认证中间件*/
const authenticate = (req, res, next) => {try {const authHeader = req.headers.authorization;if (!authHeader || !authHeader.startsWith('Bearer ')) {return sendError(res, '未提供认证令牌', 401);}const token = authHeader.substring(7);const decoded = jwt.verify(token, process.env.JWT_SECRET);// 将用户信息附加到请求对象req.user = decoded;next();} catch (error) {if (error.name === 'TokenExpiredError') {return sendError(res, '令牌已过期', 401);}if (error.name === 'JsonWebTokenError') {return sendError(res, '无效的令牌', 401);}return sendError(res, '认证失败', 401);}
};/*** 管理员权限验证*/
const isAdmin = (req, res, next) => {if (req.user.role !== 'admin') {return sendError(res, '需要管理员权限', 403);}next();
};module.exports = { authenticate, isAdmin };

3. 文章控制器实现

// src/controllers/articleController.js
const { Article, User, Category, Tag } = require('../models');
const { sendSuccess, sendError } = require('../utils/response');
const { paginate } = require('../utils/pagination');/*** 获取文章列表*/
exports.getArticles = async (req, res) => {try {const { page = 1, limit = 10, category_id, tag_id, keyword,status = 'published' } = req.query;// 构建查询条件const where = {};if (category_id) where.category_id = category_id;if (keyword) {where[Op.or] = [{ title: { [Op.like]: `%${keyword}%` } },{ summary: { [Op.like]: `%${keyword}%` } }];}// 前台仅显示已发布文章if (!req.user || req.user.role !== 'admin') {where.status = 'published';} else if (status) {where.status = status;}// 分页查询const { rows: articles, count: total } = await Article.findAndCountAll({where,include: [{ model: User, as: 'author', attributes: ['id', 'username', 'avatar'] },{ model: Category, as: 'category' },{ model: Tag, as: 'tags' }],order: [['is_top', 'DESC'],['published_at', 'DESC']],...paginate(page, limit)});sendSuccess(res, { articles, total, page, limit });} catch (error) {console.error('获取文章列表失败:', error);sendError(res, '获取文章列表失败', 500);}
};/*** 创建文章*/
exports.createArticle = async (req, res) => {try {const { title, summary, content, cover_image, category_id, tag_ids, status } = req.body;// 创建文章const article = await Article.create({title,summary,content,cover_image,category_id,author_id: req.user.id,status: status || 'draft',published_at: status === 'published' ? new Date() : null});// 关联标签if (tag_ids && tag_ids.length > 0) {await article.setTags(tag_ids);}// 返回完整的文章信息const newArticle = await Article.findByPk(article.id, {include: [{ model: User, as: 'author' },{ model: Category, as: 'category' },{ model: Tag, as: 'tags' }]});sendSuccess(res, newArticle, '文章创建成功', 201);} catch (error) {console.error('创建文章失败:', error);sendError(res, '创建文章失败', 500);}
};/*** 更新文章浏览量*/
exports.incrementViews = async (req, res) => {try {const { id } = req.params;await Article.increment('views', { where: { id } });sendSuccess(res, null, '浏览量更新成功');} catch (error) {console.error('更新浏览量失败:', error);sendError(res, '更新浏览量失败', 500);}
};

4. 文件上传中间件

// src/middlewares/upload.js
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');// 确保上传目录存在
const uploadDir = path.join(__dirname, '../../uploads/media');
if (!fs.existsSync(uploadDir)) {fs.mkdirSync(uploadDir, { recursive: true });
}// 配置存储
const storage = multer.diskStorage({destination: (req, file, cb) => {// 按年月创建子目录const date = new Date();const year = date.getFullYear();const month = String(date.getMonth() + 1).padStart(2, '0');const dir = path.join(uploadDir, `${year}/${month}`);if (!fs.existsSync(dir)) {fs.mkdirSync(dir, { recursive: true });}cb(null, dir);},filename: (req, file, cb) => {const ext = path.extname(file.originalname);const filename = `${Date.now()}-${uuidv4()}${ext}`;cb(null, filename);}
});// 文件过滤
const fileFilter = (req, file, cb) => {const allowedTypes = /jpeg|jpg|png|gif|webp/;const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());const mimetype = allowedTypes.test(file.mimetype);if (extname && mimetype) {cb(null, true);} else {cb(new Error('只支持图片格式: JPEG, PNG, GIF, WebP'));}
};const upload = multer({storage,fileFilter,limits: {fileSize: 5 * 1024 * 1024 // 5MB}
});module.exports = upload;

5. 图片处理工具

// src/utils/fileHelper.js
const sharp = require('sharp');/*** 压缩和优化图片*/
async function optimizeImage(inputPath, outputPath, options = {}) {const { width = 1920, quality = 85 } = options;const image = sharp(inputPath);const metadata = await image.metadata();// 如果图片宽度超过限制,进行缩放if (metadata.width > width) {await image.resize(width, null, { withoutEnlargement: true }).jpeg({ quality }).toFile(outputPath);} else {await image.jpeg({ quality }).toFile(outputPath);}return {width: metadata.width,height: metadata.height};
}/*** 获取图片尺寸*/
async function getImageSize(filePath) {const metadata = await sharp(filePath).metadata();return {width: metadata.width,height: metadata.height};
}module.exports = {optimizeImage,getImageSize
};

6. 统一响应格式

// src/utils/response.js/*** 成功响应*/
const sendSuccess = (res, data = null, message = 'success', statusCode = 200) => {res.status(statusCode).json({code: statusCode,message,data});
};/*** 错误响应*/
const sendError = (res, message = 'error', statusCode = 500, errors = null) => {res.status(statusCode).json({code: statusCode,message,...(errors && { errors })});
};module.exports = {sendSuccess,sendError
};

🎨 前端实现

1. Axios 请求封装

// src/api/request.ts
import axios, { AxiosError, AxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores'
import router from '@/router'// 创建 axios 实例
const request = axios.create({baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api/v1',timeout: 10000
})// 请求拦截器:添加 token
request.interceptors.request.use((config) => {const userStore = useUserStore()if (userStore.token) {config.headers.Authorization = `Bearer ${userStore.token}`}return config},(error: AxiosError) => {console.error('请求错误:', error)return Promise.reject(error)}
)// 响应拦截器:统一错误处理
request.interceptors.response.use((response: AxiosResponse) => {return response.data},(error: AxiosError<any>) => {if (error.response) {const { status, data } = error.response// 401 未认证if (status === 401) {ElMessage.error('请先登录')const userStore = useUserStore()userStore.logout()router.push('/login')}// 403 无权限else if (status === 403) {ElMessage.error('没有权限访问')}// 其他错误else {ElMessage.error(data.message || '请求失败')}} else {ElMessage.error('网络错误,请检查网络连接')}return Promise.reject(error)}
)export default request

2. 文章 API 接口

// src/api/article.ts
import request from './request'
import type { Article, ArticleListParams, ArticleListResponse } from '@/types'/*** 获取文章列表*/
export const getArticles = (params: ArticleListParams) => {return request.get<ArticleListResponse>('/articles', { params })
}/*** 获取文章详情*/
export const getArticleById = (id: number) => {return request.get<Article>(`/articles/${id}`)
}/*** 创建文章*/
export const createArticle = (data: Partial<Article>) => {return request.post<Article>('/articles', data)
}/*** 更新文章*/
export const updateArticle = (id: number, data: Partial<Article>) => {return request.put<Article>(`/articles/${id}`, data)
}/*** 删除文章*/
export const deleteArticle = (id: number) => {return request.delete(`/articles/${id}`)
}/*** 增加浏览量*/
export const incrementArticleViews = (id: number) => {return request.post(`/articles/${id}/views`)
}

3. Pinia 状态管理

// src/stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { login as loginApi, register as registerApi, getProfile } from '@/api/auth'
import type { User, LoginParams, RegisterParams } from '@/types'export const useUserStore = defineStore('user', () => {// 状态const user = ref<User | null>(null)const token = ref<string>(localStorage.getItem('token') || '')// 计算属性const isLoggedIn = computed(() => !!token.value)const isAdmin = computed(() => user.value?.role === 'admin')// 登录const login = async (params: LoginParams) => {try {const res = await loginApi(params)token.value = res.data.tokenuser.value = res.data.userlocalStorage.setItem('token', res.data.token)return res} catch (error) {throw error}}// 注册const register = async (params: RegisterParams) => {try {const res = await registerApi(params)return res} catch (error) {throw error}}// 登出const logout = () => {user.value = nulltoken.value = ''localStorage.removeItem('token')}// 获取用户信息const fetchProfile = async () => {try {const res = await getProfile()user.value = res.datareturn res} catch (error) {logout()throw error}}return {user,token,isLoggedIn,isAdmin,login,register,logout,fetchProfile}
})

4. 路由配置

// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores'const router = createRouter({history: createWebHistory(),routes: [{path: '/',name: 'Home',component: () => import('@/views/Home.vue'),meta: { title: '首页' }},{path: '/article/:id',name: 'ArticleDetail',component: () => import('@/views/ArticleDetail.vue'),meta: { title: '文章详情' }},{path: '/admin',component: () => import('@/layouts/AdminLayout.vue'),meta: { requiresAuth: true },children: [{path: '',name: 'Dashboard',component: () => import('@/views/admin/Dashboard.vue'),meta: { title: '控制台' }},{path: 'articles',name: 'ArticleList',component: () => import('@/views/admin/ArticleList.vue'),meta: { title: '文章管理' }},{path: 'articles/create',name: 'ArticleCreate',component: () => import('@/views/admin/ArticleEdit.vue'),meta: { title: '创建文章' }}]}]
})// 路由守卫
router.beforeEach((to, from, next) => {const userStore = useUserStore()// 设置页面标题document.title = to.meta.title ? `${to.meta.title} - 个人博客` : '个人博客'// 检查是否需要认证if (to.meta.requiresAuth && !userStore.isLoggedIn) {next({path: '/login',query: { redirect: to.fullPath }})} else {next()}
})export default router

5. TypeScript 类型定义

// src/types/index.ts// 用户类型
export interface User {id: numberusername: stringemail: stringavatar: stringrole: 'admin' | 'user'created_at: string
}// 文章类型
export interface Article {id: numbertitle: stringsummary: stringcontent: stringcover_image: stringauthor: Usercategory: Categorytags: Tag[]status: 'draft' | 'published'views: numberis_top: booleanpublished_at: stringcreated_at: stringupdated_at: string
}// 分类类型
export interface Category {id: numbername: stringdescription: stringarticle_count?: number
}// 标签类型
export interface Tag {id: numbername: stringcolor: stringarticle_count?: number
}// 评论类型
export interface Comment {id: numberarticle_id: numberuser_id: numberparent_id: number | nullnickname: stringemail: stringcontent: stringis_approved: booleancreated_at: stringchildren?: Comment[]
}// API 响应类型
export interface ApiResponse<T = any> {code: numbermessage: stringdata: T
}// 分页响应类型
export interface PaginationResponse<T> {total: numberpage: numberlimit: numberitems: T[]
}

💡 核心功能实现

1. JWT 认证流程

// 用户登录流程
exports.login = async (req, res) => {try {const { email, password } = req.body;// 1. 查找用户const user = await User.findOne({ where: { email } });if (!user) {return sendError(res, '用户不存在', 404);}// 2. 验证密码const isPasswordValid = await bcrypt.compare(password, user.password);if (!isPasswordValid) {return sendError(res, '密码错误', 401);}// 3. 生成 JWT Tokenconst token = jwt.sign({ id: user.id, username: user.username,role: user.role },process.env.JWT_SECRET,{ expiresIn: '7d' });// 4. 返回用户信息和 tokensendSuccess(res, {user: {id: user.id,username: user.username,email: user.email,avatar: user.avatar,role: user.role},token}, '登录成功');} catch (error) {sendError(res, '登录失败', 500);}
};

2. 文件上传流程

// 媒体文件上传
exports.uploadMedia = async (req, res) => {try {if (!req.file) {return sendError(res, '未上传文件', 400);}const file = req.file;// 1. 获取图片尺寸const { width, height } = await getImageSize(file.path);// 2. 压缩图片(可选)if (width > 1920) {await optimizeImage(file.path, file.path, { width: 1920, quality: 85 });}// 3. 生成访问 URLconst fileUrl = `${req.protocol}://${req.get('host')}/uploads/media/${file.filename}`;// 4. 保存到数据库const media = await Media.create({filename: file.originalname,stored_name: file.filename,file_path: file.path,file_url: fileUrl,file_size: file.size,mime_type: file.mimetype,width,height,uploader_id: req.user.id});sendSuccess(res, media, '上传成功', 201);} catch (error) {console.error('上传失败:', error);sendError(res, '上传失败', 500);}
};

3. 评论系统实现

// 发表评论
exports.createComment = async (req, res) => {try {const { articleId } = req.params;const { content, parent_id, nickname, email } = req.body;// 验证文章是否存在const article = await Article.findByPk(articleId);if (!article) {return sendError(res, '文章不存在', 404);}// 创建评论const comment = await Comment.create({article_id: articleId,user_id: req.user?.id || null,parent_id: parent_id || null,nickname: req.user ? req.user.username : nickname,email: req.user ? req.user.email : email,content,// 登录用户自动审核,匿名评论需审核is_approved: !!req.user});sendSuccess(res, comment, '评论成功', 201);} catch (error) {console.error('评论失败:', error);sendError(res, '评论失败', 500);}
};// 获取评论列表(带嵌套结构)
exports.getComments = async (req, res) => {try {const { articleId } = req.params;// 获取所有顶级评论const comments = await Comment.findAll({where: { article_id: articleId,parent_id: null,is_approved: true},include: [{ model: User, as: 'user', attributes: ['id', 'username', 'avatar'] }],order: [['created_at', 'DESC']]});// 递归获取子评论const commentsWithReplies = await Promise.all(comments.map(async (comment) => {const replies = await getCommentReplies(comment.id);return {...comment.toJSON(),replies};}));sendSuccess(res, commentsWithReplies);} catch (error) {sendError(res, '获取评论失败', 500);}
};

4. Markdown 编辑器组件

<!-- src/components/MarkdownEditor.vue -->
<template><div class="markdown-editor"><div class="toolbar"><el-button-group><el-button @click="insertText('**', '**')">加粗</el-button><el-button @click="insertText('*', '*')">斜体</el-button><el-button @click="insertText('`', '`')">代码</el-button><el-button @click="insertText('\n```\n', '\n```\n')">代码块</el-button><el-button @click="insertText('[', '](url)')">链接</el-button><el-button @click="handleImageUpload">图片</el-button></el-button-group><el-button @click="togglePreview" :type="showPreview ? 'primary' : ''">预览</el-button></div><div class="editor-container"><el-inputv-show="!showPreview"v-model="content"type="textarea":rows="20"placeholder="开始编写 Markdown..."@input="handleInput"/><div v-show="showPreview" class="markdown-preview" v-html="renderedHtml" /></div></div>
</template><script setup lang="ts">
import { ref, computed } from 'vue'
import { marked } from 'marked'
import DOMPurify from 'dompurify'
import hljs from 'highlight.js'const props = defineProps<{modelValue: string
}>()const emit = defineEmits<{(e: 'update:modelValue', value: string): void
}>()const content = computed({get: () => props.modelValue,set: (value) => emit('update:modelValue', value)
})const showPreview = ref(false)// 配置 marked
marked.setOptions({highlight: (code, lang) => {if (lang && hljs.getLanguage(lang)) {return hljs.highlight(code, { language: lang }).value}return hljs.highlightAuto(code).value}
})// 渲染 Markdown
const renderedHtml = computed(() => {const html = marked(content.value)return DOMPurify.sanitize(html)
})// 插入文本
const insertText = (before: string, after: string) => {// 实现光标位置插入逻辑content.value += before + after
}// 切换预览
const togglePreview = () => {showPreview.value = !showPreview.value
}
</script><style scoped lang="scss">
.markdown-editor {border: 1px solid #dcdfe6;border-radius: 4px;.toolbar {display: flex;justify-content: space-between;padding: 10px;border-bottom: 1px solid #dcdfe6;background: #f5f7fa;}.editor-container {min-height: 400px;}.markdown-preview {padding: 20px;min-height: 400px;:deep(pre) {background: #f6f8fa;padding: 16px;border-radius: 6px;overflow-x: auto;}:deep(code) {font-family: 'Courier New', monospace;}}
}
</style>

✨ 技术亮点

1. 前后端完全分离

  • RESTful API 设计,前后端独立开发和部署
  • 统一的 API 响应格式,便于前端统一处理
  • JWT Token 认证,无状态设计

2. 数据库设计规范

  • Sequelize ORM 避免手写 SQL,防止 SQL 注入
  • 完善的模型关联关系,支持联表查询
  • 合理的索引设计,提升查询性能
  • UTF8MB4 字符集,支持 emoji 表情

3. 文件上传优化

  • Multer 中间件处理文件上传
  • Sharp 图片压缩和处理
  • 按年月自动创建目录结构
  • UUID 命名防止文件名冲突

4. 前端工程化

  • TypeScript 类型检查,减少运行时错误
  • Pinia 状态管理,替代 Vuex
  • Vite 极速构建,开发体验好
  • 组件化开发,代码复用率高

5. 安全措施完善

  • bcrypt 密码加密,不存储明文密码
  • JWT Token 认证,Token 7 天有效期
  • XSS 防护:DOMPurify 过滤 HTML
  • CORS 跨域配置,限制访问来源
  • 数据验证:express-validator

⚡ 性能优化

后端优化

// 1. 数据库连接池
pool: {max: 10,min: 0,acquire: 30000,idle: 10000
}// 2. 查询优化 - 只选择需要的字段
Article.findAll({attributes: ['id', 'title', 'summary', 'cover_image', 'published_at'],// ...
})// 3. 分页查询 - 避免一次性加载大量数据
const { offset, limit } = paginate(page, limit);
Article.findAndCountAll({ offset, limit })// 4. 索引优化
indexes: [{ fields: ['author_id'] },{ fields: ['category_id'] },{ fields: ['status'] },{ fields: ['published_at'] }
]// 5. Gzip 压缩
const compression = require('compression');
app.use(compression());

前端优化

// 1. 路由懒加载
{path: '/article/:id',component: () => import('@/views/ArticleDetail.vue')
}// 2. 组件按需引入
import { ElButton, ElMessage } from 'element-plus'// 3. 图片懒加载
<el-image lazy :src="article.cover_image" />// 4. 防抖优化
import { debounce } from 'lodash-es'
const handleSearch = debounce((keyword) => {// 搜索逻辑
}, 500)// 5. 虚拟滚动(长列表)
<el-virtual-list :items="articles" :item-size="100" />

🔒 安全措施

1. 密码安全

const bcrypt = require('bcryptjs');// 注册时加密密码
const hashedPassword = await bcrypt.hash(password, 10);// 登录时验证密码
const isValid = await bcrypt.compare(inputPassword, user.password);

2. JWT Token 认证

// 生成 Token
const token = jwt.sign({ id: user.id, username: user.username, role: user.role },process.env.JWT_SECRET,{ expiresIn: '7d' }
);// 验证 Token
const decoded = jwt.verify(token, process.env.JWT_SECRET);

3. XSS 防护

// 前端使用 DOMPurify 过滤 HTML
import DOMPurify from 'dompurify'const cleanHtml = DOMPurify.sanitize(dirtyHtml)

4. CORS 配置

app.use(cors({origin: process.env.NODE_ENV === 'production' ? process.env.FRONTEND_URL : '*',credentials: true
}));

5. 数据验证

const { body, validationResult } = require('express-validator');// 验证规则
const validateArticle = [body('title').notEmpty().withMessage('标题不能为空'),body('content').notEmpty().withMessage('内容不能为空'),body('category_id').isInt().withMessage('分类ID必须是整数')
];// 验证中间件
const validate = (req, res, next) => {const errors = validationResult(req);if (!errors.isEmpty()) {return sendError(res, '数据验证失败', 400, errors.array());}next();
};

🚀 部署指南

开发环境

# 1. 安装依赖
cd backend && npm install
cd frontend && npm install# 2. 配置环境变量
cp backend/.env.example backend/.env
# 编辑 .env 填写数据库配置# 3. 初始化数据库
cd backend
npm run init-db# 4. 启动后端
npm run dev# 5. 启动前端(新终端)
cd frontend
npm run dev

生产环境部署

后端部署(使用 PM2)
# 1. 安装 PM2
npm install -g pm2# 2. 生产环境启动
cd backend
NODE_ENV=production pm2 start server.js --name blog-backend# 3. 保存 PM2 配置
pm2 save
pm2 startup
前端部署(Nginx)
# 1. 构建生产版本
cd frontend
npm run build# 2. 配置 Nginx
server {listen 80;server_name yourdomain.com;root /var/www/blog/dist;index index.html;# SPA 路由重定向location / {try_files $uri $uri/ /index.html;}# API 代理location /api {proxy_pass http://localhost:3000;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;}# 静态文件缓存location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {expires 1y;add_header Cache-Control "public, immutable";}
}# 3. 重启 Nginx
sudo nginx -t
sudo systemctl reload nginx
Docker 部署(可选)
# backend/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
# docker-compose.yml
version: '3.8'
services:mysql:image: mysql:8.0environment:MYSQL_ROOT_PASSWORD: passwordMYSQL_DATABASE: blogvolumes:- mysql_data:/var/lib/mysqlports:- "3306:3306"backend:build: ./backendports:- "3000:3000"depends_on:- mysqlenvironment:DB_HOST: mysqlDB_NAME: blogfrontend:build: ./frontendports:- "80:80"depends_on:- backendvolumes:mysql_data:

📊 项目成果

完整的 API 接口

  • 用户认证: 注册、登录、个人信息管理
  • 文章管理: CRUD、分页、搜索、排序
  • 分类管理: CRUD、文章统计
  • 标签管理: CRUD、文章关联
  • 评论系统: 发表、审核、嵌套回复
  • 媒体管理: 上传、列表、删除

共计 33+ API 接口

完整的前端页面

  • 博客首页: 文章列表、分类导航、热门标签
  • 文章详情: Markdown 渲染、评论区、相关文章
  • 分类/标签页: 按分类/标签浏览文章
  • 后台管理: 文章管理、评论审核、媒体库

共计 15+ 页面组件


🎓 总结与展望

项目收获

通过这个项目,我们实现了:

  1. 全栈开发能力提升:掌握了从数据库设计到前端界面的完整开发流程
  2. 工程化思维培养:学会了如何组织代码结构、设计 API、管理状态
  3. 最佳实践应用:JWT 认证、ORM 使用、TypeScript 类型系统
  4. 安全意识增强:密码加密、XSS 防护、SQL 注入防护
  5. 性能优化经验:数据库索引、分页查询、图片压缩

未来扩展方向

  • 搜索功能: 集成 Elasticsearch 实现全文搜索
  • 邮件通知: 评论提醒、订阅通知
  • 社交分享: 一键分享到微信、微博等平台
  • Markdown 增强: 支持流程图、数学公式
  • 多语言支持: i18n 国际化
  • 主题切换: 夜间模式、自定义主题
  • CDN 加速: 图片、静态资源 CDN 部署
  • 缓存优化: Redis 缓存热门文章
  • 数据统计: 访问量分析、用户行为追踪
  • SSR 优化: Nuxt 3 服务端渲染,SEO 优化

最后的话

这个博客系统不仅是一个实用的个人网站,更是一个学习全栈开发的完整案例。从需求分析、架构设计、编码实现到部署上线,每一个环节都遵循了工业界的最佳实践。

希望这个项目能给你带来启发,无论是用于搭建自己的博客,还是作为学习全栈开发的参考案例。

如果你觉得这个项目有帮助,欢迎 Star ⭐️


📚 参考资源

  • Express 官方文档
  • Sequelize 官方文档
  • Vue 3 官方文档
  • Element Plus 官方文档
  • TypeScript 官方文档
  • MySQL 8.0 文档

日期: 2025-11-02
版本: v1.0
项目地址

  • 前端
  • 后端
    技术栈: Node.js + Express + MySQL + Vue 3 + TypeScript + Echart

如果这篇文章对你有帮助,欢迎点赞、收藏、分享! 👍

http://www.dtcms.com/a/561587.html

相关文章:

  • Windows 安装 WSL 并集成 Docker
  • LVS-DR模式配置
  • 零基础新手小白快速了解掌握服务集群与自动化运维(十六)集群部署模块——LVS-DRTUN模式配置
  • 济南网站建设网站最新域名解析网站
  • LVS-NAT、DR、TUN模式配置
  • Qt样式深度解析
  • 怎么用自己电脑做网站优化一个网站
  • 莱芜做网站优化溧阳建设集团有限公司网站
  • id创建网站徐州品牌网站建设
  • 创意设计app青岛网站seo技巧
  • 中英文网站建设 大概要多久张掖建设网站
  • python 异步编程 -- 理解concurrent.futures.Future 对象
  • 【网络工程师】物理二层STP协议
  • 网站关键词排名优化应该怎么做网站备案成功后怎么办
  • Vue3组件间通信——pinia
  • php零基础做网站网站没后台怎么修改类容
  • 郑州做网站狼牙建立网站的链接结构有哪几种形式?
  • RTL8762KD_EVB_Board-嘉立创EDA设计
  • 西安网站制作公司怎么选宁波企业做网站哪家好
  • 手机网站开发算什么费用seo服务外包价格
  • 在 ​CentOS 7​ 的 Linux 系统中配置 ​NFS
  • 网站欣赏网站整合营销传播成功案例
  • 深圳高端网站设计建设网站推广百度优化
  • React Native 项目实战指南
  • 百度品牌网站建设优化大师如何删掉多余的学生
  • 做平面设计的一般浏览什么网站wordpress自定义文章顺序
  • 打造推理模型的4种方法——李宏毅2025大模型课程第7讲
  • 金融行业客服系统中合规高效的身份验证流程分享
  • 网站服务器怎么维护濮阳网站建设在哪里
  • 四川省住房和城乡建设厅网站官网西安做网站缑阳建