Flask Web 框架详细指南

1. Flask 简介

Flask 是一个轻量级的 Python Web 应用框架,基于 Werkzeug WSGI 工具库和 Jinja2 模板引擎。它被称为"微框架",因为它不需要特定的工具或库,但可以通过扩展来添加功能。

1.1 Flask 的特点

  • 轻量级:核心功能简单,易于学习
  • 灵活性:可以根据需求选择组件
  • 可扩展:丰富的扩展生态系统
  • RESTful:内置对 RESTful 请求分发的支持
  • 调试友好:内置开发服务器和调试器

2. Flask 安装和环境配置

2.1 基础安装

bash 复制代码
# 创建虚拟环境
python -m venv flask_env
source flask_env/bin/activate  # Linux/Mac
# flask_env\Scripts\activate  # Windows

# 安装 Flask
pip install Flask

# 安装常用扩展
pip install Flask-SQLAlchemy Flask-Migrate Flask-Login Flask-WTF Flask-Mail

2.2 项目结构

arduino 复制代码
my_flask_app/
├── app/
│   ├── __init__.py
│   ├── models.py
│   ├── routes.py
│   ├── forms.py
│   ├── templates/
│   │   ├── base.html
│   │   └── index.html
│   └── static/
│       ├── css/
│       ├── js/
│       └── images/
├── migrations/
├── config.py
├── requirements.txt
└── run.py

3. Flask 基础概念

3.1 最简单的 Flask 应用

python 复制代码
# app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

@app.route('/user/<name>')
def user_profile(name):
    return f'Hello, {name}!'

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

3.2 路由和视图函数

python 复制代码
from flask import Flask, request, jsonify, render_template

app = Flask(__name__)

# 基本路由
@app.route('/')
def index():
    return render_template('index.html')

# 带参数的路由
@app.route('/user/<int:user_id>')
def user_detail(user_id):
    return f'User ID: {user_id}'

# 多种 HTTP 方法
@app.route('/api/users', methods=['GET', 'POST'])
def users_api():
    if request.method == 'GET':
        return jsonify({'users': []})
    elif request.method == 'POST':
        data = request.get_json()
        return jsonify({'message': 'User created', 'data': data}), 201

# URL 参数类型
@app.route('/post/<string:title>')
def post_detail(title):
    return f'Post: {title}'

@app.route('/category/<path:category_path>')
def category_detail(category_path):
    return f'Category: {category_path}'

3.3 请求处理

python 复制代码
from flask import request, session, redirect, url_for

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        
        # 验证用户凭据
        if validate_user(username, password):
            session['user_id'] = get_user_id(username)
            return redirect(url_for('dashboard'))
        else:
            return render_template('login.html', error='Invalid credentials')
    
    return render_template('login.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return 'No file uploaded', 400
    
    file = request.files['file']
    if file.filename == '':
        return 'No file selected', 400
    
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        return 'File uploaded successfully'

4. 模板系统 (Jinja2)

4.1 基础模板语法

html 复制代码
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}Default Title{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
    <nav>
        <ul>
            <li><a href="{{ url_for('index') }}">Home</a></li>
            <li><a href="{{ url_for('about') }}">About</a></li>
            {% if current_user.is_authenticated %}
                <li><a href="{{ url_for('profile') }}">Profile</a></li>
                <li><a href="{{ url_for('logout') }}">Logout</a></li>
            {% else %}
                <li><a href="{{ url_for('login') }}">Login</a></li>
            {% endif %}
        </ul>
    </nav>
    
    <main>
        {% with messages = get_flashed_messages() %}
            {% if messages %}
                <div class="flash-messages">
                    {% for message in messages %}
                        <div class="alert">{{ message }}</div>
                    {% endfor %}
                </div>
            {% endif %}
        {% endwith %}
        
        {% block content %}{% endblock %}
    </main>
</body>
</html>
html 复制代码
<!-- templates/index.html -->
{% extends "base.html" %}

{% block title %}Home Page{% endblock %}

{% block content %}
    <h1>Welcome to My App</h1>
    
    {% if users %}
        <h2>Users List</h2>
        <ul>
            {% for user in users %}
                <li>
                    <a href="{{ url_for('user_detail', user_id=user.id) }}">
                        {{ user.username }}
                    </a>
                    - {{ user.email }}
                </li>
            {% endfor %}
        </ul>
    {% else %}
        <p>No users found.</p>
    {% endif %}
    
    <!-- 条件判断 -->
    {% if current_user.is_admin %}
        <a href="{{ url_for('admin_panel') }}" class="btn btn-admin">Admin Panel</a>
    {% endif %}
    
    <!-- 循环和过滤器 -->
    {% for post in posts %}
        <article>
            <h3>{{ post.title|title }}</h3>
            <p>{{ post.content|truncate(100) }}</p>
            <small>Published on {{ post.created_at|strftime('%Y-%m-%d') }}</small>
        </article>
    {% endfor %}
{% endblock %}

4.2 自定义过滤器和函数

python 复制代码
from datetime import datetime
import markdown

@app.template_filter('strftime')
def strftime_filter(date, format='%Y-%m-%d'):
    """自定义日期格式化过滤器"""
    return date.strftime(format)

@app.template_filter('markdown')
def markdown_filter(text):
    """Markdown 转 HTML 过滤器"""
    return markdown.markdown(text)

@app.context_processor
def utility_processor():
    """全局模板函数"""
    def format_price(amount):
        return f"${amount:.2f}"
    
    def is_weekend():
        return datetime.now().weekday() >= 5
    
    return dict(format_price=format_price, is_weekend=is_weekend)

5. 数据库集成 (SQLAlchemy)

5.1 配置数据库

python 复制代码
# config.py
import os

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///app.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///dev.db'

class ProductionConfig(Config):
    DEBUG = False
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')

config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
}

