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操作。

相关推荐
格砸5 小时前
从入门到辞职|从ChatGPT到OpenClaw,跟上智能时代的进化
前端·人工智能·后端
Live000006 小时前
在鸿蒙中使用 Repeat 渲染嵌套列表,修改内层列表的一个元素,页面不会更新
前端·javascript·react native
柳杉6 小时前
使用Ai从零开发智慧水利态势感知大屏(开源)
前端·javascript·数据可视化
兆子龙6 小时前
从高阶函数到 Hooks:React 如何减轻开发者的心智负担(含 Demo + ahooks 推荐)
前端
狗胜6 小时前
测试文章 - API抓取
前端
三小河6 小时前
VS Code 集成 claude-code 教程:告别海外限制,无缝对接国内大模型
前端·程序员
jerrywus6 小时前
前端老哥的救命稻草:用 Obsidian 搞定 Claude Code 的「金鱼记忆」
前端·agent·claude
球球pick小樱花7 小时前
游戏官网前端工具库:海内外案例解析
前端·javascript·css
前端Hardy7 小时前
干掉 Virtual DOM?尤雨溪开始"强推" Vapor Mode?
vue.js·vue-router