跳到主要内容
预计阅读 56 分钟

12|用Flask构建全栈Web服务:从路由到部署

一个脚本无法满足的需求

你写了一个 Python 脚本帮团队做数据汇总。起初大家很满意,但很快问题来了:每个人都要装 Python 环境,每次都要在终端里敲命令,不懂技术的同事根本用不了。团队负责人问了一个问题:“能不能做成网页?打开浏览器就能用?”

这正是 Web 开发解决的核心问题:将程序的能力以浏览器可访问的形式暴露出来,让任何人——无论是否懂编程——都可以通过网络使用你的程序。Flask 是 Python 生态中最适合起步的 Web 框架:代码量少,概念清晰,几行代码就能让一个 Python 函数变成可通过浏览器访问的服务。

本章将从 Web 通信的底层模型讲起,逐步构建一个完整的任务管理 Web 应用,最后介绍如何将其部署到生产环境。

第一部分:Web 通信模型

在写任何代码之前,先弄清楚 Web 应用是怎么工作的。

请求-响应循环

Web 应用的运行机制可以归结为一个不断重复的循环:

  浏览器端                           服务器端
     |                                 |
     |  "给我 /dashboard 页面"          |
     |  ---- HTTP Request (GET) ---->  |
     |                                 |  Flask 找到 /dashboard 对应的处理函数
     |                                 |  函数查询数据库,渲染 HTML
     |  <--- HTTP Response (HTML) ---  |  返回 HTML 内容
     |                                 |
     |  浏览器渲染 HTML 并显示           |

这个循环中有四个关键概念:

  • 请求(Request):浏览器向服务器发送的消息,包含目标 URL 和请求方法(GET/POST)
  • 响应(Response):服务器返回的消息,包含状态码(成功/失败)和内容(通常是 HTML)
  • 路由(Route):URL 路径与处理函数之间的映射关系。访问 /login 就执行登录逻辑,访问 /settings 就执行设置逻辑
  • 模板(Template):包含占位符的 HTML 文件。同一份模板填入不同数据,就产生不同页面

Flask 的定位

Flask 自称”微框架”(microframework)。“微”不是指功能弱,而是指核心精简:它只内置路由分发、模板渲染和请求处理三项基础能力。数据库、用户认证、文件上传等功能通过独立扩展按需接入。这种设计的优势是:你永远只加载自己需要的部分,学习曲线极为平缓。

第二部分:最小可运行的 Flask 应用

安装

pip install flask

五行代码启动 Web 服务

创建文件 server.py

from flask import Flask

web = Flask(__name__)  # 社区惯例通常命名为 app,本课程使用 web 以突出"Web 服务"的语义

@web.route("/")
def landing_page():
    return "<h1>Service Online</h1><p>Flask is running.</p>"

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

运行:

python server.py

终端输出:

 * Running on http://127.0.0.1:5000
 * Debug mode: on

用浏览器访问 http://127.0.0.1:5000/,你会看到页面上显示 “Service Online”。此刻,你的 Python 代码正在以一个持续运行的网络服务形式工作,等待来自浏览器的每一次请求。

debug=True 的含义与风险: 调试模式提供两个便利——代码修改后服务自动重启;出错时浏览器显示详细的错误堆栈。但在正式上线时必须关闭此模式,因为它会暴露服务器内部信息,存在严重安全隐患。

第三部分:路由系统与视图函数

路由是 Flask 的核心调度机制——它决定”用户访问某个 URL 时执行哪个函数”。被路由绑定的函数称为视图函数(view function),其返回值就是发送给浏览器的响应。

静态路由

from flask import Flask

web = Flask(__name__)

@web.route("/")
def home():
    return "Home Page"

@web.route("/status")
def health_check():
    return "System operational"

@web.route("/docs")
def documentation():
    return "API Documentation v2.0"

动态路由:URL 参数

URL 中的变量部分用尖括号标记,Flask 自动提取并传入视图函数:

from markupsafe import escape

@web.route("/profile/<handle>")
def user_profile(handle):
    return f"<h1>Profile: {escape(handle)}</h1>"

@web.route("/ticket/<int:ticket_id>")
def view_ticket(ticket_id):
    return f"Viewing ticket #{ticket_id}"

@web.route("/invoice/<float:amount>")
def generate_invoice(amount):
    return f"Invoice amount: ${amount:.2f}"

类型转换器列表:

转换器说明URL 示例
string默认,任意文本(不含 //profile/jdoe
int正整数/ticket/42
float正浮点数/invoice/99.95
path/ 的路径/files/reports/q4.pdf

处理多种 HTTP 方法

同一个 URL 在不同请求方法下可能有不同的行为。典型场景:GET 时展示表单,POST 时处理提交。

from flask import request
from markupsafe import escape

@web.route("/authenticate", methods=["GET", "POST"])
def authenticate():
    if request.method == "POST":
        acct = request.form.get("account", "")
        secret = request.form.get("secret", "")

        if acct == "operator" and secret == "s3cure!":
            return f"<h1>Welcome, {escape(acct)}</h1>"
        else:
            return '<p style="color:red">Invalid credentials</p>'
    else:
        return '''
            <h1>Sign In</h1>
            <form method="post">
                <p>Account: <input type="text" name="account"></p>
                <p>Secret: <input type="password" name="secret"></p>
                <p><input type="submit" value="Sign In"></p>
            </form>
        '''

request 是 Flask 提供的全局对象,封装了当前 HTTP 请求的全部信息。request.method 标识请求类型,request.form 包含 POST 表单数据,request.args 包含 URL 查询参数(如 ?q=test)。

第四部分:Jinja2 模板引擎

在视图函数中拼接 HTML 字符串是不可维护的。Flask 内置 Jinja2 模板引擎,将 HTML 结构与动态数据分离到不同文件中。

项目目录规范

project_root/
    server.py            # 主程序
    templates/           # 模板目录(固定名称)
        layout.html      # 基础布局
        home.html        # 首页
        auth.html        # 登录页
    static/              # 静态资源目录
        app.css          # 样式表

Flask 会自动在 templates/ 中查找模板文件,在 static/ 中提供静态资源。

模板继承

所有页面共享的结构(导航、页脚、CSS 引用)定义在基础布局中,子模板只填充差异部分:

基础布局(templates/layout.html):

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block page_title %}Task Manager{% endblock %}</title>
    <link rel="stylesheet"
          href="{{ url_for('static', filename='app.css') }}">
</head>
<body>
    <nav>
        <a href="{{ url_for('home') }}">Home</a>
        <a href="{{ url_for('documentation') }}">Docs</a>
    </nav>

    <main>
        {% for msg in get_flashed_messages() %}
            <div class="notification">{{ msg }}</div>
        {% endfor %}

        {% block main_content %}{% endblock %}
    </main>

    <footer>
        <p>Built with Flask</p>
    </footer>
</body>
</html>

首页模板(templates/home.html):

{% extends 'layout.html' %}

{% block page_title %}Dashboard{% endblock %}

{% block main_content %}
<h1>Dashboard</h1>

{% if operator %}
    <p>Logged in as: {{ operator }}</p>
{% else %}
    <p><a href="{{ url_for('authenticate') }}">Sign in</a> to continue</p>
{% endif %}

<h2>Recent Tasks</h2>
<ul>
{% for task in task_list %}
    <li>
        <a href="/ticket/{{ task.tid }}">{{ task.summary }}</a>
        <span class="timestamp">{{ task.created_at }}</span>
    </li>
{% endfor %}
</ul>
{% endblock %}

子模板通过 {% extends 'layout.html' %} 声明继承关系,用 {% block name %}...{% endblock %} 覆写父模板中定义的可替换区域。

Jinja2 语法要素