5.2 定义模型

python 复制代码
# app/models.py
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime

db = SQLAlchemy()

# 多对多关系表
user_roles = db.Table('user_roles',
    db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
    db.Column('role_id', db.Integer, db.ForeignKey('role.id'), primary_key=True)
)

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(255), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    is_active = db.Column(db.Boolean, default=True)
    
    # 关系
    posts = db.relationship('Post', backref='author', lazy='dynamic')
    roles = db.relationship('Role', secondary=user_roles, backref='users')
    profile = db.relationship('UserProfile', uselist=False, backref='user')
    
    def set_password(self, password):
        self.password_hash = generate_password_hash(password)
    
    def check_password(self, password):
        return check_password_hash(self.password_hash, password)
    
    def has_role(self, role_name):
        return any(role.name == role_name for role in self.roles)
    
    def __repr__(self):
        return f'<User {self.username}>'

class Role(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)
    description = db.Column(db.String(200))

class UserProfile(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    first_name = db.Column(db.String(50))
    last_name = db.Column(db.String(50))
    bio = db.Column(db.Text)
    avatar_url = db.Column(db.String(200))
    birth_date = db.Column(db.Date)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    published = db.Column(db.Boolean, default=False)
    
    # 外键
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
    
    # 关系
    comments = db.relationship('Comment', backref='post', lazy='dynamic', cascade='all, delete-orphan')
    tags = db.relationship('Tag', secondary='post_tags', backref='posts')
    
    def __repr__(self):
        return f'<Post {self.title}>'

class Category(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), unique=True, nullable=False)
    description = db.Column(db.Text)
    posts = db.relationship('Post', backref='category', lazy='dynamic')

class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)

# 多对多关系表
post_tags = db.Table('post_tags',
    db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True),
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True)
)

