跳到主要内容

路由与视图

路由是 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/johnjohn
int正整数/post/123123 (int)
float正浮点数/price/19.9919.99 (float)
path类似 string,但可包含斜杠/path/a/b/ca/b/c
uuidUUID 字符串/item/123e4567-e89b-12d3-a456-426614174000UUID 对象

实际应用: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() 的优势

  1. 描述性更强:函数名比 URL 更有语义
  2. 易于维护:修改 URL 路径时只需修改一处
  3. 自动转义:特殊字符自动处理
  4. 绝对路径:生成的 URL 总是绝对路径,避免相对路径问题
  5. 支持子路径部署:如果应用部署在 /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)

蓝图的优势

  1. 模块化:将相关功能组织在一起
  2. URL 前缀:统一添加前缀,避免重复
  3. 易于维护:每个蓝图可以放在单独的文件中
  4. 可重用:蓝图可以在多个应用中复用

蓝图文件结构

myapp/
├── __init__.py # 创建应用,注册蓝图
├── auth/
│ ├── __init__.py # 定义 auth 蓝图
│ └── routes.py # 认证相关路由
├── api/
│ ├── __init__.py # 定义 api 蓝图
│ └── routes.py # API 路由
└── templates/
├── auth/
│ └── login.html
└── base.html

路由最佳实践

URL 设计原则

  1. 使用有意义的 URL

    • 好的做法:/users/123/articles/how-to-learn-python
    • 不好的做法:/u/123/a/123
  2. 使用复数名词表示资源集合

    • 好的做法:/users/products
    • 不好的做法:/user/product
  3. 使用小写字母和连字符

    • 好的做法:/user-profiles/api-endpoints
    • 不好的做法:/userProfiles/api_endpoints
  4. 避免在 URL 中使用动词

    • 好的做法:POST /users(语义清晰)
    • 不好的做法:POST /create-user
  5. 使用嵌套表示资源关系

    • /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 路由的核心概念:

  1. 基本路由:使用 @app.route() 装饰器绑定 URL 和函数
  2. 动态路由:使用 <variable> 创建可变 URL,支持类型转换
  3. HTTP 方法:区分 GET、POST 等请求方法,构建 RESTful API
  4. URL 构建:使用 url_for() 生成 URL,便于维护
  5. 蓝图:模块化组织路由,适用于大型应用
  6. 最佳实践:设计清晰、安全、易于维护的 URL 结构

练习

  1. 创建一个简单的博客系统,包含文章列表、文章详情、创建文章的路由
  2. 实现一个 RESTful API,支持用户的 CRUD 操作
  3. 使用蓝图重构一个现有项目的路由结构
  4. 创建自定义转换器,验证邮箱格式的路由参数

参考资料