标记作用示例
{{ }}输出表达式的值{{ task.summary }}{{ total * 1.08 }}
{% %}控制逻辑(条件/循环){% if active %}...{% endif %}
{# #}注释(不输出到 HTML){# navigation section #}
{# 条件渲染 #}
{% if priority == "high" %}
    <span class="badge-critical">Urgent</span>
{% elif priority == "medium" %}
    <span class="badge-warning">Medium</span>
{% else %}
    <span class="badge-info">Low</span>
{% endif %}

{# 循环渲染 #}
{% for item in inventory %}
    <tr>
        <td>{{ loop.index }}</td>
        <td>{{ item.sku }}</td>
        <td>{{ item.unit_price|round(2) }}</td>
    </tr>
{% endfor %}

{# 过滤器 #}
{{ description|upper }}           {# 全部大写 #}
{{ content|truncate(80) }}        {# 截断到 80 字符 #}
{{ raw_html|safe }}               {# 不转义 HTML #}

在视图函数中渲染模板

from flask import Flask, render_template

web = Flask(__name__)

@web.route("/")
def home():
    task_list = [
        {"tid": 1, "summary": "Deploy v2.3", "created_at": "2025-11-01"},
        {"tid": 2, "summary": "Fix auth bug", "created_at": "2025-11-02"},
        {"tid": 3, "summary": "Update docs", "created_at": "2025-11-03"},
    ]
    return render_template("home.html",
                           operator="admin",
                           task_list=task_list)

render_template() 负责两件事:定位模板文件,将传入的数据填充到占位符中。返回的是完整的 HTML 字符串。

第五部分:表单数据处理

接收并验证表单提交

from flask import Flask, request, redirect, url_for, flash, render_template

web = Flask(__name__)
web.secret_key = "change-this-before-production"

@web.route("/signup", methods=["GET", "POST"])
def signup():
    if request.method == "POST":
        handle = request.form.get("handle", "").strip()
        mail = request.form.get("mail", "").strip()
        passphrase = request.form.get("passphrase", "")

        # 服务端校验
        issues = []
        if not handle or len(handle) < 3:
            issues.append("Handle must be at least 3 characters")
        if not mail or "@" not in mail:
            issues.append("Valid email address required")
        if len(passphrase) < 8:
            issues.append("Passphrase must be at least 8 characters")

        if issues:
            for issue in issues:
                flash(issue, "error")
            return render_template("signup.html")

        # 校验通过,执行注册逻辑
        flash("Account created successfully", "success")
        return redirect(url_for("authenticate"))

    return render_template("signup.html")

核心工具:

  • request.form:获取 POST 表单数据
  • flash():设置一次性消息(刷新后消失)
  • redirect():重定向到另一个 URL
  • url_for():根据函数名反向生成 URL

URL 查询参数

@web.route("/search")
def search():
    query = request.args.get("q", "")
    pg = request.args.get("page", 1, type=int)
    return f"Searching for '{query}', page {pg}"

第六部分:SQLite 数据库集成

连接管理

Web 应用中的数据库连接需要跟随请求的生命周期:请求到达时获取连接,请求结束后释放连接。Flask 的 g 对象和 teardown_appcontext 机制可以实现这一点。

import sqlite3
from flask import Flask, g

web = Flask(__name__)
web.config["DB_PATH"] = "taskman.db"

def acquire_db():
    """获取数据库连接(同一请求内复用)"""
    if "conn" not in g:
        g.conn = sqlite3.connect(web.config["DB_PATH"])
        g.conn.row_factory = sqlite3.Row
    return g.conn

@web.teardown_appcontext
def release_db(exc):
    """请求结束时自动关闭连接"""
    conn = g.pop("conn", None)
    if conn is not None:
        conn.close()

设计要点:

  • g 是 Flask 的请求级上下文变量。同一请求内多次调用 acquire_db() 返回同一个连接对象。不同请求之间的 g 完全隔离。
  • @web.teardown_appcontext 注册的回调在每次请求结束后自动执行,无论请求是否抛出异常。

初始化表结构

def setup_schema():
    """创建数据库表"""
    conn = acquire_db()
    conn.execute("""
        CREATE TABLE IF NOT EXISTS tasks (
            tid INTEGER PRIMARY KEY AUTOINCREMENT,
            summary TEXT NOT NULL,
            completed BOOLEAN NOT NULL DEFAULT 0,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    conn.commit()

第七部分:综合实战——任务管理应用

将前面所有知识组装成一个功能完整的 Web 应用。支持以下功能:

  • 创建新任务
  • 切换任务的完成状态
  • 删除单条任务
  • 批量清除已完成任务
  • 实时统计面板

目录结构

taskman/
    server.py            # 主程序
    taskman.db           # SQLite 数据库(自动创建)
    templates/
        layout.html      # 基础布局
        dashboard.html   # 主页面
    static/
        app.css          # 样式表

主程序(server.py)

import sqlite3
from flask import (Flask, render_template, request,
                   redirect, url_for, flash, g)

web = Flask(__name__)
web.secret_key = "replace-with-strong-random-value"
web.config["DB_PATH"] = "taskman.db"

# ==================== Database Layer ====================

def acquire_db():
    if "conn" not in g:
        g.conn = sqlite3.connect(web.config["DB_PATH"])
        g.conn.row_factory = sqlite3.Row
    return g.conn

@web.teardown_appcontext
def release_db(exc):
    conn = g.pop("conn", None)
    if conn is not None:
        conn.close()

def setup_schema():
    conn = acquire_db()
    conn.execute("""
        CREATE TABLE IF NOT EXISTS tasks (
            tid INTEGER PRIMARY KEY AUTOINCREMENT,
            summary TEXT NOT NULL,
            completed BOOLEAN NOT NULL DEFAULT 0,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    conn.commit()

# ==================== View Functions ====================

@web.route("/")
def dashboard():
    """主面板:展示所有任务"""
    conn = acquire_db()
    all_tasks = conn.execute(
        "SELECT * FROM tasks ORDER BY created_at DESC"
    ).fetchall()

    total_count = len(all_tasks)
    done_count = sum(1 for t in all_tasks if t["completed"])
    pending_count = total_count - done_count

    return render_template("dashboard.html",
                           task_list=all_tasks,
                           total_count=total_count,
                           done_count=done_count,
                           pending_count=pending_count)

@web.route("/create", methods=["POST"])
def create_task():
    """创建新任务"""
    summary = request.form.get("summary", "").strip()

    if not summary:
        flash("Task summary cannot be empty")
    elif len(summary) > 300:
        flash("Summary must be 300 characters or less")
    else:
        conn = acquire_db()
        conn.execute("INSERT INTO tasks (summary) VALUES (?)", (summary,))
        conn.commit()
        flash("Task created")

    return redirect(url_for("dashboard"))

@web.route("/flip/<int:tid>", methods=["POST"])
def flip_status(tid):
    """切换完成状态(使用 POST,因为这是状态变更操作)"""
    conn = acquire_db()
    task = conn.execute(
        "SELECT * FROM tasks WHERE tid = ?", (tid,)
    ).fetchone()

    if task:
        new_state = not task["completed"]
        conn.execute("UPDATE tasks SET completed = ? WHERE tid = ?",
                     (new_state, tid))
        conn.commit()

    return redirect(url_for("dashboard"))

@web.route("/remove/<int:tid>", methods=["POST"])
def remove_task(tid):
    """删除任务"""
    conn = acquire_db()
    conn.execute("DELETE FROM tasks WHERE tid = ?", (tid,))
    conn.commit()
    flash("Task removed")
    return redirect(url_for("dashboard"))

@web.route("/purge-completed", methods=["POST"])
def purge_completed():
    """清除所有已完成任务"""
    conn = acquire_db()
    result = conn.execute("DELETE FROM tasks WHERE completed = 1")
    conn.commit()
    removed = result.rowcount
    flash(f"Purged {removed} completed tasks")
    return redirect(url_for("dashboard"))

# ==================== Application Entry ====================

if __name__ == "__main__":
    with web.app_context():
        setup_schema()
    web.run(debug=True)

基础布局(templates/layout.html)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block page_title %}Task Manager{% endblock %}</title>
    <link rel="stylesheet"
          href="{{ url_for('static', filename='app.css') }}">
</head>
<body>
    <div class="wrapper">
        <header>
            <h1>Task Manager</h1>
            <p class="tagline">Built with Flask and SQLite</p>
        </header>

        {% with notifications = get_flashed_messages() %}
            {% if notifications %}
                {% for msg in notifications %}
                    <div class="notification">{{ msg }}</div>
                {% endfor %}
            {% endif %}
        {% endwith %}

        {% block main_content %}{% endblock %}
    </div>
</body>
</html>

主面板模板(templates/dashboard.html)

{% extends 'layout.html' %}

{% block main_content %}
<!-- 任务创建表单 -->
<form method="post" action="{{ url_for('create_task') }}" class="input-bar">
    <input type="text" name="summary"
           placeholder="What needs to be done?" autofocus required
           maxlength="300">
    <button type="submit">Add</button>
</form>

<!-- 统计面板 -->
<div class="metrics">
    <span>Total: {{ total_count }}</span>
    <span>Pending: {{ pending_count }}</span>
    <span>Done: {{ done_count }}</span>
    {% if done_count > 0 %}
        <form method="post" action="{{ url_for('purge_completed') }}" class="inline-form">
            <button type="submit" class="purge-link">Purge completed</button>
        </form>
    {% endif %}
</div>

<!-- 任务列表 -->
{% if total_count > 0 %}
<ul class="task-list">
    {% for task in task_list %}
    <li class="task-row {% if task.completed %}finished{% endif %}">
        <form method="post" action="{{ url_for('flip_status', tid=task.tid) }}" class="inline-form">
            <button type="submit" class="status-toggle" title="Toggle status">
                {% if task.completed %}[x]{% else %}[ ]{% endif %}
            </button>
        </form>
        <span class="task-text">{{ task.summary }}</span>
        <form method="post" action="{{ url_for('remove_task', tid=task.tid) }}" class="inline-form"
              onsubmit="return confirm('Remove this task?')">
            <button type="submit" class="remove-link">Remove</button>
        </form>
    </li>
    {% endfor %}
</ul>
{% else %}
<p class="no-data">No tasks yet. Add one above.</p>
{% endif %}
{% endblock %}

样式表(static/app.css)

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
                 Roboto, "Helvetica Neue", Arial, sans-serif;
    background: #eef1f5;
    color: #2d3436;
    line-height: 1.6;
}

.wrapper {
    max-width: 640px;
    margin: 50px auto;
    padding: 0 24px;
}

header {
    text-align: center;
    margin-bottom: 32px;
}

header h1 {
    font-size: 2.4em;
    color: #1a1a2e;
}

header .tagline {
    color: #a0a4a8;
    font-size: 0.85em;
    margin-top: 4px;
}

/* Input bar */
.input-bar {
    display: flex;
    gap: 10px;
    margin-bottom: 24px;
}

.input-bar input[type="text"] {
    flex: 1;
    padding: 14px 18px;
    border: 2px solid #d1d5db;
    border-radius: 10px;
    font-size: 16px;
    outline: none;
    transition: border-color 0.25s;
}

.input-bar input[type="text"]:focus {
    border-color: #5b6abf;
}

.input-bar button {
    padding: 14px 28px;
    background: #5b6abf;
    color: white;
    border: none;
    border-radius: 10px;
    font-size: 16px;
    cursor: pointer;
    transition: background 0.2s;
}

.input-bar button:hover {
    background: #4a59a8;
}

/* Metrics bar */
.metrics {
    display: flex;
    gap: 16px;
    margin-bottom: 16px;
    padding: 12px 18px;
    background: white;
    border-radius: 10px;
    font-size: 14px;
    color: #6b7280;
    align-items: center;
}

.inline-form {
    display: inline;
    margin: 0;
    padding: 0;
}

.purge-link {
    margin-left: auto;
    color: #dc3545;
    background: none;
    border: none;
    font-size: 13px;
    cursor: pointer;
    padding: 0;
}

.purge-link:hover { text-decoration: underline; }

/* Task list */
.task-list { list-style: none; }

.task-row {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 16px 18px;
    background: white;
    margin-bottom: 8px;
    border-radius: 10px;
    box-shadow: 0 1px 4px rgba(0,0,0,0.05);
    transition: transform 0.12s;
}

.task-row:hover { transform: translateX(4px); }

.task-row.finished .task-text {
    text-decoration: line-through;
    color: #b0b0b0;
}

.status-toggle {
    background: none;
    border: none;
    font-family: monospace;
    font-size: 20px;
    color: #5b6abf;
    cursor: pointer;
    user-select: none;
    padding: 0;
}

.task-text { flex: 1; font-size: 16px; }

.remove-link {
    color: #dc3545;
    background: none;
    border: none;
    font-size: 13px;
    cursor: pointer;
    padding: 0;
    opacity: 0;
    transition: opacity 0.2s;
}

.task-row:hover .remove-link { opacity: 1; }

/* Notifications */
.notification {
    padding: 12px 18px;
    margin-bottom: 16px;
    background: #cce5ff;
    color: #004085;
    border-radius: 8px;
    font-size: 14px;
}

.no-data {
    text-align: center;
    color: #aaa;
    margin-top: 48px;
}

运行

将上述文件按目录结构放置后,在 taskman/ 目录下执行:

python server.py

访问 http://127.0.0.1:5000/,你会看到一个具备完整 CRUD 功能的任务管理应用。

第八部分:生产环境部署

开发阶段使用 web.run() 启动的内置服务器仅支持单进程单线程,无法应对并发访问。生产环境需要专业的服务器组件。

标准架构

用户请求
   |
   v
Nginx (反向代理)
   |--- 静态文件 (CSS/JS/图片) ---> 由 Nginx 直接响应
   |--- 动态请求 ---> Gunicorn (WSGI 服务器) ---> Flask 应用
  • Gunicorn:Python WSGI 服务器,能启动多个工作进程并行处理请求
  • Nginx:高性能反向代理,负责 SSL 终止、静态资源分发和负载均衡

部署命令

# 安装 Gunicorn
pip install gunicorn

# 启动(4 个工作进程,绑定到 8000 端口)
gunicorn -w 4 -b 0.0.0.0:8000 server:web
# server = 文件名 server.py(不含 .py 后缀)
# web = Flask 实例变量名(即 web = Flask(__name__) 中的变量名)
# 如果你的文件叫 app.py 且实例变量叫 app,则写 app:app

Nginx 配置片段

server {
    listen 80;
    server_name taskman.example.com;

    location /static {
        alias /opt/taskman/static;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

上线前安全清单

import os
import secrets

# 1. 关闭调试模式
web.run(debug=False)

# 2. 使用强随机密钥
#    ⚠️ 生产环境必须通过环境变量设置固定值!
#    下面的 fallback 会在每次重启时生成不同密钥,导致已有 session 全部失效。
#    部署前务必执行:export APP_SECRET=$(python -c "import secrets; print(secrets.token_hex(32))")
web.secret_key = os.environ.get("APP_SECRET", secrets.token_hex(32))

# 3. 敏感配置通过环境变量注入
web.config["DB_PATH"] = os.environ.get("DB_PATH", "taskman.db")

# 4. 所有用户输入做校验和转义
# 5. 启用 HTTPS(Nginx + Let's Encrypt)
# 6. 定期备份数据库

本章回顾

本章从零构建了一个全栈 Web 应用,覆盖了 Web 开发的核心要素:

知识领域技术实现Flask 中的对应
路由调度URL 到函数的映射@web.route()
视图函数处理请求并返回响应返回字符串 / render_template()
模板引擎HTML 与数据的分离Jinja2:{{ }}{% %}
模板继承页面结构复用{% extends %}{% block %}
表单处理接收用户提交request.formrequest.args
数据持久化数据库存储SQLite + g 对象
消息反馈操作结果提示flash()
生产部署正式上线运行Gunicorn + Nginx

Flask 的扩展生态覆盖了几乎所有常见的 Web 开发需求:

  • Flask-SQLAlchemy:ORM 数据库抽象层
  • Flask-Login:用户认证与会话管理
  • Flask-WTF:表单校验与 CSRF 防护
  • Flask-RESTful:RESTful API 构建
  • Flask-Migrate:数据库版本迁移

从命令行脚本到可通过浏览器访问的 Web 服务,这是一个质的跨越。你的代码不再只服务于自己,而是可以服务于任何有浏览器的人。这意味着你已经具备了构建面向用户的完整软件产品的能力。

延伸:Web 开发的下一步

Flask 的”微”框架定位意味着,当你的应用复杂度增长时,你需要主动引入更多工具。以下是几个常见的进阶方向和对应的技术选择:

数据库升级。 SQLite 适合原型和轻量级应用,但不支持并发写入。当用户量增长后,应迁移到 PostgreSQL 或 MySQL。Flask-SQLAlchemy 提供了统一的 ORM 接口,切换数据库只需修改连接字符串,不需要重写查询代码。

用户认证系统。 真实应用需要注册、登录、权限管理。Flask-Login 处理会话管理和登录状态维护,Flask-Bcrypt 处理密码的安全存储(永远不要明文存储密码)。

API 化。 如果你的后端需要同时服务网页端和移动端,应该将业务逻辑暴露为 RESTful API。Flask-RESTful 或 Flask 原生的 jsonify() 函数可以帮助你构建 JSON API。

from flask import jsonify

@web.route("/api/tasks")
def api_task_list():
    conn = acquire_db()
    rows = conn.execute("SELECT * FROM tasks ORDER BY created_at DESC").fetchall()
    return jsonify([dict(row) for row in rows])

前端框架集成。 当页面交互变得复杂时,可以考虑将前端切换到 React 或 Vue.js,Flask 仅作为后端 API 服务。这种前后端分离的架构是当前 Web 开发的主流模式。

自动化测试。 Flask 内置了测试客户端,可以在不启动真实服务器的情况下测试你的视图函数:

def test_dashboard_returns_200():
    web.config["TESTING"] = True
    client = web.test_client()
    response = client.get("/")
    assert response.status_code == 200

容器化部署。 将应用打包为 Docker 容器,可以确保在任何环境中都能一致运行。一个典型的 Dockerfile 只需要几行:

FROM python:3.12-slim
WORKDIR /opt/taskman
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "server:web"]

Web 开发是一个深度和广度都很大的领域。本章为你建立了一个坚实的起点:你理解了请求-响应模型,掌握了路由、模板、表单处理和数据库操作,并且知道如何将应用部署到生产环境。在此基础上,无论你选择深入后端架构还是转向全栈开发,都有了清晰的路径。

购买课程解锁全部内容

零基础到独立开发:Python 自动化与 Web 实战

¥29.90