class Comment(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # 外键
    post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    
    # 关系
    user = db.relationship('User', backref='comments')

5.3 数据库操作

python 复制代码
# app/routes.py
from flask import render_template, request, redirect, url_for, flash, jsonify
from app.models import db, User, Post, Category

@app.route('/users')
def users_list():
    page = request.args.get('page', 1, type=int)
    users = User.query.paginate(
        page=page, per_page=10, error_out=False
    )
    return render_template('users.html', users=users)

@app.route('/posts')
def posts_list():
    # 查询所有已发布的文章,按创建时间降序排列
    posts = Post.query.filter_by(published=True)\
                     .order_by(Post.created_at.desc())\
                     .all()
    return render_template('posts.html', posts=posts)

@app.route('/create_post', methods=['GET', 'POST'])
def create_post():
    if request.method == 'POST':
        title = request.form['title']
        content = request.form['content']
        category_id = request.form.get('category_id')
        
        post = Post(
            title=title,
            content=content,
            user_id=current_user.id,
            category_id=category_id if category_id else None
        )
        
        # 处理标签
        tag_names = request.form.get('tags', '').split(',')
        for tag_name in tag_names:
            tag_name = tag_name.strip()
            if tag_name:
                tag = Tag.query.filter_by(name=tag_name).first()
                if not tag:
                    tag = Tag(name=tag_name)
                    db.session.add(tag)
                post.tags.append(tag)
        
        db.session.add(post)
        db.session.commit()
        
        flash('Post created successfully!', 'success')
        return redirect(url_for('posts_list'))
    
    categories = Category.query.all()
    return render_template('create_post.html', categories=categories)

@app.route('/api/users/<int:user_id>', methods=['GET', 'PUT', 'DELETE'])
def user_api(user_id):
    user = User.query.get_or_404(user_id)
    
    if request.method == 'GET':
        return jsonify({
            'id': user.id,
            'username': user.username,
            'email': user.email,
            'created_at': user.created_at.isoformat(),
            'posts_count': user.posts.count()
        })
    
    elif request.method == 'PUT':
        data = request.get_json()
        user.username = data.get('username', user.username)
        user.email = data.get('email', user.email)
        
        db.session.commit()
        return jsonify({'message': 'User updated successfully'})
    
    elif request.method == 'DELETE':
        db.session.delete(user)
        db.session.commit()
        return jsonify({'message': 'User deleted successfully'})

# 复杂查询示例
@app.route('/search')
def search():
    query = request.args.get('q', '')
    category_id = request.args.get('category')
    
    posts_query = Post.query.filter_by(published=True)
    
    if query:
        posts_query = posts_query.filter(
            Post.title.contains(query) | Post.content.contains(query)
        )
    
    if category_id:
        posts_query = posts_query.filter_by(category_id=category_id)
    
    posts = posts_query.order_by(Post.created_at.desc()).all()
    
    return render_template('search_results.html', posts=posts, query=query)

6. 表单处理 (Flask-WTF)

6.1 定义表单

python 复制代码
# app/forms.py
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import StringField, TextAreaField, PasswordField, SelectField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
from app.models import User

class LoginForm(FlaskForm):
    username = StringField('用户名', validators=[DataRequired(), Length(min=4, max=20)])
    password = PasswordField('密码', validators=[DataRequired()])
    remember_me = BooleanField('记住我')
    submit = SubmitField('登录')

class RegistrationForm(FlaskForm):
    username = StringField('用户名', validators=[
        DataRequired(), 
        Length(min=4, max=20)
    ])
    email = StringField('邮箱', validators=[DataRequired(), Email()])
    password = PasswordField('密码', validators=[
        DataRequired(), 
        Length(min=6)
    ])
    password2 = PasswordField('确认密码', validators=[
        DataRequired(), 
        EqualTo('password', message='密码不匹配')
    ])
    submit = SubmitField('注册')
    
    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user:
            raise ValidationError('用户名已存在,请选择其他用户名')
    
    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user:
            raise ValidationError('邮箱已被注册,请使用其他邮箱')

class PostForm(FlaskForm):
    title = StringField('标题', validators=[DataRequired(), Length(max=200)])
    content = TextAreaField('内容', validators=[DataRequired()])
    category = SelectField('分类', coerce=int)
    tags = StringField('标签 (用逗号分隔)')
    published = BooleanField('立即发布')
    featured_image = FileField('特色图片', validators=[
        FileAllowed(['jpg', 'jpeg', 'png', 'gif'], '只允许图片文件!')
    ])
    submit = SubmitField('保存')
    
    def __init__(self, *args, **kwargs):
        super(PostForm, self).__init__(*args, **kwargs)
        from app.models import Category
        self.category.choices = [(0, '选择分类')] + [
            (c.id, c.name) for c in Category.query.all()
        ]

class ProfileForm(FlaskForm):
    first_name = StringField('名字', validators=[Length(max=50)])
    last_name = StringField('姓氏', validators=[Length(max=50)])
    bio = TextAreaField('个人简介', validators=[Length(max=500)])
    avatar = FileField('头像', validators=[
        FileAllowed(['jpg', 'jpeg', 'png'], '只允许图片文件!')
    ])
    submit = SubmitField('更新资料')

6.2 在视图中使用表单

python 复制代码
from app.forms import LoginForm, RegistrationForm, PostForm
from flask_login import login_user, logout_user, login_required, current_user

@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember_me.data)
            
            # 重定向到用户原本想访问的页面
            next_page = request.args.get('next')
            if not next_page or not next_page.startswith('/'):
                next_page = url_for('index')
            
            flash(f'欢迎回来,{user.username}!', 'success')
            return redirect(next_page)
        else:
            flash('用户名或密码错误', 'error')
    
    return render_template('login.html', form=form)

