Vue + Flask 前后端分离项目实战:从零搭建一个完整博客系统

本文是一篇详尽的"保姆级"教程,指导读者从零开始使用 Vue.js (前端)和 Flask(后端)构建一个前后端分离的博客系统。

核心内容概要:

  1. 项目目标:创建一个具备博客列表展示、详情查看、文章创建、编辑和删除功能的完整Web应用。
  2. 技术栈
    • 前端:Vue 3 + Vue Router + Element Plus (UI框架) + Axios (HTTP客户端)。
    • 后端:Flask + Flask-CORS (处理跨域)。
    • 通信:RESTful API。
  3. 关键步骤
    • 环境准备:安装Node.js、Python、Vue CLI及必要依赖。
    • 后端搭建:用Flask编写API接口(GET/POST/PUT/DELETE),模拟数据存储,并启用CORS。
    • 前端搭建 :用Vue CLI初始化项目,集成Element Plus,配置vue.config.js通过开发服务器代理解决跨域问题。
    • 数据交互:使用Axios封装请求,实现前后端数据通信。
    • 页面开发:创建博客列表、详情、编辑/创建等Vue组件,并通过Vue Router进行路由管理。
  4. 重点解决方案 :详细讲解了开发环境中前后端分离导致的跨域问题,并采用Vue CLI的代理功能作为推荐解决方案。
  5. 成果与展望:最终得到一个可运行的全栈应用,并指出了后续可扩展的方向(如接入真实数据库、用户认证、生产部署等)。

引言

在当今的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

你应该能看到博客列表页面。尝试以下操作:

  1. 查看博客:点击"查看"按钮,进入详情页。
  2. 创建博客:点击"写新博客",填写标题和内容,提交。
  3. 编辑博客:在列表页点击"编辑",修改后提交。
  4. 删除博客:点击"删除",确认后删除。

所有操作都应通过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未启用。
  • 解决
    1. 确认 backend/app.pyCORS(app) 已添加。
    2. 确认 frontend/vue.config.js 代理配置正确。
    3. 确保请求URL以 /api 开头(如 request.get('/api/blogs'))。

Q4: 如何连接真实数据库?

  • 推荐 :使用 SQLAlchemy (Flask-SQLAlchemy) + SQLite/MySQL。
  • 步骤
    1. pip install flask-sqlalchemy
    2. 在Flask中配置数据库连接。
    3. 定义 Blog 模型类。
    4. blogs 列表操作替换为数据库的CRUD操作。

相关推荐
Ticnix7 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人7 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl7 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人7 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼7 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空7 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust
Mr Xu_8 小时前
Vue 3 中计算属性的最佳实践:提升可读性、可维护性与性能
前端·javascript
jerrywus8 小时前
我写了个 Claude Code Skill,再也不用手动切图传 COS 了
前端·agent·claude
larance8 小时前
Gunicorn + Nginx+systemd 配置flask
nginx·flask·gunicorn