项目架构重构(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                   # 启动文件(简单到只有两行!)

3. 手把手:创建并拼接第一个蓝图

我们先从最简单的主功能蓝图(main_bp) 入手,快速跑通流程。

3.1 定义蓝图实例

每个蓝图模块的 __init__.py,就是它的“身份证”:我们在这里给蓝图起名字、绑定文件路径、加统一的 URL 前缀(可选)。

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

# 创建蓝图实例,三个核心参数:
# 1. "main":蓝图的唯一标识(后面用 url_for 时的前缀)
# 2. __name__:蓝图的根目录(用来找对应模板/静态文件)
# 3. 可选参数:比如 url_prefix、template_folder(默认是模块下的 templates/)
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()
@main_bp.route("/")
def index():
    # 这里不用 app.py 的全局变量了,后面讲应用工厂时会处理配置
    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)   # 认证蓝图在 __init__.py 里加了 /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 可以从 app.models 导入

    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 前缀)

认证相关的路由(登录、注册、退出),加个统一前缀更规范:

# 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("index"),得写 url_for("main.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"))

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. 永远在应用工厂内部导入蓝图:这是避免循环导入的唯一有效方法
  2. 蓝图内的模板/静态文件必须加子目录:比如 templates/main/index.html,否则多个蓝图有同名模板会冲突
  3. url_for 必须加蓝图标识前缀:即使是同一个模块内的视图函数跳转,也建议加前缀,保持代码一致性
  4. 按功能拆分蓝图,不要按技术分层:比如不要拆成“路由模块”“模板模块”,要拆成“用户模块”“文章模块”
  5. 复杂的蓝图可以继续拆分子模块:比如后台模块可以拆成“文章管理”“用户管理”“配置管理”三个子蓝图

6. 小结

这篇文章我们解决了 Flask 单文件开发的维护性瓶颈,用蓝图把道满博客拆成了乐高式的可复用模块:

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

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


🔗 扩展阅读