@app.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(
            username=form.username.data,
            email=form.email.data
        )
        user.set_password(form.password.data)
        
        db.session.add(user)
        db.session.commit()
        
        flash('注册成功!请登录', 'success')
        return redirect(url_for('login'))
    
    return render_template('register.html', form=form)

@app.route('/create_post', methods=['GET', 'POST'])
@login_required
def create_post():
    form = PostForm()
    
    if form.validate_on_submit():
        post = Post(
            title=form.title.data,
            content=form.content.data,
            published=form.published.data,
            user_id=current_user.id
        )
        
        if form.category.data:
            post.category_id = form.category.data
        
        # 处理文件上传
        if form.featured_image.data:
            filename = secure_filename(form.featured_image.data.filename)
            form.featured_image.data.save(
                os.path.join(app.config['UPLOAD_FOLDER'], filename)
            )
            post.featured_image = filename
        
        # 处理标签
        if form.tags.data:
            tag_names = [tag.strip() for tag in form.tags.data.split(',')]
            for tag_name in tag_names:
                if tag_name:
                    tag = Tag.query.filter_by(name=tag_name).first()
                    if not tag:
                        tag = Tag(name=tag_name)
                        db.session.add(tag)
                    post.tags.append(tag)
        
        db.session.add(post)
        db.session.commit()
        
        flash('文章创建成功!', 'success')
        return redirect(url_for('post_detail', id=post.id))
    
    return render_template('create_post.html', form=form)

7. 用户认证 (Flask-Login)

7.1 配置用户认证

python 复制代码
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate

db = SQLAlchemy()
login_manager = LoginManager()
migrate = Migrate()

def create_app(config_name='default'):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    
    # 初始化扩展
    db.init_app(app)
    login_manager.init_app(app)
    migrate.init_app(app, db)
    
    # 配置登录管理器
    login_manager.login_view = 'login'
    login_manager.login_message = '请先登录以访问此页面'
    login_manager.login_message_category = 'info'
    
    @login_manager.user_loader
    def load_user(user_id):
        return User.query.get(int(user_id))
    
    # 注册蓝图
    from app.main import bp as main_bp
    app.register_blueprint(main_bp)
    
    from app.auth import bp as auth_bp
    app.register_blueprint(auth_bp, url_prefix='/auth')
    
    return app

7.2 权限装饰器

python 复制代码
# app/decorators.py
from functools import wraps
from flask import abort
from flask_login import current_user

def admin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not current_user.is_authenticated or not current_user.has_role('admin'):
            abort(403)
        return f(*args, **kwargs)
    return decorated_function

def permission_required(permission):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not current_user.can(permission):
                abort(403)
            return f(*args, **kwargs)
        return decorated_function
    return decorator

def same_user_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        user_id = kwargs.get('user_id') or kwargs.get('id')
        if not current_user.is_authenticated or \
           (current_user.id != user_id and not current_user.has_role('admin')):
            abort(403)
        return f(*args, **kwargs)
    return decorated_function

# 使用示例
@app.route('/admin')
@login_required
@admin_required
def admin_panel():
    return render_template('admin/dashboard.html')

@app.route('/user/<int:user_id>/edit')
@login_required
@same_user_required
def edit_profile(user_id):
    user = User.query.get_or_404(user_id)
    return render_template('edit_profile.html', user=user)

8. API 开发

8.1 RESTful API 设计

python 复制代码
# app/api.py
from flask import Blueprint, request, jsonify
from flask_login import login_required, current_user
from app.models import db, User, Post

api = Blueprint('api', __name__, url_prefix='/api/v1')

# 错误处理
@api.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Resource not found'}), 404

