FastAPI + Vue3 实战:从零搭建一个带用户认证的待办事项应用(附完整代码)
你是否曾想过,一个看似简单的待办事项列表背后,其实隐藏着现代Web开发的完整技术栈?从用户登录、数据存储到界面交互,每一个环节都考验着开发者对前后端分离架构的深刻理解。今天,我将带你从零开始,用FastAPI和Vue3构建一个功能完整的待办事项应用,它不仅支持基础的增删改查,还集成了基于JWT的用户认证系统。这个项目将是你理解现代Web开发流程的绝佳实践,无论你是想巩固全栈技能,还是为下一个项目寻找技术方案,都能从中获得实实在在的收获。
我们将采用完全分离的开发模式:后端用FastAPI提供高性能的RESTful API,前端用Vue3构建动态交互界面,两者通过HTTP请求和WebSocket(可选)进行通信。整个过程我会提供可直接运行的代码片段,并解释每个关键决策背后的考量。准备好了吗?让我们开始这场从零到一的构建之旅。
1. 项目架构设计与环境搭建
在动手写代码之前,清晰的架构设计能让你少走很多弯路。我们的应用将采用经典的三层架构:数据层、业务逻辑层和表现层。后端使用FastAPI框架,它基于Python 3.6+的类型提示,提供了自动化的API文档生成和极高的性能。前端选择Vue3的组合式API,它比Vue2的选项式API更加灵活,更适合构建复杂的交互界面。
1.1 技术栈选择与考量
为什么选择FastAPI和Vue3这个组合?让我分享一些实际项目中的体会。FastAPI的异步支持在处理高并发请求时表现优异,特别是当你的待办事项应用需要实时同步多个客户端时,这一点至关重要。而Vue3的响应式系统经过重构,性能提升了近一倍,对于频繁更新的UI界面来说,这是不容忽视的优势。
核心依赖清单:
-
后端:
- FastAPI 0.104+:Web框架核心
- SQLAlchemy 2.0+:ORM工具
- Pydantic 2.0+:数据验证与序列化
- Passlib:密码哈希
- python-jose[cryptography]:JWT令牌生成与验证
- Uvicorn:ASGI服务器
-
前端:
- Vue 3.3+:前端框架
- Pinia 2.1+:状态管理
- Vue Router 4.2+:路由管理
- Axios 1.6+:HTTP客户端
- Element Plus 2.3+:UI组件库(可选,但推荐)
1.2 开发环境配置
首先确保你的系统已经安装了Python 3.8+和Node.js 16+。我建议使用虚拟环境来管理Python依赖,这能避免不同项目间的包冲突。
后端环境配置:
# 创建项目目录
mkdir todo-app && cd todo-app
mkdir backend && cd backend
# 创建Python虚拟环境
python -m venv venv
# 激活虚拟环境(Windows)
venv\Scripts\activate
# 激活虚拟环境(macOS/Linux)
source venv/bin/activate
# 安装核心依赖
pip install fastapi[all] sqlalchemy pydantic[email] passlib[bcrypt] python-jose[cryptography] uvicorn
前端环境配置:
# 返回项目根目录,创建前端项目
cd ..
npm create vue@latest frontend
# 按照提示选择配置,我推荐:
# ✔ Add TypeScript? Yes
# ✔ Add JSX Support? No
# ✔ Add Vue Router for Single Page Application development? Yes
# ✔ Add Pinia for state management? Yes
# ✔ Add Vitest for Unit Testing? No
# ✔ Add an End-to-End Testing Solution? No
# ✔ Add ESLint for code quality? Yes
cd frontend
npm install
npm install axios element-plus
提示:如果你在安装过程中遇到网络问题,可以考虑配置npm镜像源。执行
npm config set registry https://registry.npmmirror.com可以切换到国内镜像,加速依赖下载。
环境配置完成后,你的项目结构应该大致如下:
todo-app/
├── backend/
│ ├── venv/
│ ├── app/
│ │ ├── __init__.py
│ │ ├── main.py
│ │ ├── models.py
│ │ ├── schemas.py
│ │ ├── crud.py
│ │ ├── database.py
│ │ └── auth.py
│ └── requirements.txt
└── frontend/
├── src/
│ ├── components/
│ ├── views/
│ ├── stores/
│ ├── router/
│ └── App.vue
├── package.json
└── vite.config.js
这个结构清晰地分离了前后端代码,便于团队协作和后期维护。在实际开发中,我习惯为每个功能模块创建独立的文件,而不是把所有代码都堆在main.py里。这样做虽然初期看起来有些繁琐,但当项目规模扩大时,你会发现模块化的价值。
2. 后端核心:FastAPI与数据库设计
后端是整个应用的大脑,负责数据处理、业务逻辑和API提供。我们将从数据库模型设计开始,逐步构建完整的RESTful API。
2.1 数据库模型与关系设计
我选择SQLite作为开发数据库,因为它无需额外安装服务,文件存储的方式也便于项目迁移。对于生产环境,你可以轻松切换到PostgreSQL或MySQL。
数据库连接配置:
# backend/app/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./todo.db"
# 生产环境建议使用PostgreSQL
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@localhost/todo_db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False} # SQLite专用参数
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
接下来设计用户和待办事项两个核心模型。这里有个细节需要注意:密码字段不应该存储明文,我们使用Passlib的bcrypt算法进行哈希处理。
# backend/app/models.py
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DateTime, Text
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from .database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
username = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# 建立一对多关系
todos = relationship("Todo", back_populates="owner", cascade="all, delete-orphan")
class Todo(Base):
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True, nullable=False)
description = Column(Text, nullable=True)
completed = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
due_date = Column(DateTime(timezone=True), nullable=True)
# 外键关联
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# 关系定义
owner = relationship("User", back_populates="todos")
注意:在定义模型关系时,我特意使用了
cascade="all, delete-orphan"参数。这意味着当用户被删除时,其所有的待办事项也会自动删除,避免了数据库中的孤儿记录。这在生产环境中很重要,能保持数据的一致性。
2.2 Pydantic模式与数据验证
Pydantic是FastAPI的黄金搭档,它利用Python的类型提示来进行数据验证和序列化。我习惯为每个模型创建多个模式类,分别用于创建、更新和响应。
# backend/app/schemas.py
from pydantic import BaseModel, EmailStr, Field
from datetime import datetime
from typing import Optional, List
# 用户相关模式
class UserBase(BaseModel):
email: EmailStr
username: str = Field(..., min_length=3, max_length=50)
class UserCreate(UserBase):
password: str = Field(..., min_length=8)
class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
username: Optional[str] = Field(None, min_length=3, max_length=50)
class UserInDB(UserBase):
id: int
is_active: bool
created_at: datetime
class Config:
from_attributes = True
# 待办事项相关模式
class TodoBase(BaseModel):
title: str = Field(..., min_length=1, max_length=200)
description: Optional[str] = None
due_date: Optional[datetime] = None
class TodoCreate(TodoBase):
pass
class TodoUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=200)
description: Optional[str] = None
completed: Optional[bool] = None
due_date: Optional[datetime] = None
class Todo(TodoBase):
id: int
completed: bool
created_at: datetime
updated_at: Optional[datetime] = None
owner_id: int
class Config:
from_attributes = True
# 响应模式
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
这里有几个设计要点值得注意:
- 使用
EmailStr类型自动验证邮箱格式 - 通过
Field添加额外的验证规则,如长度限制 from_attributes = True允许Pydantic从ORM对象创建实例- 为更新操作创建独立的模式类,所有字段都是可选的
2.3 JWT认证系统实现
用户认证是Web应用的安全基石。我们采用JWT(JSON Web Token)方案,它无状态、易于扩展,适合前后端分离架构。
密码哈希与验证:
# backend/app/auth.py
from passlib.context import CryptContext
from datetime import datetime, timedelta
from jose import JWTError, jwt
from typing import Optional
# 密码哈希上下文
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# JWT配置
SECRET_KEY = "your-secret-key-change-in-production" # 生产环境务必修改
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""验证密码"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""生成密码哈希"""
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
"""创建JWT访问令牌"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_token(token: str) -> Optional[dict]:
"""验证JWT令牌"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
return None
return payload
except JWTError:
return None
依赖注入与用户获取:
FastAPI的依赖注入系统让认证逻辑变得优雅。我们可以创建可重用的依赖项来获取当前用户。
# backend/app/dependencies.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from . import models, schemas, auth
from .database import SessionLocal
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def get_db():
"""数据库会话依赖"""
db = SessionLocal()
try:
yield db
finally:
db.close()
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db)
) -> models.User:
"""获取当前认证用户"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的认证凭证",
headers={"WWW-Authenticate": "Bearer"},
)
payload = auth.verify_token(token)
if payload is None:
raise credentials_exception
username: str = payload.get("sub")
if username is None:
raise credentials_exception
user = db.query(models.User).filter(models.User.username == username).first()
if user is None:
raise credentials_exception
return user
async def get_current_active_user(
current_user: models.User = Depends(get_current_user)
) -> models.User:
"""获取当前活跃用户"""
if not current_user.is_active:
raise HTTPException(status_code=400, detail="用户已被禁用")
return current_user
这种依赖注入的设计模式有几个好处:首先,它让认证逻辑与业务逻辑分离;其次,可以在路由中轻松复用;最后,便于单元测试的模拟。
2.4 CRUD操作与业务逻辑
CRUD操作是待办事项应用的核心。我习惯将数据库操作封装在独立的CRUD模块中,这样既保持了代码的整洁,也便于测试。
# backend/app/crud.py
from sqlalchemy.orm import Session
from typing import List, Optional
from . import models, schemas
from .auth import get_password_hash
# 用户CRUD操作
def get_user_by_email(db: Session, email: str) -> Optional[models.User]:
return db.query(models.User).filter(models.User.email == email).first()
def get_user_by_username(db: Session, username: str) -> Optional[models.User]:
return db.query(models.User).filter(models.User.username == username).first()
def create_user(db: Session, user: schemas.UserCreate) -> models.User:
hashed_password = get_password_hash(user.password)
db_user = models.User(
email=user.email,
username=user.username,
hashed_password=hashed_password
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
# 待办事项CRUD操作
def get_todos(
db: Session,
user_id: int,
skip: int = 0,
limit: int = 100,

&spm=1001.2101.3001.5002&articleId=153244242&d=1&t=3&u=fb67662302e34b00a5496246cf1d19e7)
328

被折叠的 条评论
为什么被折叠?



