Python之Flask开发框架(第五篇)- 使Flask + Vue 构建前后端分离项目教程

前后端分离已成为现代 Web 开发的主流模式。后端专注提供 API 服务,前端负责界面交互,两者独立开发、独立部署,极大地提高了开发效率和项目可维护性。本文将带领你从零开始,使用 Flask 作为后端框架,Vue.js 作为前端框架,搭建一个完整的前后端分离项目。


一、技术栈概览

  • 后端:Flask + Flask-RESTx(构建 RESTful API)+ Flask-CORS(处理跨域)+ Flask-SQLAlchemy(ORM)+ PyJWT(JWT 认证)
  • 前端:Vue 3 + Vue Router + Vuex(或 Pinia)+ Axios(HTTP 请求)
  • 数据库:SQLite(开发环境)/ MySQL / PostgreSQL
  • 部署:Gunicorn + Nginx(后端),Nginx 代理前端静态文件

二、项目初始化与目录结构

建议分别创建两个独立目录:backendfrontend

复制代码
myproject/
├── backend/
│   ├── app/
│   │   ├── __init__.py
│   │   ├── models.py
│   │   ├── api/
│   │   │   ├── __init__.py
│   │   │   ├── auth.py
│   │   │   └── posts.py
│   │   ├── config.py
│   │   └── utils.py
│   ├── requirements.txt
│   ├── run.py
│   └── .env
└── frontend/
    ├── public/
    ├── src/
    │   ├── api/
    │   ├── components/
    │   ├── router/
    │   ├── store/
    │   ├── views/
    │   ├── App.vue
    │   └── main.js
    ├── package.json
    └── vue.config.js

三、后端 Flask 项目搭建

3.1 环境准备

bash 复制代码
cd backend
python -m venv venv
source venv/bin/activate   # Windows: venv\Scripts\activate

3.2 安装依赖

创建 requirements.txt

复制代码
flask==2.3.3
flask-restx==1.1.0
flask-cors==4.0.0
flask-sqlalchemy==3.1.1
flask-migrate==4.0.5
pyjwt==2.8.0
python-dotenv==1.0.0

安装:

bash 复制代码
pip install -r requirements.txt

3.3 配置文件 config.py

python 复制代码
import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///app.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    JWT_EXPIRATION_HOURS = 24

3.4 应用工厂 app/__init__.py

python 复制代码
from flask import Flask
from flask_restx import Api
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import Config

db = SQLAlchemy()
migrate = Migrate()

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)

    db.init_app(app)
    migrate.init_app(app, db)
    CORS(app)  # 允许所有跨域请求

    api = Api(app, doc='/docs/', prefix='/api')
    
    # 注册命名空间
    from app.api.auth import auth_ns
    from app.api.posts import posts_ns
    api.add_namespace(auth_ns)
    api.add_namespace(posts_ns)

    return app

3.5 数据模型 app/models.py

python 复制代码
from app import db
from datetime import datetime

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(128))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    author = db.relationship('User', backref='posts')

3.6 JWT 工具函数 app/utils.py

python 复制代码
import jwt
from datetime import datetime, timedelta
from flask import current_app
from app.models import User

def generate_jwt(user_id):
    payload = {
        'user_id': user_id,
        'exp': datetime.utcnow() + timedelta(hours=current_app.config['JWT_EXPIRATION_HOURS']),
        'iat': datetime.utcnow()
    }
    token = jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
    return token

def verify_jwt(token):
    try:
        payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
        user = User.query.get(payload['user_id'])
        return user
    except:
        return None

3.7 API 命名空间 app/api/auth.py

python 复制代码
from flask_restx import Namespace, Resource, fields
from flask import request
from werkzeug.security import generate_password_hash, check_password_hash
from app import db
from app.models import User
from app.utils import generate_jwt

auth_ns = Namespace('auth', description='认证相关操作')

# 定义序列化模型
register_model = auth_ns.model('Register', {
    'username': fields.String(required=True),
    'email': fields.String(required=True),
    'password': fields.String(required=True)
})

login_model = auth_ns.model('Login', {
    'email': fields.String(required=True),
    'password': fields.String(required=True)
})

