Project architecture reconstruction (Blueprints): Turn 5000 lines of noodle code into Lego blocks

📂 Stage: Stage 4 - Practical Exercise (Daoman Blog) 🔗 Related chapters: 路由(Routing)艺术 · Flask-Login 实战


1. From "single file fun" to "refactoring nightmare"

When many people first come into contact with Flask, they will be attracted by a scene: Create a newapp.py, type a dozen lines of code, refresh the browser and you will see Hello World - this zero-configuration, instant feedback experience is exactly the charm of Flask's "micro-framework" positioning.

But this pleasure often doesn't last long. As more and more pages are added - homepage, login and registration, article publishing, background management, API interface, error handling...app.pywill irreversibly swell into a 5000+ lines of spaghetti code:

😱 三个月后再打开 app.py 的你:
─────────────────────────────────────
• 想找登录逻辑?翻 200 行才看到。
• 改一下首页渲染,却不小心动到了后台配置。
• 加一个新接口,仿佛是牵一发动全身的噩梦。
• 新人一看到这个文件,直接劝退。

At this time, what you need is Blueprints, the modular architecture tool officially provided by Flask.


2. What is the blueprint? Not a drawing software!

Simply put, a blueprint is a reusable "function module container" in Flask.

It can mix the originalapp.pyThe routes, templates, static files, error handling and even custom filters are all packaged into independent folders. Finally, these modules are connected to the core Flask application through an "application factory", just like putting together Lego blocks.

The Daoman blog directory reconstructed with the blueprint will change from "flat chaos" to a "clearly layered" Lego-like structure:

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

💡 Note: under each blueprinttemplates/In the folder, there must be one more nested subdirectory with the same name as the blueprint (such asmain/auth/), otherwise conflicts will occur when there are templates with the same name under multiple blueprints.


3. Step by step: Create and stitch together the first blueprint

Let’s start with the simplest main function blueprint (main_bp) and quickly run through the entire process.

3.1 Define blueprint instance

for each blueprint module__init__.py, is its "ID card". Here we set the blueprint name, file path, and optional URL prefix.

# 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 Write main function route

Routes are now no longer directly decoratedapp.route, instead use Blueprint instance'sroute

# 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 Register blueprint with application factory

Next is the core step of reconstruction: convert the original directly createdapp = Flask(__name__)Change it to a function - Application Factory (create_app). The advantage of this is that you can create applications with different configurations in different environments (development/test/production), and you can also register blueprints flexibly.

# 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 Write the simplest startup file

At this point, start the filerun.pyIt becomes very concise:

# run.py
from app import create_app

app = create_app()

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

4. Advanced: Prefixed certification and article blueprint

4.1 Certification Blueprint (unified plus/authprefix)

Unifiedly add authentication-related routes/authThe prefix not only makes the URL look more standardized, but also facilitates unified adjustment in the future.

# 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"))

💡 Important reminder: After using the blueprint, **allurl_forAll must be prefixed with the blueprint identifier **. Even if it is an internal jump in the same blueprint, it is recommended to write it in full to maintain consistency.

4.2 Article blueprint (business logic with paging and permissions)

Although the business logic of the article blueprint is complex, it is still clear and easy to read after splitting:

# 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. Blueprint quick review and best practices

5.1 Core API Quick Check

OperationsCode Examples
Create a blueprintbp = Blueprint("bp_name", __name__, url_prefix="/prefix")
Bind route@bp.route("/path", methods=["GET"])
Register to the appapp.register_blueprint(bp)
Generate URL within blueprinturl_for("bp_name.view_func", param1=val1)
Specify blueprint-specific template folderbp = Blueprint(..., template_folder="my_templates")(Default istemplates/
Specify blueprint-specific static folderbp = Blueprint(..., static_folder="my_static")(Default isstatic/

5.2 Pitfall avoidance guidelines and best practices

  1. Always import blueprints inside the application factory This is the only reliable way to avoid circular imports. Don't use it at the top of the filefrom app.auth import auth_bpThis way of writing.

  2. Template/static files in the blueprint must be added to a subdirectory The template path should be written astemplates/main/index.htmlrather thantemplates/index.html, otherwise multiple blueprints will overwrite each other if they have templates with the same name.

  3. **url_forMust be prefixed with blueprint identification ** Even if you jump within the same blueprint, it is recommended to writeurl_for("articles.detail", post_id=post.id), keep the code style consistent and avoid potential conflicts.

  4. Split the blueprint by function, not by technology A good division is "user module" and "article module" rather than "routing module" and "template module". The former is closer to the business, while the latter will only create difficulties in understanding.

  5. Complex blueprints can continue to be split into sub-modules If a certain module is too large (such as backend management), it can be further split into multiple sub-blueprints such as "article management", "user management" and "configuration management", while still maintaining clarity.


6. Summary

This article aims at the maintenance bottleneck caused by Flask single-file development, and uses blueprints to disassemble Daoman Blog into Lego-style reusable modules:

  1. Directory reconstruction from “noodle code” to “layered architecture”
  2. Creation and routing binding of three core blueprints: main function, authentication, and article
  3. Implementation of application factory functions and blueprint registration
  4. Blueprint core API quick check and pitfall avoidance guide

Now you can safelyarticlesAdd comment function to the module, inauthThe module is connected to a third-party login, so there is no need to worry that changes will affect the code of other modules!


🔗 Extended reading