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

全栈项目实战:Vue3+Node.js开发博客系统

全栈项目实战:Vue3+Node.js开发博客系统

一、项目架构设计

1. 技术栈选型

前端技术栈

  • Vue 3 + Composition API
  • TypeScript
  • Pinia状态管理
  • Vue Router 4
  • Element Plus UI组件库
  • Vite构建工具

后端技术栈

  • Node.js (Express/Koa)
  • MongoDB (Mongoose)
  • JWT认证
  • RESTful API设计
  • Swagger文档

2. 目录结构规划

blog-system/
├── client/                # 前端项目
│   ├── public/            # 静态资源
│   ├── src/
│   │   ├── api/           # API请求封装
│   │   ├── assets/        # 静态资源
│   │   ├── components/    # 公共组件
│   │   ├── composables/   # 自定义Hook
│   │   ├── router/        # 路由配置
│   │   ├── stores/        # Pinia状态
│   │   ├── styles/        # 全局样式
│   │   ├── utils/         # 工具函数
│   │   ├── views/         # 页面组件
│   │   ├── App.vue        # 根组件
│   │   └── main.ts        # 入口文件
│   ├── tsconfig.json      # TypeScript配置
│   └── vite.config.ts     # Vite配置
│
├── server/                # 后端项目
│   ├── config/            # 配置文件
│   ├── controllers/       # 控制器
│   ├── models/            # 数据模型
│   ├── middleware/        # 中间件
│   ├── routes/            # 路由定义
│   ├── utils/             # 工具函数
│   ├── app.js             # 应用入口
│   └── package.json
│
├── docs/                  # 项目文档
└── package.json           # 全局脚本

二、后端API开发

1. Express应用初始化

// server/app.js
const express = require('express')
const mongoose = require('mongoose')
const cors = require('cors')
const helmet = require('helmet')
const morgan = require('morgan')
const { errorHandler } = require('./middleware/error')const app = express()// 中间件
app.use(cors())
app.use(helmet())
app.use(morgan('dev'))
app.use(express.json())// 数据库连接
mongoose.connect(process.env.MONGODB_URI, {useNewUrlParser: true,useUnifiedTopology: true
})
.then(() => console.log('MongoDB connected'))
.catch(err => console.error(err))// 路由
app.use('/api/auth', require('./routes/auth'))
app.use('/api/users', require('./routes/users'))
app.use('/api/posts', require('./routes/posts'))
app.use('/api/comments', require('./routes/comments'))// 错误处理
app.use(errorHandler)const PORT = process.env.PORT || 5000
app.listen(PORT, () => console.log(`Server running on port ${PORT}`))

2. 数据模型设计

// server/models/Post.js
const mongoose = require('mongoose')
const slugify = require('slugify')const PostSchema = new mongoose.Schema({title: {type: String,required: [true, 'Please add a title'],trim: true,maxlength: [100, 'Title cannot be more than 100 characters']},slug: String,content: {type: String,required: [true, 'Please add content'],maxlength: [5000, 'Content cannot be more than 5000 characters']},excerpt: {type: String,maxlength: [300, 'Excerpt cannot be more than 300 characters']},coverImage: {type: String,default: 'no-photo.jpg'},tags: {type: [String],required: true,enum: ['technology','programming','design','business','lifestyle']},author: {type: mongoose.Schema.ObjectId,ref: 'User',required: true},status: {type: String,enum: ['draft', 'published'],default: 'draft'},createdAt: {type: Date,default: Date.now},updatedAt: Date
}, {toJSON: { virtuals: true },toObject: { virtuals: true }
})// 创建文章slug
PostSchema.pre('save', function(next) {this.slug = slugify(this.title, { lower: true })next()
})// 反向填充评论
PostSchema.virtual('comments', {ref: 'Comment',localField: '_id',foreignField: 'post',justOne: false
})module.exports = mongoose.model('Post', PostSchema)

3. RESTful API实现

// server/controllers/posts.js
const Post = require('../models/Post')
const ErrorResponse = require('../utils/errorResponse')
const asyncHandler = require('../middleware/async')// @desc    获取所有文章
// @route   GET /api/posts
// @access  Public
exports.getPosts = asyncHandler(async (req, res, next) => {res.status(200).json(res.advancedResults)
})// @desc    获取单篇文章
// @route   GET /api/posts/:id
// @access  Public
exports.getPost = asyncHandler(async (req, res, next) => {const post = await Post.findById(req.params.id).populate({path: 'author',select: 'name avatar'}).populate('comments')if (!post) {return next(new ErrorResponse(`Resource not found with id of ${req.params.id}`, 404))}res.status(200).json({ success: true, data: post })
})// @desc    创建文章
// @route   POST /api/posts
// @access  Private
exports.createPost = asyncHandler(async (req, res, next) => {// 添加作者req.body.author = req.user.idconst post = await Post.create(req.body)res.status(201).json({ success: true, data: post })
})// @desc    更新文章
// @route   PUT /api/posts/:id
// @access  Private
exports.updatePost = asyncHandler(async (req, res, next) => {let post = await Post.findById(req.params.id)if (!post) {return next(new ErrorResponse(`Resource not found with id of ${req.params.id}`, 404))}// 验证文章所有者或管理员if (post.author.toString() !== req.user.id && req.user.role !== 'admin') {return next(new ErrorResponse(`User ${req.user.id} is not authorized to update this post`, 401))}post = await Post.findByIdAndUpdate(req.params.id, req.body, {new: true,runValidators: true})res.status(200).json({ success: true, data: post })
})// @desc    删除文章
// @route   DELETE /api/posts/:id
// @access  Private
exports.deletePost = asyncHandler(async (req, res, next) => {const post = await Post.findById(req.params.id)if (!post) {return next(new ErrorResponse(`Resource not found with id of ${req.params.id}`, 404))}// 验证文章所有者或管理员if (post.author.toString() !== req.user.id && req.user.role !== 'admin') {return next(new ErrorResponse(`User ${req.user.id} is not authorized to delete this post`, 401))}await post.remove()res.status(200).json({ success: true, data: {} })
})