@api.errorhandler(400)
def bad_request(error):
    return jsonify({'error': 'Bad request'}), 400

@api.errorhandler(403)
def forbidden(error):
    return jsonify({'error': 'Forbidden'}), 403

# 用户 API
@api.route('/users', methods=['GET'])
def get_users():
    page = request.args.get('page', 1, type=int)
    per_page = min(request.args.get('per_page', 10, type=int), 100)
    
    users = User.query.paginate(
        page=page, per_page=per_page, error_out=False
    )
    
    return jsonify({
        'users': [user.to_dict() for user in users.items],
        'pagination': {
            'page': page,
            'pages': users.pages,
            'per_page': per_page,
            'total': users.total,
            'has_next': users.has_next,
            'has_prev': users.has_prev
        }
    })

@api.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    user = User.query.get_or_404(user_id)
    return jsonify(user.to_dict())

@api.route('/users', methods=['POST'])
def create_user():
    data = request.get_json()
    
    # 验证必需字段
    if not data or not data.get('username') or not data.get('email'):
        return jsonify({'error': 'Missing required fields'}), 400
    
    # 检查用户名和邮箱唯一性
    if User.query.filter_by(username=data['username']).first():
        return jsonify({'error': 'Username already exists'}), 400
    
    if User.query.filter_by(email=data['email']).first():
        return jsonify({'error': 'Email already exists'}), 400
    
    user = User(
        username=data['username'],
        email=data['email']
    )
    
    if data.get('password'):
        user.set_password(data['password'])
    
    db.session.add(user)
    db.session.commit()
    
    return jsonify(user.to_dict()), 201

@api.route('/users/<int:user_id>', methods=['PUT'])
@login_required
def update_user(user_id):
    if current_user.id != user_id and not current_user.has_role('admin'):
        return jsonify({'error': 'Forbidden'}), 403
    
    user = User.query.get_or_404(user_id)
    data = request.get_json()
    
    if 'username' in data:
        if User.query.filter_by(username=data['username']).first() and \
           data['username'] != user.username:
            return jsonify({'error': 'Username already exists'}), 400
        user.username = data['username']
    
    if 'email' in data:
        if User.query.filter_by(email=data['email']).first() and \
           data['email'] != user.email:
            return jsonify({'error': 'Email already exists'}), 400
        user.email = data['email']
    
    db.session.commit()
    return jsonify(user.to_dict())

@api.route('/users/<int:user_id>', methods=['DELETE'])
@login_required
def delete_user(user_id):
    if current_user.id != user_id and not current_user.has_role('admin'):
        return jsonify({'error': 'Forbidden'}), 403
    
    user = User.query.get_or_404(user_id)
    db.session.delete(user)
    db.session.commit()
    
    return jsonify({'message': 'User deleted successfully'})

# 文章 API
@api.route('/posts', methods=['GET'])
def get_posts():
    page = request.args.get('page', 1, type=int)
    per_page = min(request.args.get('per_page', 10, type=int), 100)
    
    query = Post.query.filter_by(published=True)
    
    # 筛选参数
    author_id = request.args.get('author_id', type=int)
    if author_id:
        query = query.filter_by(user_id=author_id)
    
    category_id = request.args.get('category_id', type=int)
    if category_id:
        query = query.filter_by(category_id=category_id)
    
    search = request.args.get('search')
    if search:
        query = query.filter(
            Post.title.contains(search) | Post.content.contains(search)
        )
    
    posts = query.order_by(Post.created_at.desc()).paginate(
        page=page, per_page=per_page, error_out=False
    )
    
    return jsonify({
        'posts': [post.to_dict() for post in posts.items],
        'pagination': {
            'page': page,
            'pages': posts.pages,
            'per_page': per_page,
            'total': posts.total
        }
    })

@api.route('/posts', methods=['POST'])
@login_required
def create_post():
    data = request.get_json()
    
    if not data or not data.get('title') or not data.get('content'):
        return jsonify({'error': 'Missing required fields'}), 400
    
    post = Post(
        title=data['title'],
        content=data['content'],
        published=data.get('published', False),
        user_id=current_user.id
    )
    
    if data.get('category_id'):
        post.category_id = data['category_id']
    
    db.session.add(post)
    db.session.commit()
    
    return jsonify(post.to_dict()), 201

