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-19Flask 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 对比

对比项FlaskDjango
设计理念微框架,灵活组合全栈框架,开箱即用
ORM无内置(常用 SQLAlchemy)内置 Django ORM
模板Jinja2Django 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-packagespip3 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

pip3 install --break-system-packages --ignore-installed blinker \
    flask flask-sqlalchemy flask-migrate flask-login flask-wtf

2.4 安装 email_validator(Flask-WTF 依赖)

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 核心组件验证

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 应用

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() 模拟请求,无需启动真实服务器:

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 启动开发服务器

# 方式 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 应用对象属性

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 默认配置项

配置项默认值说明
DEBUGFalse调试模式(自动重载 + 错误页面)
TESTINGFalse测试模式(异常直接抛出)
SECRET_KEYNoneSession 加密密钥(生产必须设置
PERMANENT_SESSION_LIFETIME31 daysSession 持久化过期时间
MAX_CONTENT_LENGTHNone请求体最大字节数
PREFERRED_URL_SCHEMEhttpURL 生成的默认协议
MAX_COOKIE_SIZE4093Cookie 最大大小
TEMPLATES_AUTO_RELOADNone模板自动重载
JSON_AS_ASCIITrueJSON 输出是否为 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

from flask import Flask

app = Flask(__name__)

from app import views

config.py

DEBUG = True
SECRET_KEY = 'dev-secret-key'
SQLALCHEMY_DATABASE_URI = 'sqlite:///app.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False

run.py

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 基本路由

@app.route('/')
def index():
    return 'Index Page'

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

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

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

6.3 多规则路由

@app.route('/hello')
@app.route('/hi')
def hello_both():
    return 'Hello or Hi!'

6.4 HTTP 方法

@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 构建

@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 对象

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)

实机输出:

{
  "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 获取请求参数

# 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 对象

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 响应

@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 重定向与错误

@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.methodstrHTTP 方法 (GET/POST/PUT/DELETE)
request.urlstr完整 URL
request.base_urlstrURL(不含 query string)
request.pathstrURL 路径部分
request.argsImmutableMultiDictURL 查询参数
request.formImmutableMultiDictPOST 表单数据
request.json / request.get_json()dictJSON 请求体
request.databytes原始请求体
request.filesMultiDict上传文件
request.headersEnvironHeaders请求头
request.cookiesdictCookie 字典
request.remote_addrstr客户端 IP
request.hoststr主机名
request.content_typestrContent-Type 头
request.content_lengthintContent-Length 头
request.is_jsonbool是否 JSON 请求
request.is_securebool是否 HTTPS

8. Flask 视图函数

8.1 返回值类型

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

返回类型Flask 处理状态码
str直接作为响应体200
dict / list自动转 JSON200
tuple(str, int)(响应体, 状态码)自定义
tuple(str, int, dict)(响应体, 状态码, 头部)自定义
Response 对象直接返回自定义
generator流式响应200
@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(通用类视图)
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 风格)
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 基础语法

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

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]

实机输出:

<h1>Hello FLASK!</h1>
<p>Length: 5</p>
<p>Reversed: ksalF</p>

9.3 循环与 loop 对象

@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']
    )

实机输出:

<ul>
  <li>1: Python</li>
  <li>2: Flask</li>
  <li>3: Jinja2</li>
  <li>4: Werkzeug</li>
</ul>

9.4 macro 宏

@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 模板继承

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><head><title>Child Page</title></head>
<body><h1>Content from Child</h1></body></html>

9.6 Jinja2 内置过滤器(54 个)

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

10. Flask 静态文件

10.1 默认静态文件配置

app = Flask(__name__)
# 默认: static_folder='static', static_url_path='/static'

10.2 自定义静态文件目录

app = Flask(__name__,
    static_folder='/path/to/static',
    static_url_path='/static'
)

10.3 实机测试

# 创建静态文件
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 模板中引用静态文件

<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. Flask Session 与 Cookie

11.1 Session 操作

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

11.2 Cookie 操作

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

11.3 set_cookie 参数说明

