一、开场白:从前端框架的选择说起
最近有读者问我:"Vue.js、React、Angular到底选哪个?和Python后端集成哪个最方便?"
我的回答永远是:没有最好,只有最适合 。但今天我必须说实话:Vue 3 + Flask是我9年实战中最顺手的组合。
为什么?
2022年,我们团队同时维护过三个项目:
- React + Django:团队2个月才跑顺,学习成本高
- Angular + Spring Boot:代码臃肿,调试繁琐
- Vue 3 + Flask:新人1周上手,联调顺畅
今天,我就把这套组合的实战经验完整分享给你,包括那些官方文档不会写的踩坑实录。
二、Vue.js 2026生态全景:为什么是它?
2.1 Vue 3的核心优势
很多人以为Vue只是"简单易学",其实它真正的优势在2026年完全显现:
组合式API(Composition API)
<script setup lang="ts">
// 传统选项式API(Vue 2)
export default {
data() {
return { count: 0 }
},
methods: {
increment() { this.count++ }
}
}
// 组合式API(Vue 3)------ 逻辑聚合,代码复用性提升200%
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => {
console.log('组件已挂载,count初始值:', count.value)
})
</script>
基于Proxy的响应式系统
Vue 2的Object.defineProperty有什么问题?
- 数组索引修改无法监听
- 对象属性添加需要$set
- 性能瓶颈明显
Vue 3的Proxy解决了所有问题:
// 这都能触发响应式更新!
const user = reactive({ name: '张三' })
user.age = 25 // ✅ 直接添加属性
user.hobbies?.push('编程') // ✅ 深层嵌套
// 性能提升数据(实测)
// Vue 2: 1000个组件渲染耗时 350ms
// Vue 3: 1000个组件渲染耗时 180ms(提升48%)
TypeScript原生支持
2026年,TypeScript已是企业级前端开发的标配。Vue 3和TS的结合简直是"天作之合"。
// 接口定义
interface User {
id: number
username: string
email: string
lastLogin?: string
}
// 类型安全的API调用
const fetchUser = async (userId: number): Promise<User> => {
const response = await axios.get<User>(`/api/users/${userId}`)
return response.data
}
// 编译时错误提示(不是运行时崩溃)
const user = await fetchUser(123)
console.log(user.emial) // ❌ TS报错:属性'emial'不存在,应该是'email'
2.2 技术栈对比:Vue vs React vs Angular
| 维度 | Vue 3 (2026主流) | React 18 (生态丰富) | Angular 17 (企业级) |
|---|---|---|---|
| 学习曲线 | 平缓,2周上手 | 中等,3-4周 | 陡峭,6-8周 |
| TypeScript支持 | 原生支持,零配置 | 需要配置,类型推导一般 | 原生支持,类型安全强 |
| 构建工具 | Vite(极致热更新) | Webpack/Vite | Angular CLI |
| 与Python集成 | ⭐⭐⭐⭐⭐(最简单) | ⭐⭐⭐(需要额外配置) | ⭐⭐(复杂) |
| 生态活跃度 | 极高,中国开发者为主 | 极高,全球生态 | 稳定,企业项目多 |
| 推荐场景 | 中小型项目、快速原型、团队新人多 | 大型应用、复杂交互、多平台 | 企业级系统、大型团队、标准化要求高 |
我的建议:
- 团队规模≤10人 → Vue 3
- 需要快速上线 → Vue 3 + Vite
- 已有React经验 → 继续React
- 国企/银行项目 → Angular
2.3 2026年Vue生态必装工具
// package.json 核心依赖
{
"dependencies": {
"vue": "^3.4.0", // 核心框架
"vue-router": "^4.3.0", // 路由管理
"pinia": "^3.1.0", // 状态管理(替代Vuex)
"axios": "^1.7.0", // HTTP客户端
"element-plus": "^2.8.0", // UI组件库(中文友好)
"@vueuse/core": "^11.0.0" // Vue组合式工具集
},
"devDependencies": {
"vite": "^6.0.0", // 构建工具(速度是Webpack 10倍)
"typescript": "^5.6.0", // TypeScript
"@vitejs/plugin-vue": "^5.0.0", // Vue插件
"vue-tsc": "^2.0.0" // Vue + TypeScript检查
}
}
三、axios实战:不只是"发请求"那么简单
3.1 错误示范:新手常犯的错
// ❌ 错误1:硬编码API地址(上线必死)
const response = await axios.get('http://localhost:5000/api/users')
// ❌ 错误2:到处写重复代码
const users = await axios.get('/api/users')
const user = await axios.get('/api/users/1')
const createUser = await axios.post('/api/users', data)
// ❌ 错误3:忽略错误处理
try {
const res = await axios.get('/api/users')
// 假设res.data就是用户数据
console.log(res.data.users) // 可能res.data结构不是这样
} catch (error) {
// 只弹个错误,不处理
alert('出错了!')
}
3.2 正确封装:一次封装,终身受益
// src/utils/request.ts
import axios, {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig
} from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
// 定义统一响应结构
interface ApiResponse<T = any> {
code: number
message: string
data: T
}
class Request {
private instance: AxiosInstance
constructor(baseURL: string) {
this.instance = axios.create({
baseURL,
timeout: 15000,
headers: {
'Content-Type': 'application/json'
}
})
this.setupInterceptors()
}
private setupInterceptors(): void {
// 请求拦截器
this.instance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 从localStorage获取token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加请求ID(便于追踪)
config.headers['X-Request-ID'] = Date.now().toString()
return config
},
(error: any) => {
return Promise.reject(error)
}
)
// 响应拦截器
this.instance.interceptors.response.use(
(response: AxiosResponse) => {
const res = response.data
// 业务成功(根据你的后端约定)
if (res.code === 200 || res.code === 0) {
return res.data // 直接返回业务数据,省去一层解包
}
// 业务失败
ElMessage.error(res.message || '操作失败')
// 特殊状态码处理
if (res.code === 401) {
// 未登录,跳转到登录页
localStorage.removeItem('token')
router.push('/login')
} else if (res.code === 403) {
// 无权限
ElMessage.warning('您没有权限执行此操作')
}
return Promise.reject(new Error(res.message || 'Error'))
},
(error: any) => {
// HTTP错误(网络错误、超时等)
if (error.code === 'ECONNABORTED') {
ElMessage.error('请求超时,请稍后重试')
} else if (!error.response) {
ElMessage.error('网络错误,请检查网络连接')
} else {
const status = error.response.status
if (status === 401) {
ElMessage.error('登录已过期,请重新登录')
localStorage.removeItem('token')
router.push('/login')
} else if (status === 404) {
ElMessage.error('请求的资源不存在')
} else if (status >= 500) {
ElMessage.error('服务器内部错误,请稍后重试')
}
}
return Promise.reject(error)
}
)
}
// 对外暴露的请求方法
public get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.instance.get(url, config)
}
public post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.instance.post(url, data, config)
}
// ... 其他方法(put, delete, patch等)
}
// 创建实例(环境变量控制baseURL)
const baseURL = import.meta.env.VITE_API_BASE_URL || '/api'
export const request = new Request(baseURL)
3.3 业务层封装:让接口调用像函数调用
// src/api/user.ts
import { request } from '@/utils/request'
export interface User {
id: number
username: string
email: string
realName?: string
isActive: boolean
dateJoined: string
lastLogin?: string
}
export interface CreateUserDto {
username: string
email: string
password: string
realName?: string
}
export interface UpdateUserDto {
username?: string
email?: string
realName?: string
}
export interface PaginationParams {
page?: number
pageSize?: number
search?: string
}
export interface PaginatedResponse<T> {
items: T[]
total: number
page: number
pageSize: number
}
class UserApi {
// 获取用户列表(带分页)
async getUsers(params?: PaginationParams): Promise<PaginatedResponse<User>> {
return request.get('/users', { params })
}
// 获取单个用户
async getUser(id: number): Promise<User> {
return request.get(`/users/${id}`)
}
// 创建用户
async createUser(data: CreateUserDto): Promise<User> {
return request.post('/users', data)
}
// 更新用户
async updateUser(id: number, data: UpdateUserDto): Promise<User> {
return request.put(`/users/${id}`, data)
}
// 删除用户
async deleteUser(id: number): Promise<void> {
return request.delete(`/users/${id}`)
}
// 用户登录
async login(username: string, password: string): Promise<{ token: string }> {
return request.post('/auth/login', { username, password })
}
// 获取当前用户信息
async getCurrentUser(): Promise<User> {
return request.get('/users/me')
}
}
export const userApi = new UserApi()
3.4 在Vue组件中使用
<!-- src/views/user/UserList.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { userApi, type User } from '@/api/user'
import { ElMessage } from 'element-plus'
// 响应式数据
const users = ref<User[]>([])
const loading = ref(false)
const pagination = ref({
page: 1,
pageSize: 10,
total: 0
})
// 加载用户列表
const loadUsers = async () => {
try {
loading.value = true
const response = await userApi.getUsers({
page: pagination.value.page,
pageSize: pagination.value.pageSize
})
users.value = response.items
pagination.value.total = response.total
} catch (error: any) {
ElMessage.error(`加载失败: ${error.message}`)
} finally {
loading.value = false
}
}
// 删除用户
const handleDelete = async (id: number) => {
try {
await userApi.deleteUser(id)
ElMessage.success('删除成功')
await loadUsers() // 重新加载列表
} catch (error: any) {
ElMessage.error(`删除失败: ${error.message}`)
}
}
// 生命周期钩子
onMounted(() => {
loadUsers()
})
</script>
<template>
<div class="user-list">
<el-table :data="users" v-loading="loading">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="realName" label="真实姓名" />
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button size="small" @click="$router.push(`/users/${row.id}/edit`)">
编辑
</el-button>
<el-button
size="small"
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
@current-change="loadUsers"
layout="total, sizes, prev, pager, next, jumper"
/>
</div>
</template>
四、Flask后端:不只是返回JSON
4.1 项目结构设计
vue-flask-project/
├── backend/ # Flask后端
│ ├── app/
│ │ ├── __init__.py # 应用工厂
│ │ ├── models/ # 数据模型
│ │ │ ├── __init__.py
│ │ │ ├── user.py # 用户模型
│ │ │ └── base.py # 基础模型
│ │ ├── services/ # 业务逻辑层
│ │ │ ├── __init__.py
│ │ │ ├── user_service.py
│ │ │ └── auth_service.py
│ │ ├── api/ # API层
│ │ │ ├── __init__.py
│ │ │ ├── user.py # 用户相关接口
│ │ │ └── auth.py # 认证相关接口
│ │ ├── utils/ # 工具函数
│ │ │ ├── __init__.py
│ │ │ ├── response.py # 统一响应格式
│ │ │ └── jwt_helper.py
│ │ └── extensions.py # 扩展初始化
│ ├── config.py # 配置文件
│ ├── requirements.txt # 依赖
│ └── run.py # 启动脚本
│
└── frontend/ # Vue前端(略)
4.2 Flask应用工厂模式
# backend/app/__init__.py
from flask import Flask
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager
from flask_migrate import Migrate
from config import config
# 扩展实例化
db = SQLAlchemy()
jwt = JWTManager()
migrate = Migrate()
def create_app(config_name='default'):
"""应用工厂函数"""
app = Flask(__name__)
app.config.from_object(config[config_name])
# 初始化扩展
db.init_app(app)
jwt.init_app(app)
migrate.init_app(app, db)
# 配置CORS(分环境)
if app.config['FLASK_ENV'] == 'development':
CORS(app,
origins=["http://localhost:5173", "http://127.0.0.1:5173"],
supports_credentials=True,
allow_headers=["Content-Type", "Authorization", "X-Request-ID"])
else:
# 生产环境配置具体域名
CORS(app,
origins=app.config['ALLOWED_ORIGINS'],
supports_credentials=True)
# 注册蓝图
from .api import auth, user
app.register_blueprint(auth.bp, url_prefix='/api/auth')
app.register_blueprint(user.bp, url_prefix='/api/users')
# 创建数据库表(首次运行时)
with app.app_context():
db.create_all()
return app
4.3 统一响应格式
# backend/app/utils/response.py
from flask import jsonify
from typing import Any, Dict, Optional
def success_response(data: Any = None, message: str = "操作成功") -> Dict:
"""成功响应"""
response = {
"code": 200,
"message": message,
"data": data
}
return response
def error_response(message: str = "操作失败", code: int = 400, data: Any = None) -> Dict:
"""错误响应"""
response = {
"code": code,
"message": message,
"data": data
}
return response
def paginated_response(items: list, total: int, page: int, page_size: int) -> Dict:
"""分页响应"""
return {
"items": items,
"total": total,
"page": page,
"pageSize": page_size,
"totalPages": (total + page_size - 1) // page_size
}
4.4 用户认证API实现
# backend/app/api/auth.py
from flask import Blueprint, request
from flask_jwt_extended import (
create_access_token,
create_refresh_token,
jwt_required,
get_jwt_identity,
current_user
)
from app.models.user import User
from app.services.auth_service import AuthService
from app.utils.response import success_response, error_response
from app.extensions import db
bp = Blueprint('auth', __name__)
auth_service = AuthService()
@bp.route('/login', methods=['POST'])
def login():
"""用户登录"""
try:
data = request.get_json()
# 基础验证
if not data or 'username' not in data or 'password' not in data:
return jsonify(error_response("用户名和密码不能为空")), 400
username = data['username']
password = data['password']
# 验证用户
user = auth_service.authenticate(username, password)
if not user:
return jsonify(error_response("用户名或密码错误")), 401
# 生成JWT令牌
access_token = create_access_token(identity=str(user.id))
refresh_token = create_refresh_token(identity=str(user.id))
response_data = {
"user": {
"id": user.id,
"username": user.username,
"email": user.email,
"realName": user.real_name
},
"accessToken": access_token,
"refreshToken": refresh_token
}
return jsonify(success_response(response_data, "登录成功")), 200
except Exception as e:
return jsonify(error_response(f"登录失败: {str(e)}")), 500
@bp.route('/refresh', methods=['POST'])
@jwt_required(refresh=True)
def refresh():
"""刷新访问令牌"""
try:
current_user_id = get_jwt_identity()
access_token = create_access_token(identity=current_user_id)
return jsonify(success_response({
"accessToken": access_token
}, "令牌刷新成功")), 200
except Exception as e:
return jsonify(error_response(f"令牌刷新失败: {str(e)}")), 500
@bp.route('/logout', methods=['POST'])
@jwt_required()
def logout():
"""用户登出"""
return jsonify(success_response(None, "登出成功")), 200
@bp.route('/me', methods=['GET'])
@jwt_required()
def get_current_user():
"""获取当前用户信息"""
try:
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
if not user:
return jsonify(error_response("用户不存在")), 404
user_data = {
"id": user.id,
"username": user.username,
"email": user.email,
"realName": user.real_name,
"isActive": user.is_active,
"dateJoined": user.date_joined.isoformat() if user.date_joined else None,
"lastLogin": user.last_login.isoformat() if user.last_login else None
}
return jsonify(success_response(user_data)), 200
except Exception as e:
return jsonify(error_response(f"获取用户信息失败: {str(e)}")), 500
五、跨域处理:从开发到生产的完整方案
5.1 开发环境:Vite代理配置
// frontend/vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
'/static': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
})
5.2 生产环境:Nginx配置
# /etc/nginx/sites-available/vue-flask-app
server {
listen 80;
server_name app.example.com;
# 重定向到HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name app.example.com;
# SSL证书配置
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
# 前端静态文件
location / {
root /var/www/frontend/dist;
try_files $uri $uri/ /index.html;
# 缓存优化
expires 1y;
add_header Cache-Control "public, immutable";
# 安全头
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "strict-origin-when-cross-origin";
}
# 后端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;
proxy_set_header X-Forwarded-Proto $scheme;
# 超时设置
proxy_connect_timeout 15s;
proxy_send_timeout 15s;
proxy_read_timeout 15s;
# CORS头(生产环境精确配置)
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Request-ID';
# 预检请求处理
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Request-ID';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}
# 静态资源(可选,也可以由Flask处理)
location /static/ {
alias /var/www/backend/static/;
expires 1y;
access_log off;
}
}
5.3 Flask CORS配置补充
# backend/config.py
import os
from datetime import timedelta
class Config:
"""基础配置"""
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
# 数据库
SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'postgresql://postgres:password@localhost:5432/vue_flask_app')
SQLALCHEMY_TRACK_MODIFICATIONS = False
# JWT配置
JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', 'jwt-secret-key-change-in-production')
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30)
# 其他配置
DEBUG = False
TESTING = False
class DevelopmentConfig(Config):
"""开发环境配置"""
DEBUG = True
FLASK_ENV = 'development'
# CORS配置(开发环境宽松)
ALLOWED_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"]
class ProductionConfig(Config):
"""生产环境配置"""
FLASK_ENV = 'production'
# 生产环境严格要求
DEBUG = False
# CORS配置(精确域名)
ALLOWED_ORIGINS = ["https://app.example.com", "https://admin.example.com"]
# 其他生产配置
SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL') # 必须从环境变量获取
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
六、联调实战:那些官方文档不会告诉你的坑
6.1 Cookie跨域问题
问题现象:Vue登录成功后,后续请求总是401(未授权)
根本原因:前后端域名不同,Cookie默认不跨域发送
解决方案:
// 前端axios配置
const instance = axios.create({
baseURL: '/api',
withCredentials: true, // ✅ 关键配置:允许跨域携带Cookie
timeout: 15000
})
// 后端Flask配置
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
# 生产环境(必须精确配置)
CORS(app,
origins=['https://app.example.com'],
supports_credentials=True, # ✅ 允许凭证
allow_headers=['Content-Type', 'Authorization'])
6.2 预检请求(OPTIONS)处理
问题现象:POST/PUT/DELETE请求失败,GET请求正常
根本原因:浏览器先发OPTIONS预检请求,后端未正确处理
解决方案:
# Flask-CORS自动处理OPTIONS请求
CORS(app,
origins=['http://localhost:5173'],
supports_credentials=True,
methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']) # ✅ 包含OPTIONS
6.3 生产环境部署路径问题
问题现象:本地正常,上线后API请求404
根本原因:前端打包时publicPath配置错误
解决方案:
// vite.config.js 生产环境配置
export default defineConfig(({ mode }) => {
const isProduction = mode === 'production'
return {
base: isProduction ? '/' : '/', // 根据实际部署路径调整
server: {
port: 5173,
proxy: isProduction ? undefined : {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
},
build: {
outDir: 'dist',
assetsDir: 'assets',
rollupOptions: {
output: {
// 文件名哈希,避免缓存
chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]'
}
}
}
}
})
七、性能优化:让你的应用飞起来
7.1 请求优化
// 请求去重(避免重复请求)
import axios, { AxiosRequestConfig } from 'axios'
const pendingRequests = new Map()
const generateRequestKey = (config: AxiosRequestConfig): string => {
return `${config.method}-${config.url}-${JSON.stringify(config.params)}-${JSON.stringify(config.data)}`
}
// 请求拦截器
axios.interceptors.request.use(config => {
const requestKey = generateRequestKey(config)
if (pendingRequests.has(requestKey)) {
// 取消重复请求
return Promise.reject(new axios.Cancel('重复请求已取消'))
}
// 存储请求标识
const controller = new AbortController()
config.signal = controller.signal
pendingRequests.set(requestKey, controller)
return config
})
// 响应拦截器
axios.interceptors.response.use(
response => {
const requestKey = generateRequestKey(response.config)
pendingRequests.delete(requestKey)
return response
},
error => {
if (axios.isCancel(error)) {
console.log('请求已取消:', error.message)
}
return Promise.reject(error)
}
)
7.2 缓存策略
// 封装带缓存的请求
class CachedRequest {
private cache = new Map<string, { data: any; timestamp: number }>()
private cacheTTL = 5 * 60 * 1000 // 5分钟
async get<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const cached = this.cache.get(key)
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
console.log('缓存命中:', key)
return cached.data
}
console.log('缓存未命中,重新请求:', key)
const data = await fetcher()
this.cache.set(key, { data, timestamp: Date.now() })
return data
}
clearCache(key?: string): void {
if (key) {
this.cache.delete(key)
} else {
this.cache.clear()
}
}
}
// 使用示例
const cachedRequest = new CachedRequest()
// 获取用户列表(带缓存)
const loadUsers = async () => {
return cachedRequest.get('user-list', async () => {
const response = await userApi.getUsers()
return response.items
})
}
八、完整项目启动脚本
8.1 一键启动脚本
#!/bin/bash
# start-project.sh
echo "🚀 启动 Vue + Flask 前后端分离项目..."
# 检查环境
echo "🔍 检查环境..."
command -v node >/dev/null 2>&1 || { echo "❌ Node.js 未安装"; exit 1; }
command -v python3 >/dev/null 2>&1 || { echo "❌ Python3 未安装"; exit 1; }
# 启动后端
echo "🔧 启动 Flask 后端..."
cd backend
# 检查虚拟环境
if [ ! -d "venv" ]; then
echo "📦 创建 Python 虚拟环境..."
python3 -m venv venv
fi
# 激活虚拟环境并安装依赖
source venv/bin/activate
pip install -r requirements.txt
# 启动 Flask(后台运行)
nohup python run.py > flask.log 2>&1 &
FLASK_PID=$!
echo "✅ Flask 后端已启动 (PID: $FLASK_PID)"
# 启动前端
echo "🎨 启动 Vue 前端..."
cd ../frontend
# 检查依赖
if [ ! -d "node_modules" ]; then
echo "📦 安装前端依赖..."
npm ci
fi
# 启动 Vue 开发服务器(后台运行)
nohup npm run dev > vue.log 2>&1 &
VUE_PID=$!
echo "✅ Vue 前端已启动 (PID: $VUE_PID)"
# 等待服务就绪
echo "⏳ 等待服务启动..."
sleep 5
# 显示启动状态
echo ""
echo "========================================"
echo "🎉 项目启动完成!"
echo ""
echo "🌐 访问地址:"
echo " 前端:http://localhost:5173"
echo " 后端 API:http://localhost:5000/api"
echo ""
echo "📊 日志文件:"
echo " 后端日志:backend/flask.log"
echo " 前端日志:frontend/vue.log"
echo ""
echo "🛑 停止服务:"
echo " ./stop-project.sh"
echo "========================================"
# 保存 PID 文件(用于停止服务)
echo $FLASK_PID > .flask.pid
echo $VUE_PID > .vue.pid
8.2 Docker Compose 部署
# docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: vue_flask_app
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
backend:
build:
context: ./backend
dockerfile: Dockerfile
environment:
- FLASK_ENV=production
- DATABASE_URL=postgresql://postgres:password@postgres:5432/vue_flask_app
- REDIS_URL=redis://redis:6379/0
- SECRET_KEY=${SECRET_KEY}
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "5000:5000"
volumes:
- ./backend:/app
command: gunicorn -w 4 -b 0.0.0.0:5000 "app:create_app('production')"
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "5173:5173"
volumes:
- ./frontend:/app
command: npm run preview
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/ssl:/etc/nginx/ssl
- frontend-dist:/usr/share/nginx/html
depends_on:
- backend
- frontend
volumes:
postgres_data:
frontend-dist:
九、总结与思考
9.1 核心要点回顾
- Vue 3优势:组合式API、Proxy响应式、TypeScript原生支持
- axios封装:统一拦截器、错误处理、请求缓存
- Flask架构:工厂模式、统一响应、JWT认证
- 跨域方案:Vite代理(开发)、Nginx配置(生产)
- 性能优化:请求去重、缓存策略、代码分割
9.2 来自9年实战的建议
技术选型:
- 团队≤10人 → Vue 3 + Flask(开发效率最高)
- 企业级系统 → React + Django(生态完善)
- 传统行业 → Angular + Spring Boot(规范严格)
团队协作:
- 接口先行(API First):前后端先定义接口文档
- 统一规范:代码风格、命名约定、目录结构
- 文档沉淀:接口文档、部署文档、故障处理手册
个人成长:
- 不要只做后端:至少了解前端基础,理解完整链路
- 拥抱变化:技术更新快,保持学习心态
- 深度思考:不仅要实现功能,更要思考"为什么这样设计"
9.3 最后的思考题
- 你的项目中,前后端联调最大的痛点是什么?如何解决?
- 如何设计一个可扩展的API架构,支持未来功能扩展?
- 当团队成员技术栈差异大时,如何统一开发规范?
欢迎在评论区分享你的经验和见解!