# 添加序列化方法到模型
# 在 models.py 中添加
class User(UserMixin, db.Model):
    # ... 现有代码 ...
    
    def to_dict(self, include_email=False):
        data = {
            'id': self.id,
            'username': self.username,
            'created_at': self.created_at.isoformat(),
            'is_active': self.is_active,
            'posts_count': self.posts.count()
        }
        if include_email:
            data['email'] = self.email
        return data

class Post(db.Model):
    # ... 现有代码 ...
    
    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'content': self.content,
            'created_at': self.created_at.isoformat(),
            'updated_at': self.updated_at.isoformat(),
            'published': self.published,
            'author': {
                'id': self.author.id,
                'username': self.author.username
            },
            'category': {
                'id': self.category.id,
                'name': self.category.name
            } if self.category else None,
            'tags': [tag.name for tag in self.tags],
            'comments_count': self.comments.count()
        }

8.2 API 认证和限流

python 复制代码
# app/auth.py
from flask import Blueprint, request, jsonify
from flask_login import login_user, logout_user, current_user
import jwt
from datetime import datetime, timedelta
from functools import wraps

auth_bp = Blueprint('auth', __name__)

def token_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        token = request.headers.get('Authorization')
        
        if not token:
            return jsonify({'error': 'Token is missing'}), 401
        
        try:
            if token.startswith('Bearer '):
                token = token[7:]
            
            data = jwt.decode(
                token, 
                current_app.config['SECRET_KEY'], 
                algorithms=['HS256']
            )
            current_user_id = data['user_id']
            current_user = User.query.get(current_user_id)
            
            if not current_user:
                return jsonify({'error': 'Token is invalid'}), 401
                
        except jwt.ExpiredSignatureError:
            return jsonify({'error': 'Token has expired'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'error': 'Token is invalid'}), 401
        
        return f(current_user, *args, **kwargs)
    return decorated_function

@auth_bp.route('/api/login', methods=['POST'])
def api_login():
    data = request.get_json()
    
    if not data or not data.get('username') or not data.get('password'):
        return jsonify({'error': 'Missing credentials'}), 400
    
    user = User.query.filter_by(username=data['username']).first()
    
    if user and user.check_password(data['password']):
        # 生成 JWT token
        token = jwt.encode({
            'user_id': user.id,
            'exp': datetime.utcnow() + timedelta(hours=24)
        }, current_app.config['SECRET_KEY'], algorithm='HS256')
        
        return jsonify({
            'token': token,
            'user': user.to_dict(include_email=True),
            'expires_in': 24 * 3600
        })
    
    return jsonify({'error': 'Invalid credentials'}), 401

# 使用 token 认证的 API
@api.route('/profile')
@token_required
def get_profile(current_user):
    return jsonify(current_user.to_dict(include_email=True))

9. 蓝图 (Blueprints) 组织代码

9.1 创建蓝图

python 复制代码
# app/main/__init__.py
from flask import Blueprint

bp = Blueprint('main', __name__)

from app.main import routes

# app/main/routes.py
from flask import render_template, request
from app.main import bp
from app.models import Post, User

@bp.route('/')
def index():
    posts = Post.query.filter_by(published=True)\
                     .order_by(Post.created_at.desc())\
                     .limit(10).all()
    return render_template('index.html', posts=posts)

@bp.route('/about')
def about():
    return render_template('about.html')

# app/auth/__init__.py
from flask import Blueprint

bp = Blueprint('auth', __name__)

from app.auth import routes

# app/auth/routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, current_user
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm
from app.models import User, db

@bp.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember_me.data)
            next_page = request.args.get('next')
            if not next_page or not next_page.startswith('/'):
                next_page = url_for('main.index')
            return redirect(next_page)
        flash('Invalid username or password')
    return render_template('auth/login.html', title='Sign In', form=form)

@bp.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('main.index'))

9.2 注册蓝图

python 复制代码
# app/__init__.py
def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)
    
    db.init_app(app)
    migrate.init_app(app, db)
    login.init_app(app)
    
    # 注册蓝图
    from app.main import bp as main_bp
    app.register_blueprint(main_bp)
    
    from app.auth import bp as auth_bp
    app.register_blueprint(auth_bp, url_prefix='/auth')
    
    from app.api import bp as api_bp
    app.register_blueprint(api_bp, url_prefix='/api')
    
    return app