@auth_ns.route('/register')
class Register(Resource):
    @auth_ns.expect(register_model)
    def post(self):
        data = request.json
        if User.query.filter_by(username=data['username']).first():
            return {'message': '用户名已存在'}, 400
        if User.query.filter_by(email=data['email']).first():
            return {'message': '邮箱已注册'}, 400
        user = User(username=data['username'], email=data['email'])
        user.password_hash = generate_password_hash(data['password'])
        db.session.add(user)
        db.session.commit()
        return {'message': '注册成功'}, 201

@auth_ns.route('/login')
class Login(Resource):
    @auth_ns.expect(login_model)
    def post(self):
        data = request.json
        user = User.query.filter_by(email=data['email']).first()
        if user and check_password_hash(user.password_hash, data['password']):
            token = generate_jwt(user.id)
            return {'access_token': token, 'user': {'id': user.id, 'username': user.username, 'email': user.email}}
        return {'message': '邮箱或密码错误'}, 401

3.8 API 命名空间 app/api/posts.py

python 复制代码
from flask_restx import Namespace, Resource, fields
from flask import request
from flask_jwt_extended import jwt_required, get_jwt_identity
from app import db
from app.models import Post

posts_ns = Namespace('posts', description='文章相关操作')

post_model = posts_ns.model('Post', {
    'id': fields.Integer,
    'title': fields.String,
    'content': fields.String,
    'created_at': fields.DateTime,
    'user_id': fields.Integer,
    'author': fields.String(attribute=lambda p: p.author.username)
})

@posts_ns.route('/')
class PostList(Resource):
    @posts_ns.marshal_list_with(post_model)
    def get(self):
        posts = Post.query.order_by(Post.created_at.desc()).all()
        return posts

    @posts_ns.expect(post_model)
    @posts_ns.marshal_with(post_model, code=201)
    @jwt_required()
    def post(self):
        data = request.json
        user_id = get_jwt_identity()
        post = Post(title=data['title'], content=data['content'], user_id=user_id)
        db.session.add(post)
        db.session.commit()
        return post, 201

@posts_ns.route('/<int:id>')
class PostResource(Resource):
    @posts_ns.marshal_with(post_model)
    def get(self, id):
        post = Post.query.get_or_404(id)
        return post

    @posts_ns.expect(post_model)
    @posts_ns.marshal_with(post_model)
    @jwt_required()
    def put(self, id):
        post = Post.query.get_or_404(id)
        user_id = get_jwt_identity()
        if post.user_id != user_id:
            return {'message': '无权限'}, 403
        data = request.json
        post.title = data.get('title', post.title)
        post.content = data.get('content', post.content)
        db.session.commit()
        return post

    @jwt_required()
    def delete(self, id):
        post = Post.query.get_or_404(id)
        user_id = get_jwt_identity()
        if post.user_id != user_id:
            return {'message': '无权限'}, 403
        db.session.delete(post)
        db.session.commit()
        return {'message': '删除成功'}

注意:上述代码中使用了 @jwt_required,但实际需要在 app/__init__.py 中配置 Flask-JWT-Extended。我们已在 utils.py 中手动处理 JWT,所以这里应修改为自定义装饰器。为了简化,我们可以直接使用 Flask-JWT-Extended 扩展。建议安装 flask-jwt-extended,并在 create_app 中初始化。为保持教程简洁,我们将手动实现一个简单的 JWT 验证装饰器。

创建 app/decorators.py

python 复制代码
from functools import wraps
from flask import request, jsonify
from app.utils import verify_jwt

def jwt_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth_header = request.headers.get('Authorization')
        if not auth_header:
            return jsonify({'message': '缺少token'}), 401
        try:
            token = auth_header.split(' ')[1]
        except IndexError:
            return jsonify({'message': 'token格式错误'}), 401
        user = verify_jwt(token)
        if not user:
            return jsonify({'message': '无效或过期的token'}), 401
        # 将当前用户注入到请求上下文
        request.current_user = user
        return f(*args, **kwargs)
    return decorated

然后在 posts.py 中使用该装饰器,并获取用户:user = request.current_user

3.9 启动文件 run.py

python 复制代码
from app import create_app

app = create_app()

if __name__ == '__main__':
    app.run(debug=True)

3.10 初始化数据库

bash 复制代码
flask db init
flask db migrate -m "initial migration"
flask db upgrade

3.11 测试后端 API

