前后端分离已成为现代 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 代理前端静态文件
二、项目初始化与目录结构
建议分别创建两个独立目录:backend 和 frontend。
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 生产环境部署
有两种常见方式:
- 分离部署:后端部署到服务器(如 Gunicorn + Nginx),前端部署到静态服务器(如 Nginx),两者通过域名/端口交互,需要配置 CORS 允许前端域名。
- 统一部署:将前端构建后的静态文件交给 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,后端可以水平扩展。希望你能基于此教程,进一步探索更复杂的项目,如添加用户权限、实时通信、文件上传等。