参数类型说明
keystrCookie 名称
valuestrCookie 值
max_ageint过期时间(秒)
expiresdatetime过期时间点
pathstr有效路径(默认 /
domainstr有效域名
securebool仅 HTTPS 传输
httponlybool禁止 JavaScript 访问
samesitestr跨站策略(Lax/Strict/None)

12. Flask 表单处理

12.1 安装依赖

pip3 install --break-system-packages flask-wtf email_validator

12.2 定义表单

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 表单验证成功

@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

实机输出(验证成功):

{
  "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 配置

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 定义模型

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 类型说明
IntegerintINTEGER整数
String(n)strVARCHAR(n)定长字符串
TextstrTEXT长文本
FloatfloatFLOAT浮点数
BooleanboolBOOLEAN布尔
DateTimedatetimeDATETIME日期时间
DatedateDATE日期
TimetimeTIME时间
NumericDecimalNUMERIC高精度小数
LargeBinarybytesBLOB二进制
JSONdict/listJSONJSON 数据

13.4 常用列参数

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

13.5 CRUD 操作

Create(插入)
# 单条插入
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(查询)
# 查询所有
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 条
聚合查询
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(更新)
# 单条更新
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(删除)
charlie = User.query.filter_by(username='charlie').first()
db.session.delete(charlie)
db.session.commit()
删除: <User charlie>
删除后剩余: 9 条

13.6 关联查询

# 一对多:通过 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 分页

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.getModel / None
.one()返回唯一一条(无结果抛异常)Model
.one_or_none()返回唯一一条或 NoneModel / 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 创建和注册蓝图

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_prefixURL 前缀
subdomain子域名
template_folder模板目录
static_folder静态文件目录
url_defaults默认 URL 值

15. Flask 错误处理

15.1 自定义错误页面

@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 自定义异常

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 错误码

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

16. Flask 中间件和扩展

16.1 请求钩子

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

钩子执行时机用途
before_request每次请求前认证检查、初始化变量
before_first_request第一个请求前(Flask 3.x 已移除)初始化操作
after_request每次请求后(无异常时)添加响应头、记录日志
teardown_request请求结束(无论是否异常)清理资源
teardown_appcontext应用上下文结束清理应用级资源
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 中间件

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-RESTfulREST API未安装
Flask-CORS跨域资源共享未安装
Flask-Limiter速率限制未安装
Flask-Admin后台管理未安装

17. Flask 配置管理

17.1 配置方式对比

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

17.2 多环境配置

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(字典配置)

app.config.from_mapping(
    CUSTOM_KEY='custom-value',
    API_VERSION='v1',
    MAX_ITEMS=100,
)

17.4 from_pyfile(文件配置)

# 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 实机验证

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 数据模型

# 用户收藏关联表(多对多)
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 表单定义

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 视图函数

# 首页
@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 路由表

EndpointMethodsURL说明权限
indexGET/首页公开
posts_listGET/posts文章列表(搜索+分页)公开
post_detailGET/post/<id>文章详情公开
loginGET,POST/login登录公开
registerGET,POST/register注册公开
logoutGET/logout退出登录
create_postGET,POST/post/new写文章登录
toggle_favoritePOST/post/<id>/favorite收藏/取消登录
favorites_listGET/favorites我的收藏登录
admin_dashboardGET/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 数据库迁移

# 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:开发服务器(不推荐生产)
flask run --host=0.0.0.0 --port=5000 --debug
方式 2:Gunicorn(推荐)
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 44 个 worker 进程
-b 0.0.0.0:5000绑定地址和端口
--timeout 120请求超时时间(秒)
--access-logfile -输出访问日志到 stdout
--error-logfile -输出错误日志到 stderr
--workersworker 数量(建议 2*CPU+1)
--threads每个 worker 的线程数
--preload预加载应用(节省内存)
方式 3:uWSGI
pip install uwsgi
uwsgi --http :5000 --wsgi-file app.py --callable app --processes 4
方式 4:Docker 部署
# 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"]
# 构建镜像
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 反向代理
# /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;
}
# 启动 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 测试框架

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 风格测试

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

解决

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.

解决

pip3 install --break-system-packages --ignore-installed blinker flask

踩坑 3:email_validator 缺失

现象

Exception: Install 'email_validator' for email validation support.

解决

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()

解决

# 旧(弃用)
user = User.query.get(1)

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

踩坑 5:Flask 3.x 移除 before_first_request

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

替代方案

# 方式 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.

解决

import importlib.metadata as meta
version = meta.version('flask')

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

现象

ImportError: cannot import name 'StringConverter' from 'werkzeug.routing'

解决

# 旧: from werkzeug.routing import StringConverter
# 新: from werkzeug.routing import UnicodeConverter

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

属性/方法说明
app.config配置字典
app.url_mapURL 映射表
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_processorShell 上下文

附录 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:版本信息

组件版本
Python3.12.3
Flask3.1.3
Werkzeug3.1.8
Jinja23.1.6
Flask-SQLAlchemy3.1.1
SQLAlchemy2.0.51
Flask-Migrate4.1.0
Flask-Login0.6.3
Flask-WTF1.3.0
Click8.4.2
Itsdangerous2.2.0
Blinker1.9.0
WTForms3.2.2
email_validator2.3.0
OSUbuntu 24.04.4 LTS
KernelLinux ecs-60a4-0001

全文完

所有代码均在华为云 FlexusX ecs-60a4-0001(Ubuntu 24.04 / Python 3.12.3 / Flask 3.1.3)上真实执行验证。
9 个单元测试全部通过,功能覆盖完整。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值