本文是一篇详尽的"保姆级"教程,指导读者从零开始使用 Vue.js (前端)和 Flask(后端)构建一个前后端分离的博客系统。
核心内容概要:
- 项目目标:创建一个具备博客列表展示、详情查看、文章创建、编辑和删除功能的完整Web应用。
- 技术栈 :
- 前端:Vue 3 + Vue Router + Element Plus (UI框架) + Axios (HTTP客户端)。
- 后端:Flask + Flask-CORS (处理跨域)。
- 通信:RESTful API。
- 关键步骤 :
- 环境准备:安装Node.js、Python、Vue CLI及必要依赖。
- 后端搭建:用Flask编写API接口(GET/POST/PUT/DELETE),模拟数据存储,并启用CORS。
- 前端搭建 :用Vue CLI初始化项目,集成Element Plus,配置
vue.config.js通过开发服务器代理解决跨域问题。 - 数据交互:使用Axios封装请求,实现前后端数据通信。
- 页面开发:创建博客列表、详情、编辑/创建等Vue组件,并通过Vue Router进行路由管理。
- 重点解决方案 :详细讲解了开发环境中前后端分离导致的跨域问题,并采用Vue CLI的代理功能作为推荐解决方案。
- 成果与展望:最终得到一个可运行的全栈应用,并指出了后续可扩展的方向(如接入真实数据库、用户认证、生产部署等)。
引言
在当今的Web开发领域,前后端分离已成为主流架构。它将前端(用户界面)与后端(数据处理和业务逻辑)解耦,使得开发更高效、维护更方便、扩展性更强。
本教程将手把手带你使用 Vue.js (前端框架)和 Flask (后端微框架)从零开始搭建一个简单的博客系统。我们将涵盖项目初始化、环境配置、接口设计、跨域处理、数据交互等核心知识点,力求做到"保姆级"细致讲解,确保你跟着步骤操作,一定能跑通项目!
项目最终效果:
- 前端:Vue项目,展示博客列表、文章详情。
- 后端:Flask项目,提供RESTful API接口,管理博客数据(增删改查)。
- 技术栈:Vue 3 + Element Plus (UI) + Axios (HTTP) + Flask + Flask-CORS + Python
第一部分:环境准备
1.1 安装 Node.js 和 npm
Vue项目依赖Node.js环境。
-
访问 https://nodejs.org/ 下载并安装 LTS 版本。
-
安装完成后,打开终端(命令行),输入:
node -v
npm -v -
若显示版本号(如 v18.x.x 和 8.x.x),则安装成功。
1.2 安装 Python 和 pip
Flask是Python的Web框架。
- 访问 https://www.python.org/ 下载并安装 Python 3.8+。
- 安装时务必勾选 "Add Python to PATH"。
- 安装完成后,打开终端,输入:
python
python --version
pip --version
- 若显示版本号,则安装成功。
1.3 安装全局工具
- 安装 Vue CLI (用于创建和管理Vue项目)
python
npm install -g @vue/cli
安装 Flask 和 Flask-CORS (Flask-CORS用于处理跨域)
python
pip install flask flask-cors
第二部分:创建后端 Flask 项目
我们先创建后端API服务。
2.1 初始化项目目录
python
# 创建项目根目录
mkdir vue-flask-blog
cd vue-flask-blog
# 创建后端目录
mkdir backend
cd backend
2.2 创建 Flask 应用
在 backend/ 目录下,创建 app.py 文件:
python
# backend/app.py
from flask import Flask, jsonify, request
from flask_cors import CORS # 导入CORS扩展
# 初始化Flask应用
app = Flask(__name__)
# 启用CORS,允许所有域名访问(生产环境请配置具体域名)
CORS(app)
# 模拟数据库:使用列表存储博客文章
# 实际项目应使用数据库如 SQLite, MySQL, PostgreSQL
blogs = [
{"id": 1, "title": "我的第一篇博客", "content": "Hello World! 这是我的第一篇博客内容。"},
{"id": 2, "title": "学习Vue和Flask", "content": "今天开始学习Vue和Flask前后端分离开发。"}
]
# 全局变量,用于生成新ID
next_id = 3
# ========== RESTful API 路由 ==========
@app.route('/api/blogs', methods=['GET'])
def get_blogs():
"""获取所有博客列表"""
return jsonify({"success": True, "data": blogs})
@app.route('/api/blogs/<int:blog_id>', methods=['GET'])
def get_blog(blog_id):
"""根据ID获取单篇博客"""
blog = next((b for b in blogs if b["id"] == blog_id), None)
if blog:
return jsonify({"success": True, "data": blog})
else:
return jsonify({"success": False, "message": "博客不存在"}), 404
@app.route('/api/blogs', methods=['POST'])
def create_blog():
"""创建一篇新博客"""
global next_id
data = request.get_json()
# 简单验证
if not data or 'title' not in data or 'content' not in data:
return jsonify({"success": False, "message": "标题和内容不能为空"}), 400
new_blog = {
"id": next_id,
"title": data['title'],
"content": data['content']
}
blogs.append(new_blog)
next_id += 1
return jsonify({"success": True, "data": new_blog}), 201
@app.route('/api/blogs/<int:blog_id>', methods=['PUT'])
def update_blog(blog_id):
"""更新博客"""
data = request.get_json()
blog = next((b for b in blogs if b["id"] == blog_id), None)
if not blog:
return jsonify({"success": False, "message": "博客不存在"}), 404
if 'title' in data:
blog['title'] = data['title']
if 'content' in data:
blog['content'] = data['content']
return jsonify({"success": True, "data": blog})
@app.route('/api/blogs/<int:blog_id>', methods=['DELETE'])
def delete_blog(blog_id):
"""删除博客"""
global blogs
blog = next((b for b in blogs if b["id"] == blog_id), None)
if not blog:
return jsonify({"success": False, "message": "博客不存在"}), 404
blogs = [b for b in blogs if b["id"] != blog_id]
return jsonify({"success": True, "message": "删除成功"})
# ========== 根路径,用于测试 ==========
@app.route('/')
def index():
return "<h1>Flask Blog API Running!</h1>"
if __name__ == '__main__':
app.run(debug=True, host='127.0.0.1', port=5000)
2.3 启动后端服务
在 backend/ 目录下运行:
python
python app.py
如果看到类似 Running on http://127.0.0.1:5000 的提示,说明后端已启动。
测试API: 打开浏览器,访问 http://127.0.0.1:5000/api/blogs,你应该能看到返回的JSON数据。
重要提示: 后端运行在
5000端口。
第三部分:创建前端 Vue 项目
现在我们创建前端用户界面。
3.1 初始化 Vue 项目
在项目根目录 vue-flask-blog/ 下执行:
python
cd ..
vue create frontend
- 选择 "Manually select features"
- 勾选:
Babel,Router,Vuex(可选,本例不用),CSS Pre-processors(选Sass/SCSS),Linter - Vue 版本选择 3.x
- 使用
history模式 (输入y) - CSS 预处理器选
Sass/SCSS - Linter 选
ESLint + Prettier - 保存预设:
n
等待安装完成。
3.2 进入前端目录并安装 UI 库
python
cd frontend
# 安装 Element Plus (优秀的Vue 3 UI组件库)
npm install element-plus
# 安装 Axios (用于HTTP请求)
npm install axios
3.3 配置 Element Plus
编辑 src/main.js:
python
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
// 1. 引入Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' // 2. 引入样式
// 可选:引入中文语言包
import zhCn from 'element-plus/es/locale/lang/zh-cn'
const app = createApp(App)
// 3. 使用Element Plus
app.use(router)
app.use(ElementPlus, { locale: zhCn }) // 使用中文
app.mount('#app')
3.4 配置 Axios 和跨域代理
由于前端(localhost:8080)和后端(localhost:5000)端口不同,会产生跨域问题 。我们使用Vue CLI的开发服务器代理功能来解决。
编辑 frontend/vue.config.js (如果不存在则创建):
javascript
// frontend/vue.config.js
module.exports = {
devServer: {
port: 8080, // 前端开发服务器端口
proxy: {
'/api': { // 拦截所有以 /api 开头的请求
target: 'http://127.0.0.1:5000', // 后端地址
changeOrigin: true, // 改变源
pathRewrite: {
'^/api': '' // 重写路径,去掉 /api 前缀
}
}
}
}
}
原理: 前端请求
/api/blogs时,开发服务器会将其代理到http://127.0.0.1:5000/blogs,从而避免浏览器的跨域限制。
3.5 创建 Axios 实例
创建统一的HTTP请求配置。
在 src/ 目录下创建 utils/request.js:
java
// src/utils/request.js
import axios from 'axios'
// 创建axios实例
const service = axios.create({
baseURL: '/api', // 所有请求都会加上 /api 前缀
timeout: 5000 // 请求超时时间
})
// 请求拦截器(可选,用于添加token等)
service.interceptors.request.use(
config => {
// 例如:config.headers['Authorization'] = 'Bearer ' + token
return config
},
error => {
console.error('请求错误:', error)
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
const res = response.data
// 假设后端返回 { success: true/false, data: ... }
if (res.success) {
return res
} else {
// 可在此处统一处理业务错误
console.error('API错误:', res.message || '未知错误')
return Promise.reject(new Error(res.message || 'Error'))
}
},
error => {
console.error('响应错误:', error)
// 可在此处统一处理网络错误、404、500等
return Promise.reject(error)
}
)
export default service
3.6 创建博客相关组件和页面
3.6.1 创建博客列表页面
创建 src/views/BlogList.vue:
java
<template>
<div class="blog-list">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>博客列表</span>
<el-button @click="goToCreate" type="primary">写新博客</el-button>
</div>
</template>
<el-table :data="blogs" style="width: 100%" v-loading="loading">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="标题" />
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button size="small" @click="handleView(scope.row)">查看</el-button>
<el-button size="small" type="primary" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
import request from '@/utils/request'
export default {
name: 'BlogList',
data() {
return {
blogs: [],
loading: false
}
},
created() {
this.fetchBlogs()
},
methods: {
async fetchBlogs() {
this.loading = true
try {
const res = await request.get('/blogs')
this.blogs = res.data
} catch (error) {
this.$message.error('获取博客列表失败')
} finally {
this.loading = false
}
},
handleView(blog) {
this.$router.push(`/blog/${blog.id}`)
},
handleEdit(blog) {
this.$router.push(`/edit/${blog.id}`)
},
handleDelete(blog) {
this.$confirm(`确定要删除 "${blog.title}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await request.delete(`/blogs/${blog.id}`)
this.$message.success('删除成功')
this.fetchBlogs() // 刷新列表
} catch (error) {
this.$message.error('删除失败')
}
}).catch(() => {
// 用户取消
})
},
goToCreate() {
this.$router.push('/create')
}
}
}
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
3.6.2 创建博客详情页面
创建 src/views/BlogDetail.vue:
java
<template>
<div class="blog-detail">
<el-card class="box-card">
<template #header>
<div class="card-header">
<el-button @click="$router.back()" icon="Back" circle />
<span>{{ blog.title }}</span>
</div>
</template>
<div class="content" v-html="blog.content"></div>
<div class="actions">
<el-button @click="$router.push(`/edit/${blog.id}`)" type="primary">编辑</el-button>
</div>
</el-card>
</div>
</template>
<script>
import request from '@/utils/request'
export default {
name: 'BlogDetail',
data() {
return {
blog: { id: 0, title: '', content: '' }
}
},
async created() {
const id = this.$route.params.id
try {
const res = await request.get(`/blogs/${id}`)
this.blog = res.data
} catch (error) {
this.$message.error('获取博客详情失败')
this.$router.back()
}
}
}
</script>
<style scoped>
.content {
line-height: 1.8;
font-size: 16px;
white-space: pre-line; /* 保留换行符 */
}
.actions {
margin-top: 20px;
text-align: right;
}
</style>
3.6.3 创建博客编辑/创建页面
创建 src/views/BlogForm.vue:
java
<template>
<div class="blog-form">
<el-card class="box-card">
<template #header>
<div class="card-header">
<el-button @click="$router.back()" icon="Back" circle />
<span>{{ isEdit ? '编辑博客' : '创建博客' }}</span>
</div>
</template>
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入博客标题" />
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input
v-model="form.content"
type="textarea"
:rows="10"
placeholder="请输入博客内容"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="$router.back()">取消</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import request from '@/utils/request'
export default {
name: 'BlogForm',
data() {
return {
form: {
title: '',
content: ''
},
rules: {
title: [
{ required: true, message: '请输入标题', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入内容', trigger: 'blur' }
]
}
}
},
computed: {
isEdit() {
return !!this.$route.params.id
}
},
async created() {
// 如果是编辑模式,获取现有博客数据
if (this.isEdit) {
const id = this.$route.params.id
try {
const res = await request.get(`/blogs/${id}`)
this.form = { ...res.data }
} catch (error) {
this.$message.error('获取博客数据失败')
this.$router.back()
}
}
},
methods: {
submitForm() {
this.$refs.formRef.validate(async (valid) => {
if (valid) {
try {
if (this.isEdit) {
// 编辑
await request.put(`/blogs/${this.$route.params.id}`, this.form)
this.$message.success('更新成功')
} else {
// 创建
await request.post('/blogs', this.form)
this.$message.success('创建成功')
}
this.$router.push('/') // 返回列表页
} catch (error) {
this.$message.error(this.isEdit ? '更新失败' : '创建失败')
}
}
})
}
}
}
</script>
<style scoped>
.el-form {
max-width: 800px;
}
</style>
3.7 配置 Vue Router
编辑 src/router/index.js:
javascript
import { createRouter, createWebHistory } from 'vue-router'
import BlogList from '../views/BlogList.vue'
import BlogDetail from '../views/BlogDetail.vue'
import BlogForm from '../views/BlogForm.vue'
const routes = [
{
path: '/',
name: 'Home',
component: BlogList
},
{
path: '/blog/:id',
name: 'BlogDetail',
component: BlogDetail
},
{
path: '/create',
name: 'CreateBlog',
component: BlogForm
},
{
path: '/edit/:id',
name: 'EditBlog',
component: BlogForm
},
// 404 页面(可选)
{
path: '/:pathMatch(.*)*',
redirect: '/'
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
3.8 修改 App.vue
编辑 src/App.vue,使用路由视图:
javascript
<template>
<div id="app">
<router-view />
</div>
</template>
<style>
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
}
#app {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
</style>
第四部分:启动项目并测试
4.1 启动后端服务
打开一个终端,进入 backend 目录:
javascript
cd backend
python app.py
确保后端在 5000 端口运行。
4.2 启动前端服务
打开另一个终端,进入 frontend 目录:
javascript
cd frontend
npm run serve
前端开发服务器通常在 8080 端口启动。
4.3 访问应用
打开浏览器,访问 http://localhost:8080。
你应该能看到博客列表页面。尝试以下操作:
- 查看博客:点击"查看"按钮,进入详情页。
- 创建博客:点击"写新博客",填写标题和内容,提交。
- 编辑博客:在列表页点击"编辑",修改后提交。
- 删除博客:点击"删除",确认后删除。
所有操作都应通过API与后端交互,并实时更新前端数据。
第五部分:常见问题与解决方案
Q1: 后端启动报错 Address already in use
- 原因:5000端口被占用。
- 解决 :在
app.py中修改app.run(port=5001)换一个端口,并同步修改vue.config.js中的target。
Q2: 前端页面空白,控制台报错404
- 检查 :确保
vue.config.js配置正确,代理生效。 - 检查:后端服务是否正常运行。
- 重启 :重启前端开发服务器
npm run serve。
Q3: Axios请求返回CORS错误
- 原因:代理未生效或后端CORS未启用。
- 解决 :
- 确认
backend/app.py中CORS(app)已添加。 - 确认
frontend/vue.config.js代理配置正确。 - 确保请求URL以
/api开头(如request.get('/api/blogs'))。
- 确认
Q4: 如何连接真实数据库?
- 推荐 :使用
SQLAlchemy(Flask-SQLAlchemy) + SQLite/MySQL。 - 步骤 :
pip install flask-sqlalchemy- 在Flask中配置数据库连接。
- 定义
Blog模型类。 - 将
blogs列表操作替换为数据库的CRUD操作。