路由与视图
路由是 Flask 应用的核心,它将 URL 映射到 Python 函数。在现代 Web 应用中,使用有意义的 URL 非常重要,用户更喜欢那些有意义、易于记忆和可以直接访问的 URL。
路由的本质
当用户在浏览器中访问一个 URL 时,Web 服务器需要知道应该执行哪段代码来响应这个请求。路由就是建立 URL 与处理函数之间映射关系的机制。
Flask 使用装饰器的方式定义路由,这种方式简洁直观:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return '首页'
@app.route('/about')
def about():
return '关于页面'
当用户访问 / 时,Flask 会调用 index() 函数;访问 /about 时,调用 about() 函数。这种映射关系由 Flask 内部的 URL 映射表维护。
动态路由
动态路由允许 URL 中包含变量部分,使得一个路由可以匹配多种 URL 模式。这在构建 RESTful API 或处理用户相关页面时非常有用。
基本变量
使用 <variable_name> 标记 URL 中的可变部分:
from markupsafe import escape
@app.route('/user/<username>')
def show_user_profile(username):
# 显示该用户的个人资料
return f'用户: {escape(username)}'
为什么使用 escape()?
当显示用户输入的数据时,必须进行 HTML 转义以防止 XSS(跨站脚本攻击)。如果用户提交恶意脚本代码,转义会将其显示为普通文本而不是执行脚本。
# 如果用户访问 /user/<script>alert("bad")</script>
# 不转义:浏览器会执行恶意脚本
# 转义后:显示为普通文本 "<script>alert("bad")</script>"
指定变量类型
Flask 支持多种类型转换器,可以自动将 URL 参数转换为指定的 Python 类型:
@app.route('/post/<int:post_id>')
def show_post(post_id):
# post_id 自动转换为整数类型
return f'文章 ID: {post_id}'
@app.route('/price/<float:price>')
def show_price(price):
# price 自动转换为浮点数
return f'价格: {price}'
@app.route('/path/<path:subpath>')
def show_subpath(subpath):
# path 类型可以包含斜杠,用于匹配多级路径
return f'子路径: {subpath}'
类型转换器列表:
| 转换器 | 说明 | 示例 URL | 匹配结果 |
|---|---|---|---|
string | 默认,接受不含斜杠的任意文本 | /user/john | john |
int | 正整数 | /post/123 | 123 (int) |
float | 正浮点数 | /price/19.99 | 19.99 (float) |
path | 类似 string,但可包含斜杠 | /path/a/b/c | a/b/c |
uuid | UUID 字符串 | /item/123e4567-e89b-12d3-a456-426614174000 | UUID 对象 |
实际应用:RESTful API 设计
动态路由非常适合设计 RESTful API:
# 获取所有用户
@app.get('/api/users')
def list_users():
return {'users': ['Alice', 'Bob', 'Charlie']}
# 获取单个用户
@app.get('/api/users/<int:user_id>')
def get_user(user_id):
return {'id': user_id, 'name': f'User {user_id}'}
# 创建用户
@app.post('/api/users')
def create_user():
return {'message': 'User created'}, 201
# 更新用户
@app.put('/api/users/<int:user_id>')
def update_user(user_id):
return {'message': f'User {user_id} updated'}
# 删除用户
@app.delete('/api/users/<int:user_id>')
def delete_user(user_id):
return {'message': f'User {user_id} deleted'}
URL 尾部斜杠的行为
Flask 对 URL 尾部斜杠有特殊处理规则,理解这一点对于设计良好的 URL 结构很重要:
@app.route('/projects/')
def projects():
return '项目列表页面'
@app.route('/about')
def about():
return '关于页面'
行为说明:
-
/projects/(有尾部斜杠):类似于文件系统中的目录- 访问
/projects/:正常响应 - 访问
/projects:Flask 自动重定向到/projects/
- 访问
-
/about(无尾部斜杠):类似于文件的路径- 访问
/about:正常响应 - 访问
/about/:返回 404 错误
- 访问
这种设计有助于保持 URL 的唯一性,避免搜索引擎将同一内容索引两次。
最佳实践:
- 对于"集合"类型的资源(如用户列表、文章列表),使用尾部斜杠
- 对于"单个"资源(如某篇文章、某个用户),不使用尾部斜杠
HTTP 方法
HTTP 协议定义了多种请求方法,每种方法有不同的语义。Flask 允许你为同一个 URL 定义不同的处理逻辑。
使用 methods 参数
from flask import request
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# 处理登录表单提交
username = request.form.get('username')
password = request.form.get('password')
# 验证逻辑...
return '处理登录'
else:
# 显示登录表单
return '显示登录表单'
使用专用装饰器(推荐)
Flask 提供了 @app.get()、@app.post() 等专用装饰器,代码更清晰:
@app.get('/login')
def login_form():
"""显示登录表单"""
return '显示登录表单'
@app.post('/login')
def login_submit():
"""处理登录请求"""
return '处理登录'
常用 HTTP 方法:
| 方法 | 用途 | 示例 |
|---|---|---|
| GET | 获取资源 | 查看文章列表 |
| POST | 创建资源 | 发布新文章 |
| PUT | 完整更新资源 | 更新文章全部内容 |
| PATCH | 部分更新资源 | 更新文章标题 |
| DELETE | 删除资源 | 删除文章 |
自动支持的方法:
如果路由支持 GET 方法,Flask 会自动支持 HEAD 方法(只返回响应头,不返回响应体)和 OPTIONS 方法(返回支持的 HTTP 方法列表)。
URL 构建
在代码中硬编码 URL 是不好的做法。使用 url_for() 函数构建 URL 有诸多优势:
from flask import url_for
@app.route('/')
def index():
return '首页'
@app.route('/user/<username>')
def profile(username):
return f'{username} 的个人资料'
# 在 Python 代码中生成 URL
with app.test_request_context():
print(url_for('index')) # /
print(url_for('profile', username='john')) # /user/john
print(url_for('profile', username='张三')) # /user/%E5%BC%A0%E4%B8%89
print(url_for('index', page=2)) # /?page=2
使用 url_for() 的优势:
- 描述性更强:函数名比 URL 更有语义
- 易于维护:修改 URL 路径时只需修改一处
- 自动转义:特殊字符自动处理
- 绝对路径:生成的 URL 总是绝对路径,避免相对路径问题
- 支持子路径部署:如果应用部署在
/myapp子路径下,url_for()会自动处理
在模板中使用:
<a href="{{ url_for('profile', username='john') }}">用户资料</a>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
重定向与错误处理
重定向
使用 redirect() 函数将用户重定向到其他页面:
from flask import redirect, url_for
@app.route('/old-page')
def old_page():
# 永久重定向(301)
return redirect(url_for('new_page'), code=301)
@app.route('/new-page')
def new_page():
return '新页面'
@app.route('/admin')
def admin():
# 未授权时重定向到登录页
if not is_logged_in():
return redirect(url_for('login'))
return '管理员页面'
错误处理
自定义错误页面,提供更好的用户体验:
from flask import render_template
@app.errorhandler(404)
def not_found(error):
"""404 错误处理"""
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
"""500 错误处理"""
return render_template('500.html'), 500
@app.errorhandler(403)
def forbidden(error):
"""403 禁止访问"""
return render_template('403.html'), 403
也可以返回 JSON 格式的错误响应(适合 API):
@app.errorhandler(404)
def api_not_found(error):
return {'error': 'Not found', 'message': '请求的资源不存在'}, 404
自定义转换器
当内置转换器不能满足需求时,可以创建自定义转换器:
from werkzeug.routing import BaseConverter
class ListConverter(BaseConverter):
"""将逗号分隔的字符串转换为列表"""
def to_python(self, value):
"""URL 到 Python 值的转换"""
return value.split(',')
def to_url(self, values):
"""Python 值到 URL 的转换"""
return ','.join(str(v) for v in values)
# 注册转换器
app.url_map.converters['list'] = ListConverter
# 使用自定义转换器
@app.route('/items/<list:items>')
def show_items(items):
# 访问 /items/apple,banana,orange
# items 为 ['apple', 'banana', 'orange']
return f'项目列表: {items}'
# url_for 中的使用
with app.test_request_context():
print(url_for('show_items', items=['a', 'b', 'c']))
# 输出: /items/a,b,c
更多自定义转换器示例:
import re
class RegexConverter(BaseConverter):
"""正则表达式转换器"""
def __init__(self, url_map, *items):
super().__init__(url_map)
self.regex = items[0] if items else '.*'
app.url_map.converters['regex'] = RegexConverter
# 只匹配 4 位数字
@app.route('/year/<regex("\d{4}"):year>')
def show_year(year):
return f'年份: {year}'
蓝图路由
当应用变得复杂时,使用蓝图组织路由可以使代码结构更清晰:
from flask import Blueprint
# 创建认证蓝图
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
@auth_bp.route('/login')
def login():
return '登录页面'
@auth_bp.route('/register')
def register():
return '注册页面'
@auth_bp.route('/logout')
def logout():
return '退出登录'
# 创建 API 蓝图
api_bp = Blueprint('api', __name__, url_prefix='/api/v1')
@api_bp.route('/users')
def list_users():
return {'users': []}
# 注册蓝图
app.register_blueprint(auth_bp)
app.register_blueprint(api_bp)
蓝图的优势:
- 模块化:将相关功能组织在一起
- URL 前缀:统一添加前缀,避免重复
- 易于维护:每个蓝图可以放在单独的文件中
- 可重用:蓝图可以在多个应用中复用
蓝图文件结构:
myapp/
├── __init__.py # 创建应用,注册蓝图
├── auth/
│ ├── __init__.py # 定义 auth 蓝图
│ └── routes.py # 认证相关路由
├── api/
│ ├── __init__.py # 定义 api 蓝图
│ └── routes.py # API 路由
└── templates/
├── auth/
│ └── login.html
└── base.html
路由最佳实践
URL 设计原则
-
使用有意义的 URL
- 好的做法:
/users/123、/articles/how-to-learn-python - 不好的做法:
/u/123、/a/123
- 好的做法:
-
使用复数名词表示资源集合
- 好的做法:
/users、/products - 不好的做法:
/user、/product
- 好的做法:
-
使用小写字母和连字符
- 好的做法:
/user-profiles、/api-endpoints - 不好的做法:
/userProfiles、/api_endpoints
- 好的做法:
-
避免在 URL 中使用动词
- 好的做法:
POST /users(语义清晰) - 不好的做法:
POST /create-user
- 好的做法:
-
使用嵌套表示资源关系
/users/123/posts- 用户 123 的文章列表/posts/456/comments- 文章 456 的评论列表
命名约定
# 路由函数命名建议使用 <资源>_<动作> 格式
@app.get('/users')
def users_list(): # 用户列表
pass
@app.get('/users/<int:id>')
def users_detail(): # 用户详情
pass
@app.post('/users')
def users_create(): # 创建用户
pass
@app.put('/users/<int:id>')
def users_update(): # 更新用户
pass
@app.delete('/users/<int:id>')
def users_delete(): # 删除用户
pass
安全考虑
from markupsafe import escape
# 始终转义用户输入
@app.route('/search')
def search():
query = request.args.get('q', '')
# 转义防止 XSS
return f'搜索结果: {escape(query)}'
# 验证动态路由参数
@app.route('/download/<filename>')
def download(filename):
# 防止路径遍历攻击
if '..' in filename or filename.startswith('/'):
abort(403)
return send_from_directory('uploads', filename)
小结
本章我们学习了 Flask 路由的核心概念:
- 基本路由:使用
@app.route()装饰器绑定 URL 和函数 - 动态路由:使用
<variable>创建可变 URL,支持类型转换 - HTTP 方法:区分 GET、POST 等请求方法,构建 RESTful API
- URL 构建:使用
url_for()生成 URL,便于维护 - 蓝图:模块化组织路由,适用于大型应用
- 最佳实践:设计清晰、安全、易于维护的 URL 结构
练习
- 创建一个简单的博客系统,包含文章列表、文章详情、创建文章的路由
- 实现一个 RESTful API,支持用户的 CRUD 操作
- 使用蓝图重构一个现有项目的路由结构
- 创建自定义转换器,验证邮箱格式的路由参数