URL 路由
URL 路由是 Django 应用的入口,它将 URL 模式映射到视图函数。一个清晰、优雅的 URL 设计对于高质量的 Web 应用至关重要。
URL 配置基础
基本 URL 配置
Django 的 URL 配置是一个 Python 模块,包含 urlpatterns 列表:
# mysite/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='home'),
path('about/', views.about, name='about'),
path('contact/', views.contact, name='contact'),
]
path() 函数
path() 函数有四个参数:
path(route, view, kwargs=None, name=None)
- route:URL 模式字符串
- view:视图函数或类视图
- kwargs:传递给视图的额外参数(可选)
- name:URL 名称,用于反向解析(可选)
URL 参数
路径转换器
Django 内置了多种路径转换器:
from django.urls import path
from . import views
urlpatterns = [
# str - 匹配非空字符串,不含 /
path('article/<str:title>/', views.article_by_title),
# int - 匹配正整数
path('article/<int:id>/', views.article_by_id),
# slug - 匹配字母、数字、下划线、连字符
path('article/<slug:slug>/', views.article_by_slug),
# uuid - 匹配 UUID 格式
path('document/<uuid:id>/', views.document_detail),
# path - 匹配任意非空字符串,包含 /
path('file/<path:file_path>/', views.file_view),
]
自定义路径转换器
当内置转换器不满足需求时,可以自定义:
# converters.py
class FourDigitYearConverter:
"""四位年份转换器"""
regex = r'\d{4}'
def to_python(self, value):
return int(value)
def to_url(self, value):
return f'{value:04d}'
# urls.py
from django.urls import path, register_converter
from . import converters, views
# 注册自定义转换器
register_converter(converters.FourDigitYearConverter, 'yyyy')
urlpatterns = [
path('archive/<yyyy:year>/', views.archive),
]
使用正则表达式
对于更复杂的 URL 模式,可以使用 re_path():
from django.urls import path, re_path
from . import views
urlpatterns = [
# 匹配年月日格式:/2024/01/15/
re_path(r'^archive/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/$',
views.archive_date),
# 匹配用户名:只允许字母、数字、下划线
re_path(r'^user/(?P<username>[\w]+)/$', views.user_profile),
# 匹配文件扩展名
re_path(r'^download/(?P<filename>[\w-]+)\.(?P<extension>pdf|doc|txt)$',
views.download_file),
]
URL 分发
include() 函数
当项目有多个应用时,使用 include() 将 URL 分发到各应用:
# mysite/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', include('blog.urls')),
path('shop/', include('shop.urls')),
path('accounts/', include('django.contrib.auth.urls')),
]
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.post_list, name='post_list'),
path('post/<int:pk>/', views.post_detail, name='post_detail'),
path('category/<slug:slug>/', views.category_posts, name='category_posts'),
]
命名空间
使用 app_name 定义应用命名空间,避免 URL 名称冲突:
# blog/urls.py
app_name = 'blog'
urlpatterns = [
path('', views.post_list, name='post_list'),
]
在模板中使用:
<!-- 使用命名空间 -->
<a href="{% url 'blog:post_list' %}">博客首页</a>
include() 的高级用法
from django.urls import path, include
# 方式一:包含另一个 URLconf 模块
urlpatterns = [
path('blog/', include('blog.urls')),
]
# 方式二:包含 URL 列表
extra_patterns = [
path('reports/', views.reports),
path('reports/<int:id>/', views.report_detail),
]
urlpatterns = [
path('admin/', admin.site.urls),
path('management/', include(extra_patterns)),
]
# 方式三:传递额外参数
urlpatterns = [
path('blog/', include('blog.urls', namespace='blog')),
]
URL 反向解析
反向解析允许通过 URL 名称获取实际的 URL 字符串,避免硬编码 URL。
在视图中反向解析
from django.urls import reverse, reverse_lazy
from django.shortcuts import redirect
def redirect_to_post(request, pk):
# 使用 reverse 获取 URL
url = reverse('blog:post_detail', kwargs={'pk': pk})
return redirect(url)
def create_post(request):
if request.method == 'POST':
# 处理表单...
return redirect('blog:post_list') # 直接使用 redirect
# 使用 reverse_lazy(延迟解析,适用于类视图)
success_url = reverse_lazy('blog:post_list')
在模板中反向解析
<!-- 基本用法 -->
<a href="{% url 'home' %}">首页</a>
<!-- 带参数 -->
<a href="{% url 'blog:post_detail' pk=post.pk %}">{{ post.title }}</a>
<!-- 带查询参数 -->
<a href="{% url 'blog:post_list' %}?page=2">下一页</a>
<!-- 存储到变量 -->
{% url 'blog:post_detail' pk=post.pk as post_url %}
<a href="{{ post_url }}">{{ post.title }}</a>
在模型中使用
from django.db import models
from django.urls import reverse
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
def get_absolute_url(self):
"""返回文章的 URL"""
return reverse('blog:post_detail', kwargs={'slug': self.slug})
在模板中使用:
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
URL 最佳实践
1. 保持 URL 简洁
# 好的 URL 设计
path('articles/2024/', views.articles_2024)
path('articles/2024/01/', views.articles_2024_01)
path('articles/<int:pk>/', views.article_detail)
# 不好的 URL 设计
path('articles/view-all-from-year/2024/', views.articles_2024)
path('articles/article-detail-page/<int:pk>/', views.article_detail)
2. 使用有意义的名称
# 好的命名
path('', views.post_list, name='post_list'),
path('create/', views.post_create, name='post_create'),
path('<int:pk>/', views.post_detail, name='post_detail'),
path('<int:pk>/edit/', views.post_edit, name='post_edit'),
path('<int:pk>/delete/', views.post_delete, name='post_delete'),
# 不好的命名
path('', views.list, name='list'),
path('add/', views.add, name='add'),
3. 按功能分组
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
# 文章相关
path('', views.post_list, name='post_list'),
path('post/<int:pk>/', views.post_detail, name='post_detail'),
path('post/create/', views.post_create, name='post_create'),
path('post/<int:pk>/edit/', views.post_edit, name='post_edit'),
path('post/<int:pk>/delete/', views.post_delete, name='post_delete'),
# 分类相关
path('categories/', views.category_list, name='category_list'),
path('category/<slug:slug>/', views.category_detail, name='category_detail'),
# 标签相关
path('tags/', views.tag_list, name='tag_list'),
path('tag/<slug:slug>/', views.tag_detail, name='tag_detail'),
]
4. 使用 URL 模式常量
对于复杂的 URL 模式,可以定义常量:
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog'
# URL 模式常量
POST_DETAIL = 'post/<int:pk>/'
POST_EDIT = 'post/<int:pk>/edit/'
POST_DELETE = 'post/<int:pk>/delete/'
urlpatterns = [
path('', views.post_list, name='post_list'),
path(POST_DETAIL, views.post_detail, name='post_detail'),
path(POST_EDIT, views.post_edit, name='post_edit'),
path(POST_DELETE, views.post_delete, name='post_delete'),
]
常见 URL 模式
CRUD 操作
# 标准 CRUD URL 模式
urlpatterns = [
path('', views.list, name='list'), # 列表
path('create/', views.create, name='create'), # 创建
path('<int:pk>/', views.detail, name='detail'), # 详情
path('<int:pk>/update/', views.update, name='update'), # 更新
path('<int:pk>/delete/', views.delete, name='delete'), # 删除
]
分页和搜索
urlpatterns = [
# 文章列表
path('articles/', views.article_list, name='article_list'),
path('articles/page/<int:page>/', views.article_list, name='article_list_page'),
# 搜索
path('search/', views.search, name='search'),
]
API 风格
# RESTful 风格 URL
urlpatterns = [
path('api/articles/', views.article_list, name='api_article_list'),
path('api/articles/<int:pk>/', views.article_detail, name='api_article_detail'),
path('api/categories/', views.category_list, name='api_category_list'),
]
调试 URL
显示所有 URL
# 在 Django shell 中查看所有 URL
python manage.py shell
>>> from django.urls import get_resolver
>>> urls = get_resolver().url_patterns
>>> for url in urls:
... print(url.pattern)
URL 测试
from django.test import TestCase
from django.urls import reverse, resolve
class URLTests(TestCase):
def test_post_list_url(self):
"""测试文章列表 URL"""
url = reverse('blog:post_list')
self.assertEqual(url, '/blog/')
def test_post_detail_url(self):
"""测试文章详情 URL"""
url = reverse('blog:post_detail', kwargs={'pk': 1})
self.assertEqual(url, '/blog/post/1/')
def test_post_detail_resolves(self):
"""测试 URL 解析到正确的视图"""
view = resolve('/blog/post/1/')
self.assertEqual(view.func.__name__, 'post_detail')
self.assertEqual(view.kwargs['pk'], 1)