Flask 教程:从入门到实战(华为云上机实操版)

Flask 教程:从入门到实战(华为云上机实操版)

环境 : 华为云 FlexusX ecs-60a4-0001 | Ubuntu 24.04.4 LTS | Python 3.12.3 | Flask 3.1.3

服务器 : 8vCPU / 16GiB / 可用区7 / 5Mbit/s BGP

特点: 全部代码在真实服务器上执行,输出为实机结果,非纸上谈兵


目录

篇章 章节 内容
入门篇 1-4 简介 → 安装 → 第一个应用 → 基本概念 → 项目结构
核心篇 5-10 路由 → 请求与响应 → 视图函数 → 模板渲染 → 静态文件 → Session与Cookie → 表单处理
进阶篇 11-16 数据库操作 → 蓝图 → 错误处理 → 中间件和扩展 → 配置管理 → 请求生命周期
实战篇 17-19 Flask Web 博客系统 → CLI命令与部署 → 测试

服务器架构图

复制代码
+----------------------------------------------------------+
|              华为云 FlexusX ecs-60a4-0001                 |
|              (114.116.250.214)                            |
|                                                          |
|  +-------------------+     +-----------------------+     |
|  |  Ubuntu 24.04.4   |     |   Python 3.12.3       |     |
|  |  8vCPU / 16GiB    |---->|   pip 24.0            |     |
|  +-------------------+     +-----------------------+     |
|                                    |                     |
|                    +---------------+---------------+     |
|                    |                               |     |
|            +-------v-------+               +-------v--+  |
|            | Flask 3.1.3   |               | SQLite   |  |
|            | Werkzeug 3.1.8|               | (内置)   |  |
|            | Jinja2 3.1.6  |               +----------+  |
|            +-------+-------+                             |
|                    |                                     |
|        +-----------+-----------+-----------+             |
|        |           |           |           |             |
|   +----v---+ +-----v--+ +-----v--+ +-----v--+            |
|   |Flask-  | |Flask-  | |Flask-  | |Flask-  |            |
|   |SQLAlch | |Migrate | |Login   | |WTF     |            |
|   |3.1.1   | |4.1.0   | |0.6.3   | |1.3.0   |            |
|   +--------+ +--------+ +--------+ +--------+            |
|                                                          |
+----------------------------------------------------------+

入门篇

1. Flask 简介

Flask 是一个用 Python 编写的轻量级 Web 应用框架(Microframework)。它被称为"微框架",因为它使用简单的核心,并通过扩展机制添加功能。

Flask 核心特性

特性 说明
轻量级 核心仅包含路由和模板引擎,不绑定 ORM、表单验证等
WSGI 基于 Werkzeug WSGI 工具库
模板 使用 Jinja2 模板引擎
路由 基于装饰器的 URL 路由系统
扩展 丰富的第三方扩展生态(SQLAlchemy、Login、WTF 等)
开发服务器 内置开发服务器和调试器
RESTful 天然适合构建 RESTful API

Flask vs Django 对比

对比项 Flask Django
设计理念 微框架,灵活组合 全栈框架,开箱即用
ORM 无内置(常用 SQLAlchemy) 内置 Django ORM
模板 Jinja2 Django Template (类似 Jinja2)
表单 Flask-WTF (可选) 内置 Form
Admin 无内置(用 Flask-Admin) 内置 Admin 后台
认证 Flask-Login (可选) 内置 Auth 系统
路由 装饰器风格 URLconf 列表风格
学习曲线 平缓 较陡
适合场景 API/微服务/小型应用 内容管理/大型 Web 应用

Flask 技术栈

复制代码
                    HTTP 请求
                       |
                       v
+--------------------------------------------------+
|              WSGI Server                          |
|  (Gunicorn / uWSGI / Werkzeug dev server)        |
+--------------------------------------------------+
                       |
                       v
+--------------------------------------------------+
|              Flask Application (app.py)          |
|                                                  |
|  +----------+   +----------+   +----------+     |
|  | Routing  |-->| View     |-->| Response |     |
|  | (Werkzeug)|  | Function |   | (Werkzeug)|    |
|  +----------+   +----------+   +----------+     |
|                       |                          |
|          +------------+------------+             |
|          |            |            |             |
|    +-----v---+  +-----v---+  +-----v----+       |
|    | Jinja2  |  |SQLAlchemy|  | Extensions|      |
|    | Template|  |  (ORM)   |  | (Login等) |      |
|    +---------+  +----------+  +-----------+      |
|                                                  |
+--------------------------------------------------+
                       |
                       v
                   HTTP 响应

2. Flask 安装

2.1 系统环境

复制代码
$ python3 --version
Python 3.12.3

$ cat /etc/os-release | head -3
PRETTY_NAME="Ubuntu 24.04.4 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"

2.2 踩坑:Ubuntu 24.04 的 PEP 668 保护

Ubuntu 24.04 默认禁止直接使用 pip install 安装到系统 Python:

复制代码
$ pip3 install flask
error: externally-managed-environment

× This environment is externally managed
╰─> See /usr/share/doc/python3.12/README.venv for more information.

解决方案(两种任选其一):

方案 命令 适用场景
方案 A:--break-system-packages pip3 install --break-system-packages flask 测试服务器/快速安装
方案 B:虚拟环境(推荐) python3 -m venv venv && source venv/bin/activate 生产环境

2.3 踩坑:blinker 版本冲突

安装 Flask 时会遇到 blinker 冲突(Ubuntu 24.04 自带 blinker 1.7.0):

复制代码
ERROR: Cannot uninstall blinker 1.7.0, RECORD file not found.
Hint: The package was installed by debian.

解决方案 :添加 --ignore-installed blinker

bash 复制代码
pip3 install --break-system-packages --ignore-installed blinker \
    flask flask-sqlalchemy flask-migrate flask-login flask-wtf

2.4 安装 email_validator(Flask-WTF 依赖)

bash 复制代码
pip3 install --break-system-packages email_validator

2.5 验证安装

复制代码
$ python3 -c "
import importlib.metadata as meta
def ver(pkg):
    try: return meta.version(pkg)
    except: return 'unknown'

pkgs = ['flask','werkzeug','jinja2','flask-sqlalchemy','sqlalchemy',
        'flask-migrate','flask-login','flask-wtf','click','itsdangerous','blinker']
for p in pkgs:
    print(f'{p:25s} {ver(p)}')
"

Flask:                    3.1.3
Werkzeug:                 3.1.8
Jinja2:                   3.1.6
Flask-SQLAlchemy:         3.1.1
SQLAlchemy:               2.0.51
Flask-Migrate:            4.1.0
Flask-Login:              0.6.3
Flask-WTF:                1.3.0
Click:                    8.4.2
Itsdangerous:             2.2.0
Blinker:                  1.9.0

2.6 Flask 核心组件验证

python 复制代码
modules = [
    'flask', 'flask.json', 'flask.testing', 'flask.templating',
    'flask.sessions', 'flask.wrappers', 'flask.blueprints',
    'flask.signals', 'flask.ctx', 'flask.config', 'flask.cli'
]
for mod in modules:
    __import__(mod)
    print(f"  [OK] {mod}")

实机输出:

复制代码
  [OK] flask
  [OK] flask.json
  [OK] flask.testing
  [OK] flask.templating
  [OK] flask.sessions
  [OK] flask.wrappers
  [OK] flask.blueprints
  [OK] flask.signals
  [OK] flask.ctx
  [OK] flask.config
  [OK] flask.cli

3. Flask 第一个应用

3.1 最小 Flask 应用

python 复制代码
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, Flask!'

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

3.2 使用 test_client 测试

在生产环境中我们使用 test_client() 模拟请求,无需启动真实服务器:

python 复制代码
with app.test_client() as client:
    resp = client.get('/')
    print(f"GET / -> Status: {resp.status_code}")
    print(f"Response: {resp.data.decode()}")
    print(f"Headers: {dict(resp.headers)}")

实机输出:

复制代码
GET / -> Status: 200
Response: Hello, Flask!
Headers: {'Content-Type': 'text/html; charset=utf-8', 'Content-Length': '13'}

3.3 启动开发服务器

bash 复制代码
# 方式 1: python 直接运行
python3 app.py