启动服务:python run.py,访问 http://localhost:5000/docs/ 查看 Swagger 文档,测试注册、登录、文章增删改查。


四、前端 Vue 项目搭建

4.1 创建 Vue 项目

确保已安装 Node.js 和 npm,全局安装 Vue CLI:

bash 复制代码
npm install -g @vue/cli

创建项目:

bash 复制代码
cd frontend
vue create frontend
# 选择 Vue 3, Vue Router, Vuex (或 Pinia)

4.2 安装依赖

bash 复制代码
npm install axios

4.3 目录结构调整

src 下新建以下目录:

  • api/:封装后端请求
  • views/:页面组件
  • components/:可复用组件
  • router/:路由配置
  • store/:状态管理

4.4 环境变量配置

创建 .env.development 文件:

复制代码
VUE_APP_API_BASE_URL=http://localhost:5000/api

vue.config.js 中配置代理(开发环境解决跨域):

javascript 复制代码
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:5000',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }  // 如果后端接口不带 /api 前缀
      }
    }
  }
}

这样,前端请求 /api/login 会被代理到 http://localhost:5000/login。如果后端接口统一前缀 /api,则无需 pathRewrite

4.5 封装 Axios 请求

src/api/request.js

javascript 复制代码
import axios from 'axios'

const request = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL,
  timeout: 10000
})

