全栈项目实战: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应用开发的各项需求。建议在此基础上继续探索更高级的功能和优化方案,打造更加完善的博客平台。