需求:一个博客平台——用户注册/登录、发表/编辑/删除文章、文章列表与详情、JSON API、管理后台。

cms/ ├── app/ │ ├── __init__.py # create_app │ ├── config.py # 环境配置 │ ├── extensions.py # db/login/migrate/csrf │ ├── models.py # User、Post │ ├── forms.py # 注册/登录/文章表单 │ ├── blueprints/ │ │ ├── auth.py # 认证 │ │ ├── main.py # 页面 │ │ └── api.py # JSON API │ ├── templates/ │ └── static/ ├── migrations/ ├── tests/ ├── wsgi.py └── requirements.txt
pip install flask flask-sqlalchemy flask-login \ flask-migrate flask-wtf flask-restful flask-caching
项目从"需求"到"上线"的完整流程,可以用一张状态图串起来:
# app/extensions.py from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager from flask_migrate import Migrate from flask_wtf import CSRFProtect db = SQLAlchemy() login_manager = LoginManager() login_manager.login_view = 'auth.login' migrate = Migrate() csrf = CSRFProtect() # app/__init__.py from flask import Flask from .config import config_map from .extensions import db, login_manager, migrate, csrf def create_app(config_name='default'): app = Flask(__name__) app.config.from_object(config_map[config_name]) db.init_app(app) login_manager.init_app(app) migrate.init_app(app, db) csrf.init_app(app) from .blueprints.auth import auth_bp from .blueprints.main import main_bp from .blueprints.api import api_bp app.register_blueprint(auth_bp, url_prefix='/auth') app.register_blueprint(main_bp) app.register_blueprint(api_bp, url_prefix='/api') return app
# app/models.py from datetime import datetime from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from .extensions import db, login_manager class User(UserMixin, db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) is_admin = db.Column(db.Boolean, default=False) posts = db.relationship('Post', backref='author', lazy=True) def set_password(self, pwd): self.password_hash = generate_password_hash(pwd) def check_password(self, pwd): return check_password_hash(self.password_hash, pwd) class Post(db.Model): __tablename__ = 'posts' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(200), nullable=False) body = db.Column(db.Text, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True) @login_manager.user_loader def load_user(user_id): return db.session.get(User, int(user_id))
# app/blueprints/auth.py from flask import Blueprint, render_template, request, redirect, url_for, flash from flask_login import login_user, logout_user, login_required, current_user from ..models import User from ..forms import LoginForm, RegisterForm from ..extensions import db auth_bp = Blueprint('auth', __name__) @auth_bp.route('/register', methods=['GET', 'POST']) def register(): form = RegisterForm() if form.validate_on_submit(): user = User(username=form.username.data) user.set_password(form.password.data) db.session.add(user) db.session.commit() flash('注册成功,请登录') return redirect(url_for('auth.login')) return render_template('register.html', form=form) @auth_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) return redirect(url_for('main.index')) flash('用户名或密码错误') return render_template('login.html', form=form) @auth_bp.route('/logout') @login_required def logout(): logout_user() return redirect(url_for('main.index'))
# app/blueprints/main.py from flask import Blueprint, render_template, request, abort from flask_login import login_required, current_user from ..models import Post from ..forms import PostForm from ..extensions import db main_bp = Blueprint('main', __name__) @main_bp.route('/') def index(): page = request.args.get('page', 1, type=int) pagination = Post.query.order_by(Post.id.desc()).paginate( page=page, per_page=10) return render_template('index.html', pagination=pagination) @main_bp.route('/post/<int:pid>') def post_detail(pid): post = db.session.get(Post, pid) if post is None: abort(404) return render_template('post.html', post=post) @main_bp.route('/post/new', methods=['GET', 'POST']) @login_required def post_new(): form = PostForm() if form.validate_on_submit(): post = Post(title=form.title.data, body=form.body.data, author=current_user) db.session.add(post) db.session.commit() return redirect(url_for('main.post_detail', pid=post.id)) return render_template('post_edit.html', form=form)
# app/blueprints/api.py from flask import Blueprint, request from flask_restful import Api, Resource, fields, marshal_with, abort from ..models import Post, User from ..extensions import db api_bp = Blueprint('api', __name__) api = Api(api_bp) post_fields = { 'id': fields.Integer, 'title': fields.String, 'body': fields.String, 'created_at': fields.DateTime(dt_format='iso8601'), 'author': fields.Nested({'id': fields.Integer, 'username': fields.String}), } class PostList(Resource): @marshal_with(post_fields) def get(self): return Post.query.order_by(Post.id.desc()).limit(50).all() class PostItem(Resource): @marshal_with(post_fields) def get(self, pid): post = db.session.get(Post, pid) if post is None: abort(404, message='文章不存在') return post api.add_resource(PostList, '/posts') api.add_resource(PostItem, '/posts/<int:pid>')
# tests/test_auth.py import pytest from app import create_app, db @pytest.fixture def app(): app = create_app('testing') with app.app_context(): db.create_all() yield app db.session.remove() db.drop_all() @pytest.fixture def client(app): return app.test_client() def test_register_and_login(client): resp = client.post('/auth/register', data={ 'username': 'alice', 'password': 'secret123', 'confirm': 'secret123', }, follow_redirects=True) assert '注册成功' in resp.get_data(as_text=True) resp = client.post('/auth/login', data={ 'username': 'alice', 'password': 'secret123', }, follow_redirects=True) assert 'alice' in resp.get_data(as_text=True) def test_create_post_requires_login(client): resp = client.get('/post/new', follow_redirects=True) assert '请先登录' in resp.get_data(as_text=True)
# 部署 gunicorn -w 4 -b 127.0.0.1:8000 wsgi:app # + Nginx 反代 + systemd 守护(见 2.7)
| 环节 | 用到的知识 |
|---|---|
| 结构 | 4.1 工厂 + 蓝图 + 配置分离 |
| 认证 | 3.4 Flask-Login + 密码哈希 |
| 表单 | 3.3 Flask-WTF + CSRF |
| 数据 | 3.2 SQLAlchemy + 3.7 迁移 |
| API | 2.8 / 3.5 RESTful 设计 |
| 安全 | 4.2 CSRF/XSS/会话配置 |
| 测试 | 2.6 pytest + test_client |
| 部署 | 2.7 Gunicorn + Nginx |
| 规范 | 4.4 black/flake8/mypy |
| 误区 | 现象 | 正解 |
|---|---|---|
| 忘了 flask db upgrade | 表不存在 | 部署链路执行迁移 |
| 表单没有 hidden_tag | CSRF 400 | 模板渲染 token |
| API 不鉴权 | 数据裸奔 | API 加登录/Token 校验 |
| 测试连开发库 | 数据被清 | TestingConfig 独立库 |
| 忘记登录装饰器 | 未登录也能发文章 | login_required 全覆盖 |
| 生产 debug 开 | 安全隐患 | 环境变量控制 |
这个项目是起点,可按需演进:
每一步都基于你已经掌握的扩展模式——这就是完整学完一本 Flask 教程后真正"能干活"的状态。
完整项目做完,复盘比"做完了"更有价值。
第一,回看每一步的"为什么"。 为什么用工厂模式(多环境测试需要)?为什么蓝图按 auth/main/api 拆(业务域划分)?为什么 CSRF 全局开(所有改数据请求都要防护)?为什么测试用独立库(隔离与安全)?把"做了什么"升级成"为什么这么做",你的知识就从"会做"变成"懂做"——这是独立开发与照葫芦画瓢的区别。
第二,给项目"挑毛病"。 对照全书找不足:这个项目有没有限流?(没有——真实项目要加)日志有没有结构化?(没有——生产要加)API 有没有文档?(没有——联调要加)有没有性能压测?(没有——上线前要做)"挑毛病"的过程就是把本书所有知识串起来的过程——你开始用"生产视角"审视自己的代码了。
第三,数据模型的演进预演。 现在项目里 User/Post 两表很简单。如果加评论(Post 一对多 Comment)、加标签(Post 多对多 Tag)、加收藏(User 多对多 Post)——用 3.2 的关系语法和 3.7 的迁移流程走一遍,你会发现"加表"已经变成一件熟练的事。用迁移演进数据模型,是项目从"demo"走向"产品"的必经之路。
第四,从单体到服务的边界感。 这个项目是单体(一个应用搞定一切)。当团队变大、模块边界清晰后,可以把"用户系统"拆成独立服务(SSO)、把"通知"拆成独立 worker。拆分不是目的——什么时候拆?当"独立部署"能带来明确收益(独立扩容、独立发布、故障隔离)时。先单体后拆分是绝大多数项目的正确路线,不要过早分布式。
第五,把这个项目变成"你的作品"。 给它起个名字、写个 README(项目简介、功能列表、快速开始)、部署到线上、配好 HTTPS——然后把它放进简历/作品集。"能访问的线上项目"比"本地跑通的代码"有说服力十倍。第四章学完,你不只是"学过 Flask",而是"能交付 Web 应用"。
第六,下一本书/下一阶段学什么。 推荐顺序:Docker 与 CI/CD(部署自动化)→ PostgreSQL(生产数据库,索引与事务)→ Redis(缓存与队列)→ 前端框架基础(Vue/React,配合 API 做完整应用)→ 系统设计基础(高并发、分布式)。Flask 是你 Web 世界的起点,不是终点——框架会过时,但这些工程能力会跟着你走很远。
最后的最后:把这本书从头翻一遍目录,合上书,画出 Flask 应用从请求到响应的完整链路图。能画出来,说明你真正毕业了。祝你用 Flask 做出自己喜欢的东西。
问:项目文件太多,记不住结构怎么办?
不用记——按"功能域"找文件:改登录找 blueprints/auth.py,改数据模型找 models.py,改页面找 templates/。结构设计的目的是"按直觉能找到",多写几次就熟悉了。
问:这个项目可以直接当模板用吗?
可以。把它作为起点,替换业务模型(User/Post → 你的业务实体)、扩展蓝图、加你需要的扩展即可。模板的价值是"已验证的结构"——不用从零设计,专注业务。
问:实战中遇到报错怎么办?
按错误类型定位:路由 404 查蓝图注册;表单 400 查 CSRF token;数据库报错查模型定义与迁移;部署问题查 Gunicorn 日志。每个错误都是一次学习机会,排查过程就是知识加深的过程。