10. 配置管理

10.1 环境配置

python 复制代码
# config.py
import os
from dotenv import load_dotenv

basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'app.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
    # 邮件配置
    MAIL_SERVER = os.environ.get('MAIL_SERVER')
    MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
    MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in \
        ['true', 'on', '1']
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    
    # 上传文件配置
    UPLOAD_FOLDER = os.path.join(basedir, 'uploads')
    MAX_CONTENT_LENGTH = 16 * 1024 * 1024  # 16MB
    
    # 分页配置
    POSTS_PER_PAGE = 10
    USERS_PER_PAGE = 20
    
    # Redis 配置
    REDIS_URL = os.environ.get('REDIS_URL') or 'redis://localhost:6379/0'
    
    # 日志配置
    LOG_TO_STDOUT = os.environ.get('LOG_TO_STDOUT')
    
    @staticmethod
    def init_app(app):
        pass

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
        'sqlite://'
    WTF_CSRF_ENABLED = False

class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data.sqlite')
    
    @classmethod
    def init_app(cls, app):
        Config.init_app(app)
        
        # 错误邮件通知
        import logging
        from logging.handlers import SMTPHandler
        if app.config['MAIL_SERVER']:
            auth = None
            if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
                auth = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
            secure = None
            if app.config['MAIL_USE_TLS']:
                secure = ()
            mail_handler = SMTPHandler(
                mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
                fromaddr='no-reply@' + app.config['MAIL_SERVER'],
                toaddrs=app.config['ADMINS'], 
                subject='Application Failure',
                credentials=auth, 
                secure=secure
            )
            mail_handler.setLevel(logging.ERROR)
            app.logger.addHandler(mail_handler)

config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
}

10.2 环境变量文件

bash 复制代码
# .env
SECRET_KEY=your-secret-key-here
DATABASE_URL=postgresql://username:password@localhost/mydatabase
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USE_TLS=1
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-app-password
REDIS_URL=redis://localhost:6379/0

11. 部署和生产环境

11.1 WSGI 配置

python 复制代码
# wsgi.py
import os
from app import create_app

app = create_app(os.getenv('FLASK_CONFIG') or 'default')

if __name__ == "__main__":
    app.run()

11.2 Gunicorn 配置

python 复制代码
# gunicorn.conf.py
bind = "0.0.0.0:8000"
workers = 4
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
max_requests = 1000
max_requests_jitter = 100
preload_app = True

11.3 Nginx 配置

nginx 复制代码
# /etc/nginx/sites-available/myapp
server {
    listen 80;
    server_name yourdomain.com;
    
    location / {
        proxy_pass http://127.0.0.1:8000;
        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;
    }
    
    location /static {
        alias /path/to/your/app/app/static;
        expires 30d;
    }
    
    location /uploads {
        alias /path/to/your/app/uploads;
        expires 30d;
    }
}

11.4 Docker 部署

dockerfile 复制代码
# Dockerfile
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

ENV FLASK_APP=wsgi.py
ENV FLASK_ENV=production

EXPOSE 8000

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "wsgi:app"]
yaml 复制代码
# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/myapp
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - db
      - redis
    volumes:
      - ./uploads:/app/uploads

  db:
    image: postgres:13
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:6-alpine

volumes:
  postgres_data:

12. 测试

12.1 单元测试

python 复制代码
# tests/test_models.py
import unittest
from app import create_app, db
from app.models import User, Post

class UserModelCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app('testing')
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()

    def test_password_hashing(self):
        u = User(username='susan')
        u.set_password('cat')
        self.assertFalse(u.check_password('dog'))
        self.assertTrue(u.check_password('cat'))

    def test_user_creation(self):
        u = User(username='john', email='john@example.com')
        u.set_password('test')
        db.session.add(u)
        db.session.commit()
        
        self.assertEqual(User.query.count(), 1)
        self.assertEqual(u.username, 'john')
        self.assertTrue(u.check_password('test'))

if __name__ == '__main__':
    unittest.main(verbosity=2)

12.2 集成测试

python 复制代码
# tests/test_routes.py
import unittest
from app import create_app, db
from app.models import User

class RoutesTestCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app('testing')
        self.client = self.app.test_client()
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()

    def test_home_page(self):
        response = self.client.get('/')
        self.assertEqual(response.status_code, 200)

    def test_login_logout(self):
        # 创建测试用户
        user = User(username='test', email='test@example.com')
        user.set_password('test')
        db.session.add(user)
        db.session.commit()

        # 测试登录
        response = self.client.post('/auth/login', data={
            'username': 'test',
            'password': 'test'
        }, follow_redirects=True)
        self.assertEqual(response.status_code, 200)

        # 测试登出
        response = self.client.get('/auth/logout', follow_redirects=True)
        self.assertEqual(response.status_code, 200)

    def test_api_users(self):
        response = self.client.get('/api/users')
        self.assertEqual(response.status_code, 200)
        data = response.get_json()
        self.assertIn('users', data)

13. 最佳实践

13.1 项目结构最佳实践

bash 复制代码
my_flask_app/
├── app/
│   ├── __init__.py          # 应用工厂
│   ├── models.py            # 数据模型
│   ├── main/                # 主要功能蓝图
│   ├── auth/                # 认证蓝图
│   ├── api/                 # API 蓝图
│   ├── templates/           # 模板文件
│   ├── static/              # 静态文件
│   └── utils/               # 工具函数
├── migrations/              # 数据库迁移
├── tests/                   # 测试文件
├── config.py               # 配置文件
├── requirements.txt        # 依赖列表
├── .env                    # 环境变量
├── .gitignore             # Git 忽略文件
└── run.py                 # 应用入口

13.2 安全最佳实践

python 复制代码
# 1. 使用强密钥
app.config['SECRET_KEY'] = os.urandom(24)

# 2. 防止 SQL 注入 - 使用 ORM
# Bad
query = f"SELECT * FROM users WHERE id = {user_id}"
# Good
user = User.query.get(user_id)

# 3. 防止 XSS 攻击 - 自动转义
# Jinja2 默认启用自动转义

# 4. 防止 CSRF 攻击
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)

# 5. 使用 HTTPS
@app.before_request
def force_https():
    if not request.is_secure and app.env != 'development':
        return redirect(request.url.replace('http://', 'https://'))

# 6. 设置安全头
@app.after_request
def set_security_headers(response):
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'DENY'
    response.headers['X-XSS-Protection'] = '1; mode=block'
    response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
    return response

13.3 性能优化

python 复制代码
# 1. 数据库查询优化
# Bad
posts = Post.query.all()
for post in posts:
    print(post.author.username)  # N+1 查询问题

# Good
posts = Post.query.options(db.joinedload(Post.author)).all()

# 2. 分页
posts = Post.query.paginate(
    page=page, per_page=20, error_out=False
)

# 3. 缓存
from flask_caching import Cache
cache = Cache(app)

@cache.cached(timeout=300)
def expensive_function():
    return calculate_something()

# 4. 数据库连接池
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
    'pool_pre_ping': True,
    'pool_recycle': 300,
}

Flask 是一个功能强大且灵活的 Web 框架,适合从简单的原型到复杂的企业级应用。通过合理的项目结构、安全实践和性能优化,可以构建出高质量的 Web 应用程序。

Similar code found with 6 license types

相关推荐
java1234_小锋1 天前
[免费]基于Python的气象天气预报数据可视化分析系统(Flask+echarts+爬虫) 【论文+源码+SQL脚本】
python·flask·python数据分析·python天气预报·flask天气预报·python气象·python可视化
程序员的世界你不懂2 天前
【Flask】测试平台开发,产品管理功能UI重构-第九篇
ui·重构·flask
hui函数3 天前
订单后台管理系统-day07菜品模块
数据库·后端·python·flask
大叔_爱编程3 天前
p049基于Flask的医疗预约与诊断系统
python·flask·毕业设计·源码·课程设计·医疗预约与诊断系统
XiaoMu_0013 天前
【Flask + Vue3 前后端分离管理系统】
python·flask
@TsUnAmI~3 天前
基于Flask的企业级产品信息管理系统技术实现笔记
笔记·python·flask
程序员的世界你不懂3 天前
【Flask】测试平台开发,开发实现应用搜索和分页-第十篇
后端·python·flask
程序员的世界你不懂3 天前
【Flask】测试平台开发,实现全局邮件发送工具 第十二篇
网络·python·flask