// 请求拦截器:添加 token
request.interceptors.request.use(config => {
  const token = localStorage.getItem('access_token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// 响应拦截器:处理错误
request.interceptors.response.use(
  response => response.data,
  error => {
    if (error.response.status === 401) {
      localStorage.removeItem('access_token')
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

export default request

4.6 API 接口定义

src/api/auth.js

javascript 复制代码
import request from './request'

export const register = (data) => request.post('/auth/register', data)
export const login = (data) => request.post('/auth/login', data)

src/api/posts.js

javascript 复制代码
import request from './request'

export const getPosts = () => request.get('/posts')
export const getPost = (id) => request.get(`/posts/${id}`)
export const createPost = (data) => request.post('/posts', data)
export const updatePost = (id, data) => request.put(`/posts/${id}`, data)
export const deletePost = (id) => request.delete(`/posts/${id}`)

4.7 Vuex 状态管理(示例)

src/store/index.js

javascript 复制代码
import { createStore } from 'vuex'
import { login } from '@/api/auth'

export default createStore({
  state: {
    user: JSON.parse(localStorage.getItem('user')) || null,
    token: localStorage.getItem('access_token') || null
  },
  mutations: {
    setUser(state, user) {
      state.user = user
      localStorage.setItem('user', JSON.stringify(user))
    },
    setToken(state, token) {
      state.token = token
      localStorage.setItem('access_token', token)
    },
    logout(state) {
      state.user = null
      state.token = null
      localStorage.removeItem('user')
      localStorage.removeItem('access_token')
    }
  },
  actions: {
    async login({ commit }, credentials) {
      const res = await login(credentials)
      commit('setToken', res.access_token)
      commit('setUser', res.user)
      return res
    },
    logout({ commit }) {
      commit('logout')
    }
  }
})

4.8 路由配置

src/router/index.js

javascript 复制代码
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Login from '../views/Login.vue'
import Register from '../views/Register.vue'
import Posts from '../views/Posts.vue'
import PostDetail from '../views/PostDetail.vue'
import CreatePost from '../views/CreatePost.vue'

const routes = [
  { path: '/', component: Home },
  { path: '/login', component: Login },
  { path: '/register', component: Register },
  { path: '/posts', component: Posts },
  { path: '/posts/:id', component: PostDetail },
  { path: '/create', component: CreatePost, meta: { requiresAuth: true } }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('access_token')
  if (to.meta.requiresAuth && !token) {
    next('/login')
  } else {
    next()
  }
})

export default router

4.9 组件示例

登录组件 Login.vue
vue 复制代码
<template>
  <div>
    <h2>登录</h2>
    <form @submit.prevent="handleLogin">
      <input v-model="email" type="email" placeholder="邮箱" required>
      <input v-model="password" type="password" placeholder="密码" required>
      <button type="submit">登录</button>
    </form>
  </div>
</template>

<script>
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'

export default {
  data() {
    return {
      email: '',
      password: ''
    }
  },
  setup() {
    const store = useStore()
    const router = useRouter()
    return { store, router }
  },
  methods: {
    async handleLogin() {
      try {
        await this.store.dispatch('login', { email: this.email, password: this.password })
        this.router.push('/')
      } catch (error) {
        alert('登录失败')
      }
    }
  }
}
</script>
文章列表组件 Posts.vue
vue 复制代码
<template>
  <div>
    <h2>文章列表</h2>
    <ul>
      <li v-for="post in posts" :key="post.id">
        <router-link :to="`/posts/${post.id}`">{{ post.title }}</router-link>
        <p>{{ post.content.slice(0, 100) }}...</p>
      </li>
    </ul>
  </div>
</template>

<script>
import { getPosts } from '@/api/posts'

export default {
  data() {
    return {
      posts: []
    }
  },
  async created() {
    this.posts = await getPosts()
  }
}
</script>

4.10 启动前端

bash 复制代码
npm run serve

访问 http://localhost:8080,测试登录、注册、文章操作等。


五、前后端联调与部署

5.1 开发环境联调

确保 Flask 后端运行在 localhost:5000,Vue 前端运行在 localhost:8080,通过代理解决跨域问题。前端请求 /api 会被代理到后端。

5.2 生产环境部署

有两种常见方式:

  1. 分离部署:后端部署到服务器(如 Gunicorn + Nginx),前端部署到静态服务器(如 Nginx),两者通过域名/端口交互,需要配置 CORS 允许前端域名。
  2. 统一部署:将前端构建后的静态文件交给 Nginx,Nginx 同时代理后端 API 请求,实现同域部署,无需 CORS。
5.2.1 构建前端
bash 复制代码
npm run build

生成 dist/ 目录。

5.2.2 配置 Nginx

假设后端使用 Gunicorn 运行在 127.0.0.1:5000,前端静态文件放在 /var/www/frontend

Nginx 配置示例:

nginx 复制代码
server {
    listen 80;
    server_name example.com;

    # 前端静态文件
    location / {
        root /var/www/frontend;
        try_files $uri $uri/ /index.html;
    }

    # 后端 API 代理
    location /api/ {
        proxy_pass http://127.0.0.1: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;
    }
}

这样,前端访问 /api/xxx 就会自动代理到后端,且同域无需 CORS。

5.2.3 使用 Gunicorn 运行 Flask
bash 复制代码
gunicorn -w 4 -b 127.0.0.1:5000 run:app

配置 Supervisor 或 systemd 保证服务持续运行。


六、总结

通过本教程,你已经掌握了使用 Flask 和 Vue 构建前后端分离项目的完整流程:

  • 后端:构建 RESTful API,使用 JWT 认证,处理跨域,管理数据库。
  • 前端:使用 Vue 3、Vue Router、Vuex、Axios 搭建界面,调用后端接口。
  • 部署:Nginx 作为统一入口,代理前端静态文件和后端 API。

前后端分离架构让开发更灵活,团队可以并行工作,前端可以独立部署 CDN,后端可以水平扩展。希望你能基于此教程,进一步探索更复杂的项目,如添加用户权限、实时通信、文件上传等。

相关推荐
叹一曲当时只道是寻常2 小时前
Python 飞书开放平台自动化配置工具 feishu-auto 使用教程
python·自动化·飞书
2401_827499992 小时前
python核心语法05-模块
java·前端·python
Lauren_Blueblue2 小时前
第十六届蓝桥杯省赛Python研究生组-C变换数组
python·算法·蓝桥杯·编程基础
yaoxin5211232 小时前
375. Java IO API - 列出目录内容
java·开发语言·python
小陈工2 小时前
2026年4月5日技术资讯洞察:AI商业模式变革、知识管理革命与开源生态反击
开发语言·人工智能·python·安全·oracle·开源
ZC跨境爬虫2 小时前
Playwright模拟鼠标滚轮实战:从原理到百度图片_豆瓣电影爬取
爬虫·python·计算机外设
桔筐2 小时前
Axios 从入门到实战封装全解析(附异步/拦截器/生命周期)
vue.js
甄心爱学习3 小时前
【项目实训】法律文书智能摘要系统2
前端·javascript·vue.js
华科易迅3 小时前
Vue通过Ajax获取后台路由信息
vue.js·ajax·okhttp