视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
一、为什么需要前端篇?
在上一篇《后端篇》中,我们已经用Spring Boot + MyBatis-Plus搭建好了博客系统的 RESTful API。现在,是时候让这些接口“活”起来——通过Vue 3 + Vite + Axios构建一个清爽、响应式的博客前台页面。
即使你是 Vue 小白,只要会 HTML 和一点 JavaScript,也能跟着一步步完成!
二、需求场景回顾
我们的博客前端要实现以下功能:
- 首页:展示所有文章列表(标题 + 分类 + 发布时间);
- 文章详情页:点击某篇文章,查看完整内容;
- 简洁美观:不需要复杂后台,但要有良好的阅读体验;
- 与后端无缝对接:调用上一篇写好的
/api/posts接口。
⚠️ 注意:本篇聚焦前台展示(读者视角),不包含后台管理(如写文章、登录等),后续可扩展。
三、技术选型(前端)
| 技术 | 作用 |
|---|---|
| Vue 3 (Composition API) | 响应式框架,构建用户界面 |
| Vite | 新一代前端构建工具,启动快、热更新快 |
| Axios | 发送 HTTP 请求,调用后端 API |
| Vue Router | 实现页面路由跳转(首页 ↔ 详情页) |
| Tailwind CSS(可选) | 快速美化 UI(本文用原生 CSS 简化) |
四、项目初始化
1. 创建 Vue 3 项目(使用 Vite)
npm create vue@latest blog-frontend按提示选择:
- ✔ Project name:
blog-frontend - ✔ Add Vue Router for Single Page Application development?Yes
- 其他选 No(简化)
进入目录并安装依赖:
cd blog-frontend npm install npm install axios npm run dev默认访问http://localhost:5173
五、核心代码实现
1. 配置 Axios(统一 API 调用)
创建src/api/index.js:
// src/api/index.js import axios from 'axios'; const api = axios.create({ baseURL: 'http://localhost:8080/api', // 后端地址(Spring Boot 默认端口) timeout: 5000, }); export default api;💡 注意:如果你的 Spring Boot 运行在 8080,而 Vue 在 5173,会遇到跨域问题。
临时解决方案:在 Spring Boot 的 Controller 上加@CrossOrigin(开发阶段可用)。
@RestController @RequestMapping("/api/posts") @CrossOrigin(origins = "http://localhost:5173") // ← 加这一行 public class PostController { // ... }2. 定义数据类型(TypeScript 可选,这里用 JS 注释说明)
我们假设后端返回的文章结构如下:
{ "id": 1, "title": "Spring Boot 入门", "content": "详细内容...", "categoryName": "Java", "createTime": "2026-01-05T10:00:00" }3. 首页:文章列表(HomeView.vue)
修改src/views/HomeView.vue:
<template> <div class="container"> <h1>我的技术博客</h1> <div v-if="loading">加载中...</div> <div v-else-if="error" class="error">{{ error }}</div> <div v-else class="posts"> <div v-for="post in posts" :key="post.id" class="post-item" @click="goToDetail(post.id)" > <h2>{{ post.title }}</h2> <p class="meta"> <span class="category">{{ post.categoryName }}</span> <span class="time">{{ formatDate(post.createTime) }}</span> </p> </div> </div> </div> </template> <script setup> import { ref, onMounted } from 'vue'; import { useRouter } from 'vue-router'; import api from '@/api'; const posts = ref([]); const loading = ref(true); const error = ref(null); const router = useRouter(); onMounted(async () => { try { const response = await api.get('/posts'); posts.value = response.data; } catch (err) { error.value = '加载文章失败,请检查后端是否运行'; console.error(err); } finally { loading.value = false; } }); function goToDetail(id) { router.push(`/post/${id}`); } function formatDate(dateString) { const date = new Date(dateString); return date.toLocaleDateString('zh-CN'); } </script> <style scoped> .container { max-width: 800px; margin: 0 auto; padding: 20px; } .post-item { border-bottom: 1px solid #eee; padding: 15px 0; cursor: pointer; } .post-item:hover { background-color: #f9f9f9; } .meta { color: #666; font-size: 0.9em; } .category { background: #e0f7fa; padding: 2px 6px; border-radius: 4px; margin-right: 10px; } </style>4. 详情页:文章内容(PostView.vue)
创建src/views/PostView.vue:
<template> <div class="container"> <button @click="goBack" class="back-btn">← 返回首页</button> <div v-if="loading">加载中...</div> <div v-else-if="error" class="error">{{ error }}</div> <article v-else> <h1>{{ post.title }}</h1> <div class="meta"> <span class="category">{{ post.categoryName }}</span> <span class="time">{{ formatDate(post.createTime) }}</span> </div> <div class="content" v-html="post.content"></div> </article> </div> </template> <script setup> import { ref, onMounted } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import api from '@/api'; const route = useRoute(); const router = useRouter(); const post = ref(null); const loading = ref(true); const error = ref(null); onMounted(async () => { const id = route.params.id; try { const response = await api.get(`/posts/${id}`); post.value = response.data; } catch (err) { error.value = '文章不存在或加载失败'; } finally { loading.value = false; } }); function goBack() { router.go(-1); } function formatDate(dateString) { const date = new Date(dateString); return date.toLocaleDateString('zh-CN'); } </script> <style scoped> .container { max-width: 800px; margin: 0 auto; padding: 20px; } .back-btn { margin-bottom: 20px; background: #007bff; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; } .content { line-height: 1.6; margin-top: 20px; } </style>5. 配置路由(router/index.js)
确保src/router/index.js包含详情页路由:
import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' import PostView from '../views/PostView.vue' const routes = [ { path: '/', name: 'home', component: HomeView }, { path: '/post/:id', name: 'post', component: PostView, props: true } ] const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes }) export default router六、反例 & 常见坑点
❌ 反例1:直接在模板里写v-html不过滤(XSS 风险)
<!-- 危险!如果 content 来自用户输入,可能注入恶意脚本 --> <div v-html="post.content"></div>✅ 正确做法:
- 如果内容是你自己写的(可信),可接受;
- 如果支持用户评论/投稿,必须用 DOMPurify 等库过滤 HTML。
❌ 反例2:硬编码 API 地址
// 不好维护! axios.get('http://localhost:8080/api/posts')✅ 正确做法:统一配置baseURL(如我们做的api.js)。
❌ 反例3:忽略加载状态和错误处理
用户看到空白页面会以为“网站坏了”。
✅ 正确做法:显示 loading / error 提示,提升用户体验。
七、注意事项
- 跨域问题:开发时前后端端口不同(8080 vs 5173),务必处理 CORS;
- 后端必须运行:启动 Spring Boot 项目后再运行 Vue;
- 内容安全:若未来支持富文本编辑,务必对 HTML 进行消毒;
- 部署建议:生产环境可将 Vue 打包后放入 Spring Boot 的
static目录,或用 Nginx 代理。
八、效果预览
- 启动后端:
./mvnw spring-boot:run - 启动前端:
npm run dev - 访问
http://localhost:5173 - 点击文章 → 跳转详情页 ✅
九、下一步扩展方向
- 添加Markdown 渲染(用
marked库); - 增加分页或无限滚动;
- 实现搜索和分类筛选;
- 搭建后台管理页面(需登录 + JWT 鉴权)。
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!