项目架构重构(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 速查

操作代码示例
创建蓝图bp = Blueprint("bp_name", __name__, url_prefix="/prefix")
绑定路由@bp.route("/path", methods=["GET"])
注册到应用app.register_blueprint(bp)
生成蓝图内的 URLurl_for("bp_name.view_func", param1=val1)
指定蓝图专属模板文件夹bp = Blueprint(..., template_folder="my_templates")(默认是 templates/
指定蓝图专属静态文件夹bp = Blueprint(..., static_folder="my_static")(默认是 static/

5.2 避坑指南与最佳实践

  1. 永远在应用工厂内部导入蓝图
    这是避免循环导入(circular import)的唯一可靠方式。
    不要在文件顶部用 from app.auth import auth_bp 这种写法。

  2. 蓝图内的模板/静态文件必须加子目录
    模板路径应写成 templates/main/index.html 而非 templates/index.html,否则多个蓝图有同名模板时会互相覆盖。

  3. url_for 必须加上蓝图标识前缀
    即便是在同一个蓝图内跳转,也建议写上 url_for("articles.detail", post_id=post.id),保持代码风格一致,也避免潜在冲突。

  4. 按功能拆分蓝图,不要按技术分层
    好的划分是“用户模块”“文章模块”,而不是“路由模块”“模板模块”。前者更贴近业务,后者只会制造理解困难。

  5. 复杂的蓝图可以继续拆分子模块
    如果某个模块过于庞大(比如后台管理),可以进一步拆成“文章管理”“用户管理”“配置管理”等多个子蓝图,依然保持清晰。


6. 小结

本文针对 Flask 单文件开发带来的维护性瓶颈,使用蓝图将道满博客拆解成了乐高式的可复用模块:

  1. 从“面条代码”到“分层架构”的目录重构
  2. 主功能、认证、文章三个核心蓝图的创建与路由绑定
  3. 应用工厂函数的实现与蓝图注册
  4. 蓝图核心 API 速查及避坑指南

现在,你可以放心地在 articles 模块里添加评论功能,在 auth 模块里接入第三方登录,完全不用担心改动会影响到其他模块的代码了!


🔗 扩展阅读