项目架构重构(Blueprints):把 5000 行面条代码捋成乐高积木
📂 所属阶段:第四阶段 — 实战演练(道满博客)
🔗 相关章节:路由(Routing)艺术 · Flask-Login 实战
1. 从“单文件爽点”到“重构噩梦”
很多人初次接触 Flask 时,都会被一个场景吸引:新建一个 app.py,敲十几行代码,刷新浏览器就看到 Hello World —— 这种零配置、即时反馈的体验,正是 Flask “微框架”定位的迷人之处。
但这份快感往往持续不了多久。随着页面越加越多——首页、登录注册、文章发布、后台管理、API 接口、错误处理……app.py 会不可逆转地膨胀成 5000+ 行的面条代码锅:
😱 三个月后再打开 app.py 的你:
─────────────────────────────────────
• 想找登录逻辑?翻 200 行才看到。
• 改一下首页渲染,却不小心动到了后台配置。
• 加一个新接口,仿佛是牵一发动全身的噩梦。
• 新人一看到这个文件,直接劝退。
这时,你需要的正是 Flask 官方提供的模块化架构工具——Blueprints(蓝图)。
2. 蓝图是什么?不是画图软件!
简单来说,蓝图是 Flask 中可复用的“功能模块容器”。
它能把原本混在 app.py 里的路由、模板、静态文件、错误处理甚至自定义过滤器,全部封装到独立的文件夹中。最后再通过一个“应用工厂”把这些模块拼接到核心 Flask 应用上,就像拼乐高积木一样。
用蓝图重构后的道满博客目录,会从“扁平混乱”变成“分层清晰”的乐高式结构:
dao-man-blog/
├── app/ # 应用核心代码
│ ├── __init__.py # ✨ 应用工厂函数(关键!)
│ ├── extensions.py # 全局扩展(DB、LoginManager、Cache 等)
│ ├── models.py # 数据库模型
│ ├── forms.py # WTForms 表单
│ │
│ ├── main/ # 🎯 主功能蓝图(首页、关于页、搜索页)
│ │ ├── __init__.py # 定义蓝图实例
│ │ ├── routes.py # 主路由逻辑
│ │ └── templates/ # 模板文件夹(须加 main/ 子目录,避免冲突)
│ │ └── main/
│ │ ├── index.html
│ │ └── about.html
│ │
│ ├── auth/ # 🔐 认证蓝图(登录、注册、退出、重置密码)
│ │ ├── __init__.py
│ │ ├── routes.py
│ │ └── templates/auth/
│ │
│ └── articles/ # 📝 文章蓝图(列表、详情、发布、编辑、删除)
│ ├── __init__.py
│ ├── routes.py
│ └── templates/articles/
│
└── run.py # 启动文件(简单到只有两行!)
💡 注意:每个蓝图下的 templates/ 文件夹里,必须再多嵌套一层与蓝图同名的子目录(如 main/、auth/),否则多个蓝图下有同名模板时会发生冲突。
3. 手把手:创建并拼接第一个蓝图
我们先从最简单的主功能蓝图(main_bp)入手,快速跑通整个流程。
3.1 定义蓝图实例
每个蓝图模块的 __init__.py,就是它的“身份证”。我们在这里设置蓝图名称、文件路径以及可选的 URL 前缀。
# app/main/__init__.py
from flask import Blueprint
# 创建蓝图实例,三个核心参数:
# ① "main":蓝图的唯一标识,后续 url_for 需要使用它
# ② __name__:蓝图的根目录,用来定位模板/静态文件
# ③ url_prefix(可选):统一为所有路由添加前缀
main_bp = Blueprint("main", __name__)
# 最后导入 routes,将路由绑定到蓝图
# 注意:必须放在定义蓝图之后,避免循环导入!
from . import routes
3.2 写主功能路由
现在路由不再直接装饰 app.route,而是改用 蓝图实例的 route:
# app/main/routes.py
from flask import render_template
from app.main import main_bp
@main_bp.route("/")
def index():
return render_template("main/index.html")
@main_bp.route("/about")
def about():
return render_template("main/about.html")
3.3 用应用工厂注册蓝图
接下来是重构的核心步骤:把原来直接创建的 app = Flask(__name__) 改成一个函数——应用工厂(create_app)。这样做的好处是可以在不同环境(开发/测试/生产)下创建不同配置的应用,也能灵活注册蓝图。
# app/__init__.py
from flask import Flask
from app.extensions import db, login_manager # 假定已写好 extensions.py
def create_app():
# 创建核心 Flask 应用
app = Flask(__name__)
# 1. 加载配置(后面单独介绍,这里先留空)
app.config.from_object("app.config.DevConfig")
# 2. 初始化全局扩展(DB、LoginManager 等)
db.init_app(app)
login_manager.init_app(app)
# 3. ✨ 关键:注册蓝图
# 注意:导入蓝图必须放在函数内部!!!
# 否则会出现“循环导入”的死循环
# (例如 routes.py 导入 db,extensions.py 又导入 create_app)
from app.main import main_bp
from app.auth import auth_bp
from app.articles import articles_bp
app.register_blueprint(main_bp) # 主蓝图没有前缀,直接挂载到根路径
app.register_blueprint(auth_bp) # 认证蓝图,在内部已设置 /auth 前缀
app.register_blueprint(articles_bp) # 文章蓝图,在内部已设置 /articles 前缀
# 4. 注册 Flask-Login 的用户加载器(必须和 login_manager 在同一个 create_app 内)
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id)) # User 可从 models.py 导入
return app
3.4 写最简启动文件
此时,启动文件 run.py 会变得异常简洁:
# run.py
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run(debug=True)
4. 进阶:带前缀的认证与文章蓝图
4.1 认证蓝图(统一加 /auth 前缀)
为认证相关的路由统一加上 /auth 前缀,不仅让 URL 看起来更规范,也方便以后统一调整。
# app/auth/__init__.py
from flask import Blueprint
# url_prefix="/auth":所有该蓝图下的路由都会自动加上 /auth
# 比如 @auth_bp.route("/login") 实际访问路径就是 /auth/login
auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
from . import routes
# app/auth/routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.security import generate_password_hash, check_password_hash
from app.auth import auth_bp
from app.extensions import db
from app.models import User
from app.forms import LoginForm, RegisterForm
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
# ⚠️ 注意:url_for 现在必须使用「蓝图标识.视图函数名」
# 跳转主首页应写成 url_for("main.index"),而不是 url_for("index")
return redirect(url_for("main.index"))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.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")
return redirect(next_page or url_for("main.index"))
flash("邮箱或密码错误,请重试", "danger")
return render_template("auth/login.html", form=form)
@auth_bp.route("/logout")
@login_required
def logout():
logout_user()
flash("已成功退出登录,欢迎下次再来~", "info")
return redirect(url_for("auth.login"))
💡 重要提醒:使用蓝图后,所有 url_for 都必须加上蓝图标识作为前缀,即便是同一个蓝图内部跳转也建议写全,以保持一致性。
4.2 文章蓝图(带分页与权限的业务逻辑)
文章蓝图的业务逻辑虽然复杂,但拆分后依然清晰易读:
# app/articles/__init__.py
from flask import Blueprint
articles_bp = Blueprint("articles", __name__, url_prefix="/articles")
from . import routes
# app/articles/routes.py
from flask import render_template, redirect, url_for, flash, abort, request
from flask_login import login_required, current_user
from app.articles import articles_bp
from app.extensions import db
from app.models import Post, Category
from app.forms import ArticleForm
@articles_bp.route("/")
def index():
"""文章列表页(带分页)"""
page = request.args.get("page", 1, type=int)
# 只显示已发布的文章,按创建时间倒序
pagination = Post.query.filter_by(is_published=True)\
.order_by(Post.created_at.desc())\
.paginate(page=page, per_page=10, error_out=False)
return render_template("articles/index.html", pagination=pagination)
@articles_bp.route("/<int:post_id>")
def detail(post_id):
"""文章详情页(带浏览量统计)"""
post = Post.query.get_or_404(post_id)
# 只有已发布的文章普通用户才能看
if not post.is_published and (not current_user.is_authenticated or post.author != current_user):
abort(403)
post.views += 1
db.session.commit()
return render_template("articles/detail.html", post=post)
@articles_bp.route("/create", methods=["GET", "POST"])
@login_required
def create():
"""创建文章"""
form = ArticleForm()
if form.validate_on_submit():
post = Post(
title=form.title.data,
content=form.content.data,
summary=form.summary.data,
author=current_user,
)
if form.is_published.data:
post.is_published = True
db.session.add(post)
db.session.commit()
flash("文章创建成功!", "success")
return redirect(url_for("articles.detail", post_id=post.id))
return render_template("articles/create.html", form=form)
5. 蓝图速查与最佳实践
5.1 核心 API 速查
5.2 避坑指南与最佳实践
-
永远在应用工厂内部导入蓝图
这是避免循环导入(circular import)的唯一可靠方式。
不要在文件顶部用 from app.auth import auth_bp 这种写法。
-
蓝图内的模板/静态文件必须加子目录
模板路径应写成 templates/main/index.html 而非 templates/index.html,否则多个蓝图有同名模板时会互相覆盖。
-
url_for 必须加上蓝图标识前缀
即便是在同一个蓝图内跳转,也建议写上 url_for("articles.detail", post_id=post.id),保持代码风格一致,也避免潜在冲突。
-
按功能拆分蓝图,不要按技术分层
好的划分是“用户模块”“文章模块”,而不是“路由模块”“模板模块”。前者更贴近业务,后者只会制造理解困难。
-
复杂的蓝图可以继续拆分子模块
如果某个模块过于庞大(比如后台管理),可以进一步拆成“文章管理”“用户管理”“配置管理”等多个子蓝图,依然保持清晰。
6. 小结
本文针对 Flask 单文件开发带来的维护性瓶颈,使用蓝图将道满博客拆解成了乐高式的可复用模块:
- 从“面条代码”到“分层架构”的目录重构
- 主功能、认证、文章三个核心蓝图的创建与路由绑定
- 应用工厂函数的实现与蓝图注册
- 蓝图核心 API 速查及避坑指南
现在,你可以放心地在 articles 模块里添加评论功能,在 auth 模块里接入第三方登录,完全不用担心改动会影响到其他模块的代码了!
🔗 扩展阅读