跳到主要内容

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)