三、前端Vue3实现

1. 前端工程初始化

npm init vite@latest client --template vue-ts
cd client
npm install pinia vue-router axios element-plus @element-plus/icons-vue

2. 状态管理设计

// client/src/stores/auth.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { login, logout, getMe } from '@/api/auth'
import type { User } from '@/types'export const useAuthStore = defineStore('auth', () => {const user = ref<User | null>(null)const token = ref(localStorage.getItem('token') || '')const isAuthenticated = ref(false)async function loginUser(credentials: { email: string; password: string }) {const response = await login(credentials)token.value = response.tokenlocalStorage.setItem('token', token.value)await fetchUser()}async function fetchUser() {try {user.value = await getMe()isAuthenticated.value = true} catch (error) {logoutUser()}}function logoutUser() {logout()user.value = nulltoken.value = ''isAuthenticated.value = falselocalStorage.removeItem('token')}return { user, token, isAuthenticated, loginUser, logoutUser, fetchUser }
})

3. 博客首页实现

<!-- client/src/views/HomeView.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { usePostStore } from '@/stores/post'
import PostList from '@/components/PostList.vue'
import PostFilter from '@/components/PostFilter.vue'const postStore = usePostStore()
const isLoading = ref(false)
const error = ref<string | null>(null)const fetchPosts = async () => {try {isLoading.value = trueerror.value = nullawait postStore.fetchPosts()} catch (err) {error.value = err.message || 'Failed to fetch posts'} finally {isLoading.value = false}
}onMounted(() => {if (postStore.posts.length === 0) {fetchPosts()}
})
</script><template><div class="home"><el-container><el-main><el-row :gutter="20"><el-col :md="16" :sm="24"><post-filter @filter-change="fetchPosts" /><div v-if="isLoading" class="loading-spinner"><el-skeleton :rows="5" animated /></div><template v-else><post-list v-if="postStore.posts.length > 0":posts="postStore.posts"/><el-empty v-else description="No posts found" /></template></el-col><el-col :md="8" :sm="24"><div class="sidebar"><el-card><template #header><h3>Popular Tags</h3></template><el-tag v-for="tag in postStore.tags" :key="tag" size="large"@click="postStore.setCurrentTag(tag)">{{ tag }}</el-tag></el-card></div></el-col></el-row></el-main></el-container></div>
</template><style scoped>
.home {max-width: 1200px;margin: 0 auto;padding: 20px;
}.sidebar {position: sticky;top: 20px;
}.el-tag {margin: 5px;cursor: pointer;
}.loading-spinner {padding: 20px;
}
</style>

4. Markdown编辑器集成

<!-- client/src/components/Editor/MarkdownEditor.vue -->
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import VMdEditor from '@kangc/v-md-editor'
import '@kangc/v-md-editor/lib/style/base-editor.css'
import githubTheme from '@kangc/v-md-editor/lib/theme/github.js'
import '@kangc/v-md-editor/lib/theme/style/github.css'// 引入所有你需要的插件
import hljs from 'highlight.js'
import createEmojiPlugin from '@kangc/v-md-editor/lib/plugins/emoji/index'
import '@kangc/v-md-editor/lib/plugins/emoji/emoji.css'VMdEditor.use(githubTheme, {Hljs: hljs,
})
VMdEditor.use(createEmojiPlugin())const props = defineProps({modelValue: {type: String,default: ''}
})const emit = defineEmits(['update:modelValue'])const content = ref(props.modelValue)watch(() => props.modelValue, (newVal) => {if (newVal !== content.value) {content.value = newVal}
})watch(content, (newVal) => {emit('update:modelValue', newVal)
})
</script><template><v-md-editor v-model="content" :mode="'edit'"height="500px"left-toolbar="undo redo clear | h bold italic strikethrough quote | ul ol table hr | link image code emoji"/>
</template>

四、前后端交互

1. API请求封装

