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():重定向到另一个 URLurl_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.form、request.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