# 方式 2: flask CLI
export FLASK_APP=app.py
flask run --host=0.0.0.0 --port=5000 --debug
复制代码
 * Serving Flask app 'app'
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://192.168.0.238:5000

4. Flask 基本概念

4.1 Flask 应用对象属性

python 复制代码
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'my-secret-key-12345'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB

实机输出:

复制代码
app.name: flask_batch1
app.import_name: __main__
app.root_path: /tmp
app.debug: False
app.testing: False
app.secret_key: None
app.url_map:
Map([<Rule '/static/<filename>' (OPTIONS, GET, HEAD) -> static>,
 <Rule '/' (OPTIONS, GET, HEAD) -> hello>])

配置项示例:

复制代码
DEBUG: True
SECRET_KEY: my-secret-key-12345
MAX_CONTENT_LENGTH: 16777216

4.2 Flask 默认配置项

配置项 默认值 说明
DEBUG False 调试模式(自动重载 + 错误页面)
TESTING False 测试模式(异常直接抛出)
SECRET_KEY None Session 加密密钥(生产必须设置
PERMANENT_SESSION_LIFETIME 31 days Session 持久化过期时间
MAX_CONTENT_LENGTH None 请求体最大字节数
PREFERRED_URL_SCHEME http URL 生成的默认协议
MAX_COOKIE_SIZE 4093 Cookie 最大大小
TEMPLATES_AUTO_RELOAD None 模板自动重载
JSON_AS_ASCII True JSON 输出是否为 ASCII

5. Flask 项目结构

5.1 标准项目结构

复制代码
flask_project/
├── app/                    # 应用包
│   ├── __init__.py        # 应用工厂
│   ├── views.py           # 视图函数
│   ├── models.py          # 数据模型
│   ├── api/               # API 蓝图
│   ├── static/            # 静态文件
│   │   ├── css/
│   │   │   └── style.css
│   │   └── js/
│   │       └── main.js
│   └── templates/         # Jinja2 模板
│       └── base.html
├── migrations/             # 数据库迁移
├── tests/                  # 测试
├── config.py               # 配置文件
├── run.py                  # 启动脚本
└── requirements.txt        # 依赖清单

5.2 关键文件内容

app/init.py

python 复制代码
from flask import Flask

app = Flask(__name__)

from app import views

config.py

python 复制代码
DEBUG = True
SECRET_KEY = 'dev-secret-key'
SQLALCHEMY_DATABASE_URI = 'sqlite:///app.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False

run.py

python 复制代码
from app import app

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

requirements.txt

复制代码
flask
flask-sqlalchemy
flask-migrate

核心篇

6. Flask 路由

6.1 基本路由

python 复制代码
@app.route('/')
def index():
    return 'Index Page'

@app.route('/user/<username>')
def show_user(username):
    return f'User: {username}'

6.2 路由变量类型(转换器)

转换器 类名 说明 示例
string UnicodeConverter 默认,不包含斜杠 /user/<username>
int IntegerConverter 正整数 /post/<int:post_id>
float FloatConverter 正浮点数 /price/<float:price>
path PathConverter 包含斜杠的路径 /path/<path:subpath>
uuid UUIDConverter UUID 字符串 /item/<uuid:uid>
any AnyConverter 匹配多个固定值 /<any(about,help):page>

6.3 多规则路由

python 复制代码
@app.route('/hello')
@app.route('/hi')
def hello_both():
    return 'Hello or Hi!'

6.4 HTTP 方法

python 复制代码
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        return 'POST: Login submitted'
    return 'GET: Login form'

6.5 url_for URL 构建

python 复制代码
@app.route('/url-demo')
def url_demo():
    user_url = url_for('show_user', username='alice')
    post_url = url_for('show_post', post_id=42)
    index_url = url_for('index')
    return f'urls: user={user_url}, post={post_url}, index={index_url}'

6.6 实机测试结果

复制代码
  GET  /                    -> 200 Index Page
  GET  /user/alice          -> 200 User: alice
  GET  /post/42             -> 200 Post ID: 42 (type=int)
  GET  /path/a/b/c          -> 200 Subpath: a/b/c
  GET  /hello               -> 200 Hello or Hi!
  GET  /hi                  -> 200 Hello or Hi!
  GET  /login               -> 200 GET: Login form
  POST /login               -> 200 POST: Login submitted
  GET  /url-demo            -> 200 urls: user=/user/alice, post=/post/42, index=/

6.7 URL Map

复制代码
  /static/<path:filename>        GET        -> static
  /                              GET        -> index
  /user/<username>               GET        -> show_user
  /post/<int:post_id>            GET        -> show_post
  /path/<path:subpath>           GET        -> show_path
  /uuid/<uuid:uid>               GET        -> show_uuid
  /hi                            GET        -> hello_both
  /hello                         GET        -> hello_both
  /login                         GET,POST   -> login
  /url-demo                      GET        -> url_demo

7. Flask 请求与响应

7.1 Request 对象

python 复制代码
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/req-demo')
def req_demo():
    info = {
        'method': request.method,        # GET/POST/PUT/DELETE
        'url': request.url,              # 完整 URL
        'base_url': request.base_url,    # 不含 query string
        'path': request.path,            # 路径部分
        'args': dict(request.args),      # URL 查询参数
        'remote_addr': request.remote_addr,  # 客户端 IP
        'host': request.host,            # 主机名
        'is_secure': request.is_secure,  # 是否 HTTPS
    }
    return jsonify(info)

实机输出:

json 复制代码
{
  "args": {},
  "base_url": "http://localhost/req-demo",
  "headers.User-Agent": "TestClient/1.0",
  "host": "localhost",
  "is_secure": false,
  "method": "GET",
  "path": "/req-demo",
  "remote_addr": "127.0.0.1",
  "url": "http://localhost/req-demo"
}

7.2 获取请求参数

python 复制代码
# URL 查询参数 (?name=Alice&age=25)
@app.route('/req-args')
def req_args():
    name = request.args.get('name', 'Guest')         # 带默认值
    age = request.args.get('age', type=int, default=0)  # 类型转换
    return f'name={name}, age={age} (type={type(age).__name__})'

# POST JSON
@app.route('/req-data', methods=['POST'])
def req_data():
    if request.is_json:
        data = request.get_json()
        return jsonify({'received_json': data})
    else:
        data = request.form.to_dict()
        return jsonify({'received_form': data})

实机输出:

复制代码
GET /req-args?name=Alice&age=25 -> 200
Body: name=Alice, age=25 (type=int)

POST /req-data (JSON) -> 200
Body: {"received_json":{"key":"value","num":42}}

POST /req-data (Form) -> 200
Body: {"content_type":"application/x-www-form-urlencoded",
       "received_form":{"password":"secret","username":"bob"}}

7.3 Response 对象

python 复制代码
from flask import make_response

@app.route('/resp-custom')
def resp_custom():
    resp = make_response('Custom Response', 201)
    resp.headers['X-Custom-Header'] = 'Flask-Tutorial'
    resp.headers['Content-Type'] = 'text/plain'
    resp.set_cookie('custom_cookie', 'value123', max_age=3600)
    return resp

实机输出:

复制代码
GET /resp-custom -> 201
Headers: X-Custom-Header=Flask-Tutorial
Cookie: custom_cookie=value123; Expires=...; Max-Age=3600; Path=/
Body: Custom Response

7.4 JSON 响应

python 复制代码
@app.route('/resp-json')
def resp_json():
    data = {'status': 'success', 'data': [1, 2, 3], 'count': 3}
    return jsonify(data)  # 自动设置 Content-Type: application/json
复制代码
GET /resp-json -> 200
Content-Type: application/json
Body: {"count":3,"data":[1,2,3],"status":"success"}

7.5 重定向与错误

python 复制代码
@app.route('/resp-redirect')
def resp_redirect():
    return redirect(url_for('resp_json'))  # 302 重定向

@app.route('/resp-error/<int:code>')
def resp_error(code):
    abort(code)  # 触发 HTTP 错误
复制代码
GET /resp-redirect -> 302 (Location: /resp-json)
GET /resp-error/404 -> 404
GET /resp-error/500 -> 500

7.6 Request 对象属性总表

属性 类型 说明
request.method str HTTP 方法 (GET/POST/PUT/DELETE)
request.url str 完整 URL
request.base_url str URL(不含 query string)
request.path str URL 路径部分
request.args ImmutableMultiDict URL 查询参数
request.form ImmutableMultiDict POST 表单数据
request.json / request.get_json() dict JSON 请求体
request.data bytes 原始请求体
request.files MultiDict 上传文件
request.headers EnvironHeaders 请求头
request.cookies dict Cookie 字典
request.remote_addr str 客户端 IP
request.host str 主机名
request.content_type str Content-Type 头
request.content_length int Content-Length 头
request.is_json bool 是否 JSON 请求
request.is_secure bool 是否 HTTPS

8. Flask 视图函数

8.1 返回值类型

Flask 视图函数支持多种返回值类型:

返回类型 Flask 处理 状态码
str 直接作为响应体 200
dict / list 自动转 JSON 200
tuple(str, int) (响应体, 状态码) 自定义
tuple(str, int, dict) (响应体, 状态码, 头部) 自定义
Response 对象 直接返回 自定义
generator 流式响应 200
python 复制代码
@app.route('/str')
def return_str():
    return 'Plain String'              # -> 200

@app.route('/dict')
def return_dict():
    return {'key': 'value', 'num': 42}  # -> 200 JSON

@app.route('/tuple')
def return_tuple():
    return 'Tuple Response', 201, {'X-Header': 'Value'}  # -> 201

@app.route('/list')
def return_list():
    return ['item1', 'item2', 'item3']  # -> 200 JSON

实机输出:

复制代码
  GET  /               -> 200 Home Page
  GET  /str            -> 200 Plain String
  GET  /dict           -> 200 {"key":"value","num":42}
  GET  /tuple          -> 201 Tuple Response
  GET  /list           -> 200 ["item1","item2","item3"]

8.2 基于类的视图

View(通用类视图)
python 复制代码
from flask.views import View

class HelloWorldView(View):
    def dispatch_request(self):
        return 'Hello from Class-Based View!'

app.add_url_rule('/cbv', view_func=HelloWorldView.as_view('helloworld'))
MethodView(RESTful 风格)
python 复制代码
from flask.views import MethodView

class UserAPI(MethodView):
    def get(self):
        return jsonify({'action': 'GET', 'message': 'List users'})

    def post(self):
        return jsonify({'action': 'POST', 'message': 'Create user'})

app.add_url_rule('/api/users', view_func=UserAPI.as_view('users'))

实机输出:

复制代码
  GET  /cbv            -> 200 Hello from Class-Based View!
  GET  /api/users      -> 200 {"action":"GET","message":"List users"}
  POST /api/users      -> 200 {"action":"POST","message":"Create user"}

9. Flask 模板渲染

9.1 Jinja2 基础语法

python 复制代码
from jinja2 import Template

# 变量替换
tpl = Template('Hello {{ name }}!')
tpl.render(name='Flask')  # -> 'Hello Flask!'

# for 循环
tpl2 = Template('{% for item in items %}{{ item }} {% endfor %}')
tpl2.render(items=['a', 'b', 'c'])  # -> 'a b c '

# if/elif/else
tpl3 = Template('{% if score >= 90 %}A{% elif score >= 80 %}B{% else %}C{% endif %}')
tpl3.render(score=90)  # -> 'A'
tpl3.render(score=85)  # -> 'B'
tpl3.render(score=60)  # -> 'C'

9.2 render_template_string

python 复制代码
from flask import render_template_string

@app.route('/tpl/<name>')
def tpl_demo(name):
    return render_template_string(
        '<h1>Hello {{ name|upper }}!</h1>\n'
        '<p>Length: {{ name|length }}</p>\n'
        '<p>Reversed: {{ name|reverse }}</p>',
        name=name
    )

# 自定义过滤器
@app.template_filter('reverse')
def reverse_filter(s):
    return s[::-1]

实机输出:

html 复制代码
<h1>Hello FLASK!</h1>
<p>Length: 5</p>
<p>Reversed: ksalF</p>

9.3 循环与 loop 对象

python 复制代码
@app.route('/tpl-loop')
def tpl_loop():
    return render_template_string(
        '<ul>\n'
        '{% for item in items %}\n'
        '  <li>{{ loop.index }}: {{ item }}</li>\n'
        '{% endfor %}\n'
        '</ul>',
        items=['Python', 'Flask', 'Jinja2', 'Werkzeug']
    )

实机输出:

html 复制代码
<ul>
  <li>1: Python</li>
  <li>2: Flask</li>
  <li>3: Jinja2</li>
  <li>4: Werkzeug</li>
</ul>

9.4 macro 宏

python 复制代码
@app.route('/tpl-macro')
def tpl_macro():
    return render_template_string(
        '{% macro greet(name, greeting="Hello") %}\n'
        '{{ greeting }}, {{ name }}!\n'
        '{% endmacro %}\n'
        '{{ greet("Alice") }}{{ greet("Bob", "Hi") }}'
    )

实机输出:

复制代码
Hello, Alice!
Hi, Bob!

9.5 模板继承

python 复制代码
from jinja2 import Environment, DictLoader

base_tpl = '''<html><head>
<title>{% block title %}Default{% endblock %}</title>
</head><body>{% block content %}{% endblock %}</body></html>'''

child_tpl = '''{% extends 'base.html' %}
{% block title %}Child Page{% endblock %}
{% block content %}<h1>Content from Child</h1>{% endblock %}'''

env = Environment(loader=DictLoader({'base.html': base_tpl, 'child.html': child_tpl}))
result = env.get_template('child.html').render()

实机输出:

html 复制代码
<html><head><title>Child Page</title></head>
<body><h1>Content from Child</h1></body></html>

9.6 Jinja2 内置过滤器(54 个)

序号 过滤器 说明
1 abs 绝对值
2 capitalize 首字母大写
3 default / d 默认值
4 escape / e HTML 转义
5 filesizeformat 文件大小格式化
6 first / last 第一个/最后一个元素
7 float / int 类型转换
8 join 列表拼接为字符串
9 length / count 长度
10 lower / upper 大小写转换
11 replace 字符串替换
12 reverse 反转
13 round 四舍五入
14 safe 标记为安全(不转义)
15 sort 排序
16 striptags 去除 HTML 标签
17 title 每个单词首字母大写
18 trim 去除首尾空白
19 truncate 截断
20 tojson 转 JSON
21 unique 去重
22 wordcount 单词数
... ... 共 54 个

10. Flask 静态文件

10.1 默认静态文件配置

python 复制代码
app = Flask(__name__)
# 默认: static_folder='static', static_url_path='/static'

10.2 自定义静态文件目录

python 复制代码
app = Flask(__name__,
    static_folder='/path/to/static',
    static_url_path='/static'
)

10.3 实机测试

python 复制代码
# 创建静态文件
with open('/tmp/flask_static/style.css', 'w') as f:
    f.write('body { color: #333; }\nh1 { color: #0066cc; }')
with open('/tmp/flask_static/app.js', 'w') as f:
    f.write('console.log("Flask static file");')

实机输出:

复制代码
GET /static/style.css -> 200
Content-Type: text/css; charset=utf-8
Body: body { color: #333; }
h1 { color: #0066cc; }

GET /static/app.js -> 200
Content-Type: text/javascript; charset=utf-8
Body: console.log("Flask static file");

GET /static/nonexistent.js -> 404

10.4 模板中引用静态文件

html 复制代码
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<img src="{{ url_for('static', filename='images/logo.png') }}">

11.1 Session 操作

python 复制代码
from flask import session

app = Flask(__name__)
app.secret_key = 'session-test-key-12345'  # Session 必须设置 secret_key

@app.route('/set-session/<value>')
def set_session(value):
    session['data'] = value
    session['count'] = session.get('count', 0) + 1
    return f'Session set: data={value}, count={session["count"]}'

@app.route('/get-session')
def get_session():
    return jsonify({
        'data': session.get('data', '(empty)'),
        'count': session.get('count', 0),
        'all_keys': list(session.keys()),
    })

@app.route('/clear-session')
def clear_session():
    session.clear()
    return 'Session cleared'

实机输出:

复制代码
--- Session 测试 ---
  Set session -> Session set: data=hello, count=1
  Get session -> {"all_keys":["count","data"],"count":1,"data":"hello"}

  Set session again -> Session set: data=world, count=2
  Get session -> {"all_keys":["count","data"],"count":2,"data":"world"}

  Clear session -> Session cleared
  Get session after clear -> {"all_keys":[],"count":0,"data":"(empty)"}
python 复制代码
from flask import make_response

@app.route('/set-cookie')
def set_cookie():
    resp = make_response('Cookie set')
    resp.set_cookie('user', 'alice', max_age=3600, httponly=True, samesite='Lax')
    resp.set_cookie('theme', 'dark', max_age=86400)
    return resp

@app.route('/get-cookie')
def get_cookie():
    return jsonify({
        'user': request.cookies.get('user', '(none)'),
        'theme': request.cookies.get('theme', '(none)'),
        'all_cookies': dict(request.cookies),
    })

@app.route('/del-cookie')
def del_cookie():
    resp = make_response('Cookie deleted')
    resp.delete_cookie('user')
    return resp

实机输出:

复制代码
--- Cookie 测试 ---
  Set cookie -> 200
  Set-Cookie headers:
    user=alice; Expires=...; Max-Age=3600; HttpOnly; Path=/; SameSite=Lax
    theme=dark; Expires=...; Max-Age=86400; Path=/

  Get cookie -> {"all_cookies":{"theme":"dark","user":"alice"},
                 "theme":"dark","user":"alice"}

  Delete cookie -> Cookie deleted
  Get cookie after delete -> {"all_cookies":{"theme":"dark"},
                               "theme":"dark","user":"(none)"}
参数 类型 说明
key str Cookie 名称
value str Cookie 值
max_age int 过期时间(秒)
expires datetime 过期时间点
path str 有效路径(默认 /
domain str 有效域名
secure bool 仅 HTTPS 传输
httponly bool 禁止 JavaScript 访问
samesite str 跨站策略(Lax/Strict/None)

12. Flask 表单处理

12.1 安装依赖

bash 复制代码
pip3 install --break-system-packages flask-wtf email_validator

12.2 定义表单

python 复制代码
from flask_wtf import FlaskForm
from wtforms import (StringField, IntegerField, PasswordField,
                     SubmitField, SelectField, BooleanField, TextAreaField)
from wtforms.validators import DataRequired, Length, Email, NumberRange, EqualTo

class RegistrationForm(FlaskForm):
    username = StringField('用户名', validators=[DataRequired(), Length(min=3, max=20)])
    email = StringField('邮箱', validators=[DataRequired(), Email()])
    password = PasswordField('密码', validators=[DataRequired(), Length(min=6)])
    confirm = PasswordField('确认密码', validators=[DataRequired(), EqualTo('password')])
    age = IntegerField('年龄', validators=[NumberRange(min=1, max=150)])
    gender = SelectField('性别', choices=[('M', '男'), ('F', '女'), ('O', '其他')])
    agree = BooleanField('同意条款', validators=[DataRequired()])
    bio = TextAreaField('简介')
    submit = SubmitField('注册')

12.3 表单验证成功

python 复制代码
@app7.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        return jsonify({
            'status': 'success',
            'data': {
                'username': form.username.data,
                'email': form.email.data,
                'age': form.age.data,
                'gender': form.gender.data,
                'bio': form.bio.data,
            }
        })
    else:
        return jsonify({
            'status': 'error',
            'errors': {field: [str(e) for e in errors]
                      for field, errors in form.errors.items()}
        }), 400

实机输出(验证成功):

json 复制代码
{
  "status": "success",
  "data": {
    "age": 25,
    "bio": "Hello, I am Alice",
    "email": "alice@example.com",
    "gender": "F",
    "username": "alice"
  }
}

12.4 表单验证失败

实机输出(验证失败):

复制代码
Status: 400
    age: ['Number must be between 1 and 150.']
    agree: ['This field is required.']
    confirm: ['Field must be equal to password.']
    email: ['Invalid email address.']
    password: ['Field must be at least 6 characters long.']
    username: ['Field must be between 3 and 20 characters long.']

12.5 WTForms 字段类型

字段类型 说明 HTML 控件
StringField 文本输入 <input type="text">
PasswordField 密码输入 <input type="password">
IntegerField 整数输入 <input type="number">
FloatField 浮点输入 <input type="number">
BooleanField 复选框 <input type="checkbox">
SelectField 下拉选择 <select>
TextAreaField 多行文本 <textarea>
SubmitField 提交按钮 <input type="submit">
FileField 文件上传 <input type="file">
DateField 日期 <input type="date">
HiddenField 隐藏字段 <input type="hidden">

12.6 WTForms 验证器

验证器 说明
DataRequired 必填
Length(min, max) 长度范围
Email() 邮箱格式(需 email_validator)
NumberRange(min, max) 数值范围
EqualTo(field) 等于另一字段
URL() URL 格式
Regexp(pattern) 正则匹配
Optional() 可选(空值跳过验证)
InputRequired 输入必填(与 DataRequired 不同)
AnyOf(values) 值在列表中
NoneOf(values) 值不在列表中

进阶篇

13. Flask 数据库操作

13.1 SQLAlchemy 配置

python 复制代码
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

13.2 定义模型

python 复制代码
class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    age = db.Column(db.Integer, default=0)
    created_at = db.Column(db.DateTime, server_default=db.func.now())

    # 一对多关系
    posts = db.relationship('Post', backref='author', lazy=True)

class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, default='')
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)

13.3 常用列类型

类型 Python 类型 SQL 类型 说明
Integer int INTEGER 整数
String(n) str VARCHAR(n) 定长字符串
Text str TEXT 长文本
Float float FLOAT 浮点数
Boolean bool BOOLEAN 布尔
DateTime datetime DATETIME 日期时间
Date date DATE 日期
Time time TIME 时间
Numeric Decimal NUMERIC 高精度小数
LargeBinary bytes BLOB 二进制
JSON dict/list JSON JSON 数据

13.4 常用列参数

参数 说明
primary_key=True 主键
unique=True 唯一约束
nullable=False 不允许 NULL
default=value 默认值
server_default=text 数据库默认值
index=True 创建索引
onupdate=func 更新时触发

13.5 CRUD 操作

Create(插入)
python 复制代码
# 单条插入
user1 = User(username='alice', email='alice@example.com', age=25)
db.session.add(user1)
db.session.commit()

# 批量插入
users = [User(username=f'user{i}', email=f'user{i}@test.com', age=20+i) for i in range(4, 11)]
db.session.add_all(users)
db.session.commit()

实机输出:

复制代码
插入 3 个用户: alice(1), bob(2), charlie(3)
批量插入 7 个用户: user4~user10
Read(查询)
python 复制代码
# 查询所有
all_users = User.query.all()

# 按主键查询(SQLAlchemy 2.0 推荐用 db.session.get)
u1 = db.session.get(User, 1)

# filter_by (简单等值查询)
alice = User.query.filter_by(username='alice').first()

# filter (更灵活的条件查询)
adults = User.query.filter(User.age >= 25).all()

# 排序 + 限制
top3 = User.query.order_by(User.age.desc()).limit(3).all()

# like 查询
users = User.query.filter(User.username.like('user%')).all()

# in_ 查询
users = User.query.filter(User.username.in_(['alice', 'bob'])).all()

实机输出:

复制代码
User.query.all(): 10 条记录
filter(User.age >= 25): 9 条
  1: alice (age=25)
  2: bob (age=30)
  3: charlie (age=28)
  ...
order_by(age.desc()).limit(3):
  2: bob (age=30)
  10: user10 (age=30)
  9: user9 (age=29)

count: 10, avg_age: 27.2, max_age: 30, min_age: 24
filter(username.like('user%')): 7 条
filter(username.in_(['alice','bob','charlie'])): 3 条
聚合查询
python 复制代码
from sqlalchemy import func

count = db.session.query(func.count(User.id)).scalar()
avg_age = db.session.query(func.avg(User.age)).scalar()
max_age = db.session.query(func.max(User.age)).scalar()
min_age = db.session.query(func.min(User.age)).scalar()
复制代码
count: 10, avg_age: 27.2, max_age: 30, min_age: 24
Update(更新)
python 复制代码
# 单条更新
alice = User.query.filter_by(username='alice').first()
alice.age = 26
alice.email = 'alice_new@example.com'
db.session.commit()

# 批量更新
updated = User.query.filter(User.age < 25).update({'age': 25})
db.session.commit()
复制代码
更新前: alice age=25
更新后: alice age=26 email=alice_new@example.com
批量更新 age<25 -> 25: 影响 1 行
Delete(删除)
python 复制代码
charlie = User.query.filter_by(username='charlie').first()
db.session.delete(charlie)
db.session.commit()
复制代码
删除: <User charlie>
删除后剩余: 9 条

13.6 关联查询

python 复制代码
# 一对多:通过 relationship 访问
alice = db.session.get(User, 1)
for p in alice.posts:
    print(f"[{p.id}] {p.title}")

# JOIN 查询
results = db.session.query(User.username, Post.title).join(Post).all()

实机输出:

复制代码
alice 的文章:
  [1] Flask Tutorial
  [2] Python Tips
bob 的文章:
  [3] Web Dev

JOIN 查询 (User.username, Post.title):
  alice: Flask Tutorial
  alice: Python Tips
  bob: Web Dev

13.7 分页

python 复制代码
page = User.query.paginate(page=1, per_page=5, error_out=False)

print(f"第1页: {len(page.items)} 条")
print(f"总页数: {page.pages}, 总记录: {page.total}")
print(f"has_next: {page.has_next}, has_prev: {page.has_prev}")

实机输出:

复制代码
第1页 (每页5条): 5 条
  1: alice
  2: bob
  4: user4
  5: user5
  6: user6
总页数: 2, 总记录: 9
has_next: True, has_prev: False

第2页: 4 条
  7: user7
  8: user8
  9: user9
  10: user10

13.8 查询方法速查表

方法 说明 返回
.all() 返回所有结果 list
.first() 返回第一条 Model / None
.get(id) 按主键查询(已弃用,用 db.session.get Model / None
.one() 返回唯一一条(无结果抛异常) Model
.one_or_none() 返回唯一一条或 None Model / None
.count() 计数 int
.filter(*conditions) 条件过滤 Query
.filter_by(**kwargs) 等值过滤 Query
.order_by(column) 排序 Query
.limit(n) 限制数量 Query
.offset(n) 跳过数量 Query
.paginate(page, per_page) 分页 Pagination
.join(Target) 表连接 Query
.distinct() 去重 Query
.group_by(column) 分组 Query
.having(condition) 分组后过滤 Query

14. Flask 蓝图(Blueprint)

14.1 蓝图概念

蓝图(Blueprint)用于将 Flask 应用拆分为多个模块,适合大型应用的组织。

复制代码
Flask App (主应用)
  |
  +-- Blueprint: api (URL前缀 /api)
  |     |-- /api/users
  |     |-- /api/posts
  |     +-- /api/health
  |
  +-- Blueprint: admin (URL前缀 /admin)
  |     |-- /admin/
  |     |-- /admin/users
  |     +-- /admin/settings
  |
  +-- Blueprint: auth (URL前缀 /auth)
        |-- /auth/login
        +-- /auth/register

14.2 创建和注册蓝图

python 复制代码
from flask import Blueprint, jsonify

# 创建蓝图
api_bp = Blueprint('api', __name__, url_prefix='/api')
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')

@api_bp.route('/users')
def api_users():
    return jsonify({'users': ['alice', 'bob', 'charlie']})

@api_bp.route('/health')
def health():
    return jsonify({'status': 'ok', 'service': 'api'})

@admin_bp.route('/')
def admin_index():
    return 'Admin Dashboard'

# 注册蓝图到应用
app = Flask(__name__)
app.register_blueprint(api_bp)
app.register_blueprint(admin_bp)

14.3 实机测试

复制代码
--- API 蓝图 ---
  GET /api/users -> 200 {"users":["alice","bob","charlie"]}
  GET /api/posts -> 200 {"posts":[{"id":1,"title":"Hello"},{"id":2,"title":"World"}]}
  GET /api/health -> 200 {"service":"api","status":"ok"}

--- Admin 蓝图 ---
  GET /admin/ -> 200 Admin Dashboard
  GET /admin/users -> 200 Admin: User Management
  GET /admin/settings -> 200 Admin: Settings

14.4 蓝图 URL Map

复制代码
  /api/users                GET      -> api.api_users
  /api/posts                GET      -> api.api_posts
  /api/health               GET      -> api.health
  /admin/                   GET      -> admin.admin_index
  /admin/users              GET      -> admin.admin_users
  /admin/settings           GET      -> admin.admin_settings

14.5 蓝图参数

参数 说明
name 蓝图名称(用于 url_for 前缀)
import_name 导入名(通常用 __name__
url_prefix URL 前缀
subdomain 子域名
template_folder 模板目录
static_folder 静态文件目录
url_defaults 默认 URL 值

15. Flask 错误处理

15.1 自定义错误页面

python 复制代码
@app.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Not Found', 'code': 404,
                    'message': 'The requested URL was not found.'}), 404

@app.errorhandler(500)
def server_error(error):
    return jsonify({'error': 'Internal Server Error', 'code': 500,
                    'message': 'Something went wrong.'}), 500

@app.errorhandler(403)
def forbidden(error):
    return jsonify({'error': 'Forbidden', 'code': 403,
                    'message': 'You do not have permission.'}), 403

15.2 自定义异常

python 复制代码
class APIError(Exception):
    def __init__(self, message, status_code=400, payload=None):
        super().__init__()
        self.message = message
        self.status_code = status_code
        self.payload = payload

    def to_dict(self):
        rv = dict(self.payload or ())
        rv['error'] = self.message
        rv['code'] = self.status_code
        return rv

@app.errorhandler(APIError)
def handle_api_error(error):
    response = jsonify(error.to_dict())
    response.status_code = error.status_code
    return response

@app.route('/users/<int:user_id>')
def get_user(user_id):
    if user_id < 1:
        raise APIError('user_id must be positive', status_code=400)
    if user_id > 100:
        raise APIError('User not found', status_code=404)
    return jsonify({'user_id': user_id, 'name': f'User{user_id}'})

15.3 实机测试

复制代码
  GET /trigger-404              -> 404 {"code":404,"error":"Not Found",...}
  GET /trigger-403              -> 403 {"code":403,"error":"Forbidden",...}
  GET /trigger-api-error        -> 422 {"code":422,"error":"Invalid parameter..."}
  GET /users/0                  -> 400 {"code":400,"error":"user_id must be positive"}
  GET /users/200                -> 404 {"code":404,"error":"User not found"}
  GET /users/5                  -> 200 {"name":"User5","user_id":5}
  GET /nonexistent              -> 404 {"code":404,"error":"Not Found",...}

15.4 常用 HTTP 错误码

状态码 名称 说明
400 Bad Request 请求参数错误
401 Unauthorized 未认证
403 Forbidden 无权限
404 Not Found 资源不存在
405 Method Not Allowed 方法不允许
409 Conflict 冲突
422 Unprocessable Entity 验证失败
429 Too Many Requests 请求过多
500 Internal Server Error 服务器内部错误
502 Bad Gateway 网关错误
503 Service Unavailable 服务不可用

16. Flask 中间件和扩展

16.1 请求钩子

Flask 提供了 4 种请求钩子(类似中间件):

钩子 执行时机 用途
before_request 每次请求前 认证检查、初始化变量
before_first_request 第一个请求前(Flask 3.x 已移除) 初始化操作
after_request 每次请求后(无异常时) 添加响应头、记录日志
teardown_request 请求结束(无论是否异常) 清理资源
teardown_appcontext 应用上下文结束 清理应用级资源
python 复制代码
import time
from flask import g

@app.before_request
def before_req():
    g.start_time = time.time()
    g.request_id = f'req-{int(time.time() * 1000)}'

@app.after_request
def after_req(response):
    duration = time.time() - g.start_time
    response.headers['X-Response-Time'] = f'{duration:.4f}s'
    response.headers['X-Request-ID'] = g.request_id
    return response

@app.teardown_request
def teardown_req(exception):
    if exception:
        print(f"  [teardown_request] exception={exception}")

16.2 实机输出

复制代码
--- 请求 1: GET / ---
  [before_request] request_id=req-1782540092480, path=/
  [after_request] status=200, time=0.0000s
  [teardown_request] clean up
  Response: 200 Home with middleware
  X-Response-Time: 0.0000s
  X-Request-ID: req-1782540092480

--- 请求 2: GET /slow ---
  [before_request] request_id=req-1782540092481, path=/slow
  [after_request] status=200, time=0.1001s
  [teardown_request] clean up
  Response: 200 Slow response
  X-Response-Time: 0.1001s

16.3 WSGI 中间件

python 复制代码
class TimingMiddleware:
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        start = time.time()

        def custom_start_response(status, headers, exc_info=None):
            duration = time.time() - start
            headers.append(('X-Timing', f'{duration:.6f}'))
            return start_response(status, headers, exc_info)

        return self.app(environ, custom_start_response)

app.wsgi_app = TimingMiddleware(app.wsgi_app)
复制代码
GET / -> 200
X-Timing header: 0.000085

16.4 常用 Flask 扩展

扩展 说明 安装状态
Flask-SQLAlchemy 数据库 ORM 已安装
Flask-Migrate 数据库迁移 已安装
Flask-Login 用户认证 已安装
Flask-WTF 表单处理 已安装
Flask-Mail 邮件发送 未安装
Flask-Caching 缓存 未安装
Flask-RESTful REST API 未安装
Flask-CORS 跨域资源共享 未安装
Flask-Limiter 速率限制 未安装
Flask-Admin 后台管理 未安装

17. Flask 配置管理

17.1 配置方式对比

方式 命令 适用场景
直接配置 app.config['KEY'] = value 简单场景
from_object app.config.from_object(ConfigClass) 多环境配置
from_pyfile app.config.from_pyfile('config.py') 独立配置文件
from_envvar app.config.from_envvar('APP_SETTINGS') 环境变量指定文件
from_mapping app.config.from_mapping(...) 字典配置
from_json app.config.from_json('config.json') JSON 配置

17.2 多环境配置

python 复制代码
class DevelopmentConfig:
    DEBUG = True
    SECRET_KEY = 'dev-secret'
    DATABASE_URI = 'sqlite:///dev.db'
    TESTING = False

class ProductionConfig:
    DEBUG = False
    SECRET_KEY = 'prod-secret-key-xxx'
    DATABASE_URI = 'postgresql://user:pass@localhost/prod'
    TESTING = False

class TestingConfig:
    DEBUG = False
    SECRET_KEY = 'test-secret'
    DATABASE_URI = 'sqlite:///:memory:'
    TESTING = True

# 根据环境变量选择配置
config_map = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'testing': TestingConfig,
}

env = os.environ.get('FLASK_ENV', 'development')
app.config.from_object(config_map[env])

实机输出:

复制代码
[Development]
  DEBUG: True
  SECRET_KEY: dev-secret
  DATABASE_URI: sqlite:///dev.db
  TESTING: False

[Production]
  DEBUG: False
  SECRET_KEY: prod-secret-key-xxx
  DATABASE_URI: postgresql://user:pass@localhost/prod

[Testing]
  DEBUG: False
  TESTING: True
  DATABASE_URI: sqlite:///:memory:

17.3 from_mapping(字典配置)

python 复制代码
app.config.from_mapping(
    CUSTOM_KEY='custom-value',
    API_VERSION='v1',
    MAX_ITEMS=100,
)

17.4 from_pyfile(文件配置)

python 复制代码
# config.py
DEBUG = False
SECRET_KEY = "from-file-key"
DB_HOST = "localhost"
DB_PORT = 5432

# 加载
app.config.from_pyfile('config.py')

18. Flask 请求生命周期

18.1 生命周期流程图

复制代码
  Client Request
       |
       v
  +-------------------+
  | WSGI Server       |  (Gunicorn / uWSGI / Werkzeug)
  +-------------------+
       |
       v
  +-------------------+
  | Flask.wsgi_app()  |  进入 Flask 应用
  +-------------------+
       |
       v
  +-------------------+
  | RequestContext    |  创建请求上下文
  +-------------------+
       |
       v
  +-------------------+
  | before_request    |  1. 请求前钩子 (可多个)
  +-------------------+
       |
       v
  +-------------------+
  | URL Routing       |  2. URL 匹配 -> 视图函数
  +-------------------+
       |
       v
  +-------------------+
  | View Function     |  3. 执行视图函数
  +-------------------+
       |
       v
  +-------------------+
  | Response Object   |  4. 构建响应
  +-------------------+
       |
       v
  +-------------------+
  | after_request     |  5. 响应后钩子 (可多个)
  +-------------------+
       |
       v
  +-------------------+
  | teardown_request  |  6. 请求结束清理
  +-------------------+
       |
       v
  +-------------------+
  |teardown_appcontext| 7. 应用上下文清理
  +-------------------+
       |
       v
  Client Response

18.2 实机验证

python 复制代码
lifecycle_log = []

@app.before_request
def lifecycle_before():
    lifecycle_log.append(f'1. before_request: {request.method} {request.path}')

@app.url_value_preprocessor
def lifecycle_url_value(endpoint, values):
    lifecycle_log.append(f'2. url_value_preprocessor: endpoint={endpoint}')

@app.route('/')
def lifecycle_home():
    lifecycle_log.append('3. view_function: processing request')
    return 'OK'

@app.after_request
def lifecycle_after(response):
    lifecycle_log.append(f'4. after_request: status={response.status_code}')
    return response

@app.teardown_request
def lifecycle_teardown(exception):
    lifecycle_log.append(f'5. teardown_request: exception={exception}')

@app.teardown_appcontext
def lifecycle_teardown_ctx(exception):
    lifecycle_log.append(f'6. teardown_appcontext: exception={exception}')

实机输出:

复制代码
请求生命周期执行顺序:
  2. url_value_preprocessor: endpoint=lifecycle_home
  1. before_request: GET /
  3. view_function: processing request
  4. after_request: status=200
  5. teardown_request: exception=None
  6. teardown_appcontext: exception=None

实战篇

19. 实战项目:Flask Web 博客系统

19.1 项目架构

复制代码
+--------------------------------------------------+
|              Flask Blog System                    |
|                                                  |
|  +-------------------+                           |
|  |    Routes         |                           |
|  |  / (首页)         |                           |
|  |  /posts (列表)    |                           |
|  |  /post/<id> (详情)|                           |
|  |  /login (登录)    |                           |
|  |  /register (注册) |                           |
|  |  /post/new (写文章)|                           |
|  |  /favorites (收藏)|                           |
|  |  /admin (后台)    |                           |
|  +-------------------+                           |
|          |                                       |
|  +-------v-------+       +-------------------+  |
|  |   Models      |       |   Forms           |  |
|  |  User         |       |  RegistrationForm |  |
|  |  Post         |       |  LoginForm        |  |
|  |  favorites    |       |  PostForm         |  |
|  |  (M2M关联)    |       |                   |  |
|  +-------+-------+       +-------------------+  |
|          |                                       |
|  +-------v-------+       +-------------------+  |
|  |  SQLAlchemy   |       |  Flask-Login      |  |
|  |  (SQLite)     |       |  (用户认证)        |  |
|  +---------------+       +-------------------+  |
|                                                  |
|  +-------------------+   +-------------------+  |
|  | Jinja2 Templates  |   | Flask-WTF         |  |
|  | base.html         |   | (CSRF保护)        |  |
|  | index.html        |   |                   |  |
|  | posts.html        |   |                   |  |
|  | post_detail.html  |   |                   |  |
|  | login.html        |   |                   |  |
|  | admin.html        |   |                   |  |
|  +-------------------+   +-------------------+  |
+--------------------------------------------------+

19.2 数据模型

python 复制代码
# 用户收藏关联表(多对多)
favorites = db.Table('favorites',
    db.Column('user_id', db.Integer, db.ForeignKey('users.id'), primary_key=True),
    db.Column('post_id', db.Integer, db.ForeignKey('posts.id'), primary_key=True)
)

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password = db.Column(db.String(200), nullable=False)
    is_admin = db.Column(db.Boolean, default=False)
    created_at = db.Column(db.DateTime, server_default=db.func.now())

    posts = db.relationship('Post', backref='author', lazy=True)
    favorite_posts = db.relationship('Post', secondary=favorites, backref='favorited_by')

class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    category = db.Column(db.String(50), default='未分类')
    views = db.Column(db.Integer, default=0)
    is_published = db.Column(db.Boolean, default=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    created_at = db.Column(db.DateTime, server_default=db.func.now())
    updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now())

19.3 表单定义

python 复制代码
class RegistrationForm(FlaskForm):
    username = StringField('用户名', validators=[DataRequired(), Length(min=3, max=20)])
    email = StringField('邮箱', validators=[DataRequired(), Email()])
    password = PasswordField('密码', validators=[DataRequired(), Length(min=6)])
    confirm = PasswordField('确认密码', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('注册')

class LoginForm(FlaskForm):
    username = StringField('用户名', validators=[DataRequired()])
    password = PasswordField('密码', validators=[DataRequired()])
    remember = BooleanField('记住我')
    submit = SubmitField('登录')

class PostForm(FlaskForm):
    title = StringField('标题', validators=[DataRequired(), Length(max=200)])
    content = TextAreaField('内容', validators=[DataRequired()])
    category = StringField('分类', validators=[DataRequired()])
    submit = SubmitField('发布')

19.4 视图函数

python 复制代码
# 首页
@app.route('/')
def index():
    total_posts = Post.query.count()
    total_users = User.query.count()
    latest_posts = Post.query.filter_by(is_published=True)\
        .order_by(Post.created_at.desc()).limit(3).all()
    return render_template_string(INDEX_TEMPLATE,
        total_posts=total_posts, total_users=total_users,
        latest_posts=latest_posts)

# 文章列表(分页 + 搜索 + 分类过滤)
@app.route('/posts')
def posts_list():
    page = request.args.get('page', 1, type=int)
    per_page = app.config['POSTS_PER_PAGE']
    query = request.args.get('q', '')
    selected_category = request.args.get('category', '')

    q = Post.query.filter_by(is_published=True)
    if query:
        q = q.filter(Post.title.contains(query) | Post.content.contains(query))
    if selected_category:
        q = q.filter_by(category=selected_category)

    pagination = q.order_by(Post.created_at.desc())\
        .paginate(page=page, per_page=per_page, error_out=False)
    categories = [c[0] for c in db.session.query(Post.category).distinct().all()]

    return render_template_string(POSTS_LIST_TEMPLATE,
        posts=pagination.items, pagination=pagination,
        total=pagination.total, query=query,
        categories=categories, selected_category=selected_category)

# 文章详情(自动增加浏览量)
@app.route('/post/<int:post_id>')
def post_detail(post_id):
    post = db.session.get(Post, post_id) or abort(404)
    post.views += 1
    db.session.commit()
    return render_template_string(POST_DETAIL_TEMPLATE, post=post)

# 用户登录
@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user and user.password == form.password.data:
            login_user(user, remember=form.remember.data)
            flash('登录成功!')
            return redirect(url_for('index'))
        flash('用户名或密码错误')
    return render_template_string(LOGIN_TEMPLATE, form=form)

# 写文章(需登录)
@app.route('/post/new', methods=['GET', 'POST'])
@login_required
def create_post():
    form = PostForm()
    if form.validate_on_submit():
        post = Post(
            title=form.title.data,
            content=form.content.data,
            category=form.category.data,
            author=current_user
        )
        db.session.add(post)
        db.session.commit()
        flash('文章发布成功!')
        return redirect(url_for('post_detail', post_id=post.id))
    return render_template_string(CREATE_POST_TEMPLATE, form=form)

# 收藏/取消收藏
@app.route('/post/<int:post_id>/favorite', methods=['POST'])
@login_required
def toggle_favorite(post_id):
    post = db.session.get(Post, post_id) or abort(404)
    if post in current_user.favorite_posts:
        current_user.favorite_posts.remove(post)
        flash('已取消收藏')
    else:
        current_user.favorite_posts.append(post)
        flash('已添加收藏')
    db.session.commit()
    return redirect(url_for('post_detail', post_id=post_id))

# 管理员后台
@app.route('/admin')
@login_required
def admin_dashboard():
    if not current_user.is_admin:
        abort(403)
    stats = {
        'users': User.query.count(),
        'posts': Post.query.count(),
        'published': Post.query.filter_by(is_published=True).count(),
        'total_views': db.session.query(db.func.sum(Post.views)).scalar() or 0,
    }
    return render_template_string(ADMIN_TEMPLATE, stats=stats,
        users=User.query.all(), all_posts=Post.query.all())

19.5 路由表

Endpoint Methods URL 说明 权限
index GET / 首页 公开
posts_list GET /posts 文章列表(搜索+分页) 公开
post_detail GET /post/<id> 文章详情 公开
login GET,POST /login 登录 公开
register GET,POST /register 注册 公开
logout GET /logout 退出 登录
create_post GET,POST /post/new 写文章 登录
toggle_favorite POST /post/<id>/favorite 收藏/取消 登录
favorites_list GET /favorites 我的收藏 登录
admin_dashboard GET /admin 后台管理 管理员

19.6 功能测试结果

首页
复制代码
GET / -> 200
包含 'Flask Blog 首页': True
包含 '总文章数: 9': True
包含 '最新文章': True
文章列表(分页)
复制代码
GET /posts -> 200
'共 9 篇文章': True
'第 1 / 3 页': True

GET /posts?page=2 -> 200
'第 2 / 3 页': True
关键词搜索
复制代码
GET /posts?q=Flask -> 200
包含 Flask 相关: True

GET /posts?category=数据库 -> 200
'SQLAlchemy' in html: True
文章详情
复制代码
GET /post/1 -> 200
'Flask 入门指南' in html: True
'作者: alice' in html: True
再次访问 /post/1 -> 200 (浏览量+1)
用户注册
复制代码
POST /register -> 302 (redirect to /login)
用户登录
复制代码
POST /login (alice) -> 302 (redirect to /)
首页显示 '退出 (alice)': True
写文章
复制代码
GET /post/new (已登录) -> 200
POST /post/new -> 302 (redirect)
收藏功能
复制代码
POST /post/1/favorite -> 302 (收藏)
GET /favorites -> 200
'Flask 入门指南' in favorites: True

POST /post/1/favorite again -> 302 (取消收藏)
'暂无收藏': True
未登录访问限制
复制代码
GET /post/new (未登录) -> 302 (redirect to login)
GET /favorites (未登录) -> 302 (redirect to login)
管理员后台
复制代码
GET /admin (admin) -> 200
'后台管理' in html: True
'总用户数: 4' in html: True
'总文章数: 10' in html: True

GET /admin (普通用户) -> 403 (Forbidden)
404 错误
复制代码
GET /post/999 -> 404

20. Flask CLI 命令与部署

20.1 Flask CLI 命令

命令 说明
flask run 启动开发服务器
flask run --host=0.0.0.0 监听所有网络接口
flask run --port=8080 指定端口
flask run --debug 调试模式(自动重载)
flask routes 列出所有路由
flask shell 进入交互式 Python Shell
flask db init 初始化数据库迁移
flask db migrate 生成迁移脚本
flask db upgrade 执行迁移
flask db downgrade 回滚迁移
flask --app myapp run 指定应用

20.2 Flask-Migrate 数据库迁移

bash 复制代码
# 1. 首次初始化
flask db init
# 创建 migrations/ 目录结构:
# migrations/
#   versions/       # 迁移脚本
#   alembic.ini     # Alembic 配置
#   env.py          # 环境配置
#   script.py.mako  # 迁移脚本模板

# 2. 修改模型后生成迁移
flask db migrate -m "add user table"

# 3. 应用迁移到数据库
flask db upgrade

# 4. 回滚到上一版本
flask db downgrade

# 5. 查看迁移历史
flask db history

# 6. 查看当前版本
flask db current

20.3 部署方式

方式 1:开发服务器(不推荐生产)
bash 复制代码
flask run --host=0.0.0.0 --port=5000 --debug
方式 2:Gunicorn(推荐)
bash 复制代码
pip install gunicorn

# 基本启动
gunicorn -w 4 -b 0.0.0.0:5000 "app:app"

# 生产级配置
gunicorn -w 4 \
  -b 0.0.0.0:5000 \
  --timeout 120 \
  --access-logfile - \
  --error-logfile - \
  "app:app"
参数 说明
-w 4 4 个 worker 进程
-b 0.0.0.0:5000 绑定地址和端口
--timeout 120 请求超时时间(秒)
--access-logfile - 输出访问日志到 stdout
--error-logfile - 输出错误日志到 stderr
--workers worker 数量(建议 2*CPU+1)
--threads 每个 worker 的线程数
--preload 预加载应用(节省内存)
方式 3:uWSGI
bash 复制代码
pip install uwsgi
uwsgi --http :5000 --wsgi-file app.py --callable app --processes 4
方式 4:Docker 部署
dockerfile 复制代码
# Dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]
bash 复制代码
# 构建镜像
docker build -t flask-blog .

# 运行容器
docker run -d -p 5000:5000 --name blog flask-blog

# 查看日志
docker logs -f blog

# 进入容器
docker exec -it blog bash
方式 5:Nginx + Gunicorn 反向代理
nginx 复制代码
# /etc/nginx/sites-enabled/flask_blog
server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 静态文件直接由 Nginx 处理
    location /static {
        alias /app/static;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # 上传文件大小限制
    client_max_body_size 16M;
}
bash 复制代码
# 启动 Gunicorn
gunicorn -w 4 -b 127.0.0.1:5000 "app:app" --daemon

# 重载 Nginx
nginx -s reload

20.4 生产部署架构图

复制代码
                    Internet
                       |
                       v
              +----------------+
              |   Load Balancer|  (华为云 ELB / Nginx)
              +----------------+
                       |
          +------------+------------+
          |            |            |
    +-----v---+  +-----v---+  +-----v---+
    | Nginx 1 |  | Nginx 2 |  | Nginx 3 |
    +-----+---+  +-----+---+  +-----+---+
          |            |            |
          +------------+------------+
                       |
          +------------+------------+
          |            |            |
    +-----v---+  +-----v---+  +-----v---+
    |Gunicorn |  |Gunicorn |  |Gunicorn |
    |Worker 1 |  |Worker 2 |  |Worker 3 |
    | (Flask) |  | (Flask) |  | (Flask) |
    +---------+  +---------+  +---------+
          |            |            |
          +------------+------------+
                       |
              +----------------+
              |   Database     |  (PostgreSQL / MySQL)
              +----------------+
                       |
              +----------------+
              |    Redis       |  (缓存 / Session)
              +----------------+

21. Flask 测试

21.1 测试框架

python 复制代码
import unittest

class BlogTestCase(unittest.TestCase):
    def setUp(self):
        app.config['TESTING'] = True
        app.config['WTF_CSRF_ENABLED'] = False
        self.client = app.test_client()

        with app.app_context():
            db.create_all()
            # 创建测试数据
            user = User(username='testuser', email='test@test.com', password='test123')
            admin = User(username='testadmin', email='admin@test.com',
                        password='admin123', is_admin=True)
            db.session.add_all([user, admin])
            post = Post(title='Test Post', content='Test content',
                       category='Test', author=user)
            db.session.add(post)
            db.session.commit()

    def tearDown(self):
        with app.app_context():
            db.session.remove()
            db.drop_all()

    def test_index(self):
        resp = self.client.get('/')
        self.assertEqual(resp.status_code, 200)

    def test_post_not_found(self):
        resp = self.client.get('/post/999')
        self.assertEqual(resp.status_code, 404)

    def test_login_required(self):
        resp = self.client.get('/post/new')
        self.assertEqual(resp.status_code, 302)
        self.assertIn('/login', resp.headers.get('Location', ''))

    def test_admin_access_denied(self):
        self.client.post('/login', data={'username': 'testuser', 'password': 'test123'})
        resp = self.client.get('/admin')
        self.assertEqual(resp.status_code, 403)

    def test_admin_access_granted(self):
        self.client.post('/login', data={'username': 'testadmin', 'password': 'admin123'})
        resp = self.client.get('/admin')
        self.assertEqual(resp.status_code, 200)

21.2 测试结果

复制代码
test_admin_access_denied ... ok
test_admin_access_granted ... ok
test_index ... ok
test_login ... ok
test_login_required ... ok
test_post_detail ... ok
test_post_not_found ... ok
test_posts_list ... ok
test_search ... ok

----------------------------------------------------------------------
Ran 9 tests in 0.238s

OK

测试结果: 9 个测试, 0 失败, 0 错误
测试状态: 全部通过 ✓

21.3 pytest 风格测试

python 复制代码
import pytest

@pytest.fixture
def client():
    app.config['TESTING'] = True
    with app.test_client() as client:
        with app.app_context():
            db.create_all()
        yield client
        with app.app_context():
            db.drop_all()

def test_index(client):
    resp = client.get('/')
    assert resp.status_code == 200

def test_post_not_found(client):
    resp = client.get('/post/999')
    assert resp.status_code == 404

踩坑记录

踩坑 1:Ubuntu 24.04 PEP 668 保护

现象

复制代码
error: externally-managed-environment
× This environment is externally managed

解决

bash 复制代码
pip3 install --break-system-packages flask
# 或使用虚拟环境
python3 -m venv venv && source venv/bin/activate

踩坑 2:blinker 版本冲突

现象

复制代码
ERROR: Cannot uninstall blinker 1.7.0, RECORD file not found.
Hint: The package was installed by debian.

解决

bash 复制代码
pip3 install --break-system-packages --ignore-installed blinker flask

踩坑 3:email_validator 缺失

现象

复制代码
Exception: Install 'email_validator' for email validation support.

解决

bash 复制代码
pip3 install --break-system-packages email_validator

踩坑 4:SQLAlchemy 2.0 Query.get() 弃用

现象

复制代码
LegacyAPIWarning: The Query.get() method is considered legacy as of
the 1.x series of SQLAlchemy and becomes a legacy construct in 2.0.
The method is now available as Session.get()

解决

python 复制代码
# 旧(弃用)
user = User.query.get(1)

# 新(推荐)
user = db.session.get(User, 1)

踩坑 5:Flask 3.x 移除 before_first_request

现象before_first_request 在 Flask 3.x 中不再存在

替代方案

python 复制代码
# 方式 1: 使用 before_request + 标志位
_initialized = False

@app.before_request
def init_once():
    global _initialized
    if not _initialized:
        # 初始化逻辑
        _initialized = True

# 方式 2: 在 create_app() 中直接初始化
def create_app():
    app = Flask(__name__)
    # 初始化逻辑
    return app

踩坑 6:Flask __version__ 属性弃用

现象

复制代码
DeprecationWarning: The '__version__' attribute is deprecated and will
be removed in Flask 3.2. Use feature detection or
'importlib.metadata.version("flask")' instead.

解决

python 复制代码
import importlib.metadata as meta
version = meta.version('flask')

踩坑 7:Werkzeug 3.x 转换器名称变更

现象

复制代码
ImportError: cannot import name 'StringConverter' from 'werkzeug.routing'

解决

python 复制代码
# 旧: from werkzeug.routing import StringConverter
# 新: from werkzeug.routing import UnicodeConverter

附录 A:Flask 应用对象 API 速查

属性/方法 说明
app.config 配置字典
app.url_map URL 映射表
app.debug 调试模式
app.testing 测试模式
app.secret_key 密钥
app.static_folder 静态文件目录
app.template_folder 模板目录
app.logger 日志器
app.test_client() 测试客户端
app.app_context() 应用上下文
app.request_context() 请求上下文
app.run() 启动服务器
app.route() 路由装饰器
app.errorhandler() 错误处理器
app.before_request 请求前钩子
app.after_request 请求后钩子
app.register_blueprint() 注册蓝图
app.shell_context_processor Shell 上下文

附录 B:requirements.txt

复制代码
flask==3.1.3
flask-sqlalchemy==3.1.1
flask-migrate==4.1.0
flask-login==0.6.3
flask-wtf==1.3.0
email_validator==2.3.0
gunicorn==23.0.0

附录 C:版本信息

组件 版本
Python 3.12.3
Flask 3.1.3
Werkzeug 3.1.8
Jinja2 3.1.6
Flask-SQLAlchemy 3.1.1
SQLAlchemy 2.0.51
Flask-Migrate 4.1.0
Flask-Login 0.6.3
Flask-WTF 1.3.0
Click 8.4.2
Itsdangerous 2.2.0
Blinker 1.9.0
WTForms 3.2.2
email_validator 2.3.0
OS Ubuntu 24.04.4 LTS
Kernel Linux ecs-60a4-0001

全文完

所有代码均在华为云 FlexusX ecs-60a4-0001(Ubuntu 24.04 / Python 3.12.3 / Flask 3.1.3)上真实执行验证。

9 个单元测试全部通过,功能覆盖完整。