// client/src/api/http.ts
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'
import { useAuthStore } from '@/stores/auth'const apiClient: AxiosInstance = axios.create({baseURL: import.meta.env.VITE_API_BASE_URL,timeout: 10000,headers: {'Content-Type': 'application/json'}
})// 请求拦截器
apiClient.interceptors.request.use((config) => {const authStore = useAuthStore()if (authStore.token) {config.headers.Authorization = `Bearer ${authStore.token}`}return config
})// 响应拦截器
apiClient.interceptors.response.use((response) => response.data,(error) => {if (error.response?.status === 401) {const authStore = useAuthStore()authStore.logoutUser()window.location.href = '/login'}return Promise.reject(error.response?.data?.message || error.message || 'Unknown error')}
)export default apiClient

2. 文章API模块

// client/src/api/post.ts
import apiClient from './http'
import type { Post, PostListParams } from '@/types'export const fetchPosts = (params?: PostListParams) => {return apiClient.get<Post[]>('/api/posts', { params })
}export const getPost = (id: string) => {return apiClient.get<Post>(`/api/posts/${id}`)
}export const createPost = (data: FormData) => {return apiClient.post<Post>('/api/posts', data, {headers: {'Content-Type': 'multipart/form-data'}})
}export const updatePost = (id: string, data: FormData) => {return apiClient.put<Post>(`/api/posts/${id}`, data, {headers: {'Content-Type': 'multipart/form-data'}})
}export const deletePost = (id: string) => {return apiClient.delete(`/api/posts/${id}`)
}

五、项目部署方案

1. Docker容器化部署

前端Dockerfile:

# client/Dockerfile
FROM node:18-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run buildFROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

后端Dockerfile:

# server/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 5000
CMD ["node", "app.js"]

docker-compose.yml:

version: '3.8'services:client:build: ./clientports:- "80:80"depends_on:- serverrestart: unless-stoppedserver:build: ./serverports:- "5000:5000"environment:- MONGODB_URI=mongodb://mongo:27017/blogdepends_on:- mongorestart: unless-stoppedmongo:image: mongo:5.0volumes:- mongo-data:/data/dbports:- "27017:27017"restart: unless-stoppedvolumes:mongo-data:

2. Nginx配置

# client/nginx.conf
server {listen 80;server_name localhost;location / {root /usr/share/nginx/html;index index.html;try_files $uri $uri/ /index.html;}location /api {proxy_pass http://server:5000;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;}
}

六、项目扩展方向

1. 性能优化建议

  • 实现前端缓存策略
  • 添加服务端渲染(SSR)支持
  • 使用CDN加速静态资源
  • 优化数据库查询索引

2. 功能扩展建议

  • 实现文章草稿自动保存
  • 添加文章系列功能
  • 集成第三方登录(OAuth)
  • 开发移动端应用
  • 实现全文搜索功能

3. 安全增强建议

  • 实现CSRF防护
  • 添加速率限制
  • 增强输入验证
  • 定期安全审计

通过本实战教程,您已经掌握了使用Vue3和Node.js开发全栈博客系统的完整流程。从项目架构设计到具体功能实现,再到最终部署上线,这套技术栈能够满足现代Web应用开发的各项需求。建议在此基础上继续探索更高级的功能和优化方案,打造更加完善的博客平台。

http://www.xdnf.cn/news/5517.html

相关文章:

  • Python-MCPAgent开发-DeepSeek版本
  • MySQL索引原理以及SQL优化(二)
  • 【更新至2023年】1999-2023年上市公司人工智能词频统计数据(年报词频统计)
  • RGA模块讲解
  • 低代码平台与 AI 融合:从 Activity 流程到智能 ITSM 的落地实践
  • 单片机-STM32部分:12、I2C
  • 2003-2022年 地级市-政府干预程度指标数据-社科数据
  • springboot3整合SpringSecurity实现登录校验与权限认证
  • c++ 类的语法2
  • Windows使用虚拟环境执行sh脚本
  • 【深度学习】将本地工程上传到Colab运行的方法
  • (十一)Java面向对象进阶:深入理解抽象类、接口与内部类
  • 如何使用依赖注入来实现依赖倒置原则?
  • RK35XX 环境搭建
  • [ERTS2012] 航天器星载软件形式化模型驱动研发 —— 对 Scade 语言本身的影响
  • python打卡训练营打卡记录day22
  • Java SSM 框架(详解)
  • Java 多态:原理与实例深度剖析
  • 【Java学习日记36】:javabeen学生系统
  • [思维模式-30]:《本质思考力》-30- 计划经济与市场经济结合中的“自顶向下”与“自底向上”思维模式。
  • PXE安装Ubuntu系统
  • 免安装 + 快速响应Photoshop CS6 精简版低配置电脑修图
  • 计算机网络笔记(二十二)——4.4网际控制报文协议ICMP
  • # Anaconda3 常用命令
  • Grafana v12.0 引入了多项新功能和改进
  • KAG:通过知识增强生成提升专业领域的大型语言模型(四)
  • 【LeetCode Hot100 | 每日刷题】排序数组
  • 内存泄露,如何判断是资源泄露还是堆栈泄露?
  • Telnetlib 库完全指南
  • MySQL 索引与事务详解