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
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 默认配置项
| 配置项 | 默认值 | 说明 |
|---|---|---|
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:
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 路由变量类型(转换器)
| 转换器 | 类名 | 说明 | 示例 |
|---|---|---|---|
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 多规则路由
@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.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 |
@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 个)
| 序号 | 过滤器 | 说明 |
|---|---|---|
| 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 默认静态文件配置
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 参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
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 安装依赖
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 类型 | 说明 |
|---|---|---|---|
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(插入)
# 单条插入
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.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 创建和注册蓝图
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 自定义错误页面
@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 错误码
| 状态码 | 名称 | 说明 |
|---|---|---|
| 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 | 应用上下文结束 | 清理应用级资源 |
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-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 多环境配置
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 路由表
| 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 数据库迁移
# 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 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
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_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 个单元测试全部通过,功能覆盖完整。
507

被折叠的 条评论
为什么被折叠?



