Skip to content

第3章:URL路由系统

3.2 Django 高级URL特性

命名URL和反向解析

为什么需要命名URL

在模板和视图中硬编码URL路径是不好的实践,因为:

  1. 维护困难:URL变更时需要修改多处代码
  2. 容易出错:手写URL容易拼写错误
  3. 不够灵活:难以适应URL结构的变化

Django提供了命名URL和反向解析机制来解决这些问题。

命名URL示例

python
# blog/urls.py
from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [
    path('', views.home, name='home'),
    path('articles/', views.article_list, name='article_list'),
    path('articles/<slug:slug>/', views.article_detail, name='article_detail'),
    path('categories/', views.category_list, name='category_list'),
    path('categories/<slug:slug>/', views.category_detail, name='category_detail'),
    path('authors/<str:username>/', views.author_profile, name='author_profile'),
    path('search/', views.search, name='search'),
    path('archive/<int:year>/', views.year_archive, name='year_archive'),
    path('archive/<int:year>/<int:month>/', views.month_archive, name='month_archive'),
    path('rss.xml', views.rss_feed, name='rss_feed'),
]

在视图中使用反向解析

python
# views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse, reverse_lazy
from django.http import HttpResponseRedirect
from .models import Article, Category
from .forms import ArticleForm

def create_article(request):
    """创建文章视图"""
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user
            article.save()
            
            # 使用reverse进行重定向
            return redirect('blog:article_detail', slug=article.slug)
    else:
        form = ArticleForm()
    
    return render(request, 'blog/create_article.html', {'form': form})

def article_published(request, article_id):
    """发布文章后的处理"""
    article = get_object_or_404(Article, id=article_id)
    article.status = 'published'
    article.save()
    
    # 构造重定向URL
    redirect_url = reverse('blog:article_detail', kwargs={'slug': article.slug})
    return HttpResponseRedirect(redirect_url)

# 类视图中使用reverse_lazy
from django.views.generic import CreateView
from django.urls import reverse_lazy

class ArticleCreateView(CreateView):
    model = Article
    form_class = ArticleForm
    template_name = 'blog/create_article.html'
    success_url = reverse_lazy('blog:article_list')  # 使用reverse_lazy
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)
    
    def get_success_url(self):
        """动态获取成功后的URL"""
        return reverse('blog:article_detail', kwargs={'slug': self.object.slug})
reverse_lazy说明

reverse_lazy是 reverse函数的延迟加载(Lazy)版本​。它的核心特点是:

  • ​延迟解析​:​只有在实际需要字符串值的时候(例如在视图处理请求时),才会真正执行解析URL的操作​。这避免了在Django的URL配置还未就绪时(比如类定义、模块加载阶段)就去解析URL可能引发的 django.core.exceptions.ImproperlyConfigured异常。
  • ​用于类属性定义​:正因为其延迟特性,它特别适合在类视图(Class-Based Views, CBV)​​ 中定义为类属性,例如success_url。

记住一个简单的原则:​当需要在类属性或模块级别定义URL时,优先考虑 reverse_lazy;在视图函数或方法内部,使用 reverse。

在类视图中指定 success_url​:这是最常见的情况。当删除一个对象成功后,你希望重定向到某个URL。如果这里使用 reverse,Django 在启动加载这个类时可能因为URL配置还未完全加载而报错。

python
from django.views.generic.edit import DeleteView
from django.urls import reverse_lazy
from .models import Article

class ArticleDeleteView(DeleteView):
    model = Article
    success_url = reverse_lazy('article_list')  # 在这里使用reverse_lazy是安全的

在模板中使用URL标签

html
<!-- blog/templates/blog/base.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>{% block title %}我的博客{% endblock %}</title>
</head>
<body>
    <nav class="navbar">
        <div class="container">
            <a class="navbar-brand" href="{% url 'blog:home' %}">我的博客</a>
            <ul class="navbar-nav">
                <li><a href="{% url 'blog:home' %}">首页</a></li>
                <li><a href="{% url 'blog:article_list' %}">文章</a></li>
                <li><a href="{% url 'blog:category_list' %}">分类</a></li>
                <li><a href="{% url 'blog:search' %}">搜索</a></li>
            </ul>
        </div>
    </nav>
    
    <main class="container">
        {% block content %}
        {% endblock %}
    </main>
    
    <footer>
        <p><a href="{% url 'blog:rss_feed' %}">RSS订阅</a></p>
    </footer>
</body>
</html>
html
<!-- blog/templates/blog/article_list.html -->
{% extends 'blog/base.html' %}

{% block content %}
<h2>文章列表</h2>

{% for article in articles %}
<article class="article-card">
    <h3>
        <a href="{% url 'blog:article_detail' slug=article.slug %}">
            {{ article.title }}
        </a>
    </h3>
    <p class="meta">
        作者:<a href="{% url 'blog:author_profile' username=article.author.username %}">
            {{ article.author.username }}
        </a>
        {% if article.category %}
        | 分类:<a href="{% url 'blog:category_detail' slug=article.category.slug %}">
            {{ article.category.name }}
        </a>
        {% endif %}
        | 发布时间:{{ article.publish_date|date:"Y年m月d日" }}
    </p>
    <p>{{ article.excerpt }}</p>
    <a href="{% url 'blog:article_detail' slug=article.slug %}" class="read-more">
        阅读全文
    </a>
</article>
{% empty %}
<p>暂无文章发布。</p>
{% endfor %}

<!-- 分页链接 -->
{% if is_paginated %}
<div class="pagination">
    {% if page_obj.has_previous %}
    <a href="{% url 'blog:article_list' %}?page={{ page_obj.previous_page_number }}">
        上一页
    </a>
    {% endif %}
    
    {% if page_obj.has_next %}
    <a href="{% url 'blog:article_list' %}?page={{ page_obj.next_page_number }}">
        下一页
    </a>
    {% endif %}
</div>
{% endif %}
{% endblock %}

带参数的URL反向解析

html
<!-- 在模板中传递多个参数 -->
{% url 'blog:monthly_archive' year=2023 month=12 %}

<!-- 使用变量作为参数 -->
{% url 'blog:article_detail' slug=article.slug %}

<!-- 复杂的URL参数 -->
{% url 'blog:article_detail' year=article.publish_date.year month=article.publish_date.month day=article.publish_date.day slug=article.slug %}
python
# 在Python代码中传递参数
from django.urls import reverse

# 单个参数
url = reverse('blog:article_detail', args=[article.slug])
# 或使用关键字参数
url = reverse('blog:article_detail', kwargs={'slug': article.slug})

# 多个参数
url = reverse('blog:monthly_archive', kwargs={
    'year': 2023,
    'month': 12
})

# 复杂参数
url = reverse('blog:article_detail', kwargs={
    'year': article.publish_date.year,
    'month': article.publish_date.month,
    'day': article.publish_date.day,
    'slug': article.slug
})

URL命名空间

应用命名空间

使用应用命名空间可以避免不同应用间URL名称冲突。比如blog首页和news首页都有home时,可以使用应用命名空间来区分。

python
# blog/urls.py
from django.urls import path
from . import views

app_name = 'blog'  # 应用命名空间

urlpatterns = [
    path('', views.home, name='home'),
    path('articles/', views.article_list, name='article_list'),
    # ...
]
python
# news/urls.py
from django.urls import path
from . import views

app_name = 'news'  # 应用命名空间

urlpatterns = [
    path('', views.home, name='home'),  # 与blog应用的home不冲突
    path('articles/', views.article_list, name='article_list'),
    # ...
]

实例命名空间

实例命名空间用于同一个应用的多个实例:

python
# mysite/urls.py
from django.urls import path, include

urlpatterns = [
    path('blog/', include('blog.urls', namespace='blog')),
    path('news/', include('news.urls', namespace='news')),  # 同一个应用,不同实例
    path('admin/', admin.site.urls),
]

命名空间的使用

python
# 在视图中使用命名空间
from django.urls import reverse

def some_view(request):
    # 使用应用命名空间
    blog_url = reverse('blog:article_list')
    news_url = reverse('news:article_list')
    
    # 使用当前命名空间
    current_namespace = request.resolver_match.namespace
    url = reverse(f'{current_namespace}:home')
    
    return redirect(url)
html
<!-- 在模板中使用命名空间 -->
<a href="{% url 'blog:home' %}">博客首页</a>
<a href="{% url 'news:home' %}">新闻首页</a>

<!-- 动态命名空间 -->
{% with current_app as app_name %}
<a href="{% url app_name|add:':home' %}">首页</a>
{% endwith %}

包含其他URLconf

基本包含

python
# mysite/urls.py (主URLconf)
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
    path('api/', include('api.urls')),
    path('users/', include('users.urls')),
    path('', include('core.urls')),  # 根路径
]

带命名空间的包含

python
# mysite/urls.py
from django.urls import path, include

urlpatterns = [
    path('blog/', include('blog.urls', namespace='blog')),
    path('api/v1/', include('api.urls', namespace='api-v1')),
    path('api/v2/', include('api.urls', namespace='api-v2')),
]

条件包含

python
# mysite/urls.py
from django.conf import settings
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
]

# 开发环境特有的URL
if settings.DEBUG:
    import debug_toolbar
    urlpatterns += [
        path('__debug__/', include(debug_toolbar.urls)),
    ]
    
    # 媒体文件服务(仅开发环境)
    from django.conf.urls.static import static
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

# API文档(仅在特定环境下)
if getattr(settings, 'ENABLE_API_DOCS', False):
    urlpatterns += [
        path('api-docs/', include('rest_framework.urls')),
    ]

嵌套包含

python
# api/urls.py
from django.urls import path, include

app_name = 'api'

urlpatterns = [
    path('v1/', include('api.v1.urls', namespace='v1')),
    path('v2/', include('api.v2.urls', namespace='v2')),
]
python
# api/v1/urls.py
from django.urls import path
from . import views

app_name = 'v1'

urlpatterns = [
    path('articles/', views.ArticleListAPIView.as_view(), name='article_list'),
    path('articles/<int:id>/', views.ArticleDetailAPIView.as_view(), name='article_detail'),
]

使用嵌套命名空间:

python
# 访问嵌套的URL
url = reverse('api:v1:article_list')
# 结果:/api/v1/articles/

实际示例:用户个人页面路由

让我们通过一个完整的用户个人页面路由系统来演示高级URL特性:

1. 用户应用URL配置

python
# users/urls.py
from django.urls import path, include
from . import views

app_name = 'users'

# 用户个人资料相关URL
profile_patterns = [
    path('', views.ProfileView.as_view(), name='profile'),
    path('edit/', views.ProfileEditView.as_view(), name='profile_edit'),
    path('avatar/', views.AvatarUpdateView.as_view(), name='avatar_update'),
    path('settings/', views.SettingsView.as_view(), name='settings'),
]

# 用户内容相关URL
content_patterns = [
    path('articles/', views.UserArticlesView.as_view(), name='articles'),
    path('comments/', views.UserCommentsView.as_view(), name='comments'),
    path('favorites/', views.UserFavoritesView.as_view(), name='favorites'),
    path('drafts/', views.UserDraftsView.as_view(), name='drafts'),
]

urlpatterns = [
    # 用户列表和搜索
    path('', views.UserListView.as_view(), name='list'),
    path('search/', views.UserSearchView.as_view(), name='search'),
    
    # 认证相关
    path('login/', views.LoginView.as_view(), name='login'),
    path('logout/', views.LogoutView.as_view(), name='logout'),
    path('register/', views.RegisterView.as_view(), name='register'),
    path('password-reset/', views.PasswordResetView.as_view(), name='password_reset'),
    
    # 用户个人页面(包含子路由)
    path('<str:username>/', views.UserDetailView.as_view(), name='detail'),
    path('<str:username>/profile/', include(profile_patterns)),
    path('<str:username>/content/', include(content_patterns)),
    
    # 关注系统
    path('<str:username>/follow/', views.FollowUserView.as_view(), name='follow'),
    path('<str:username>/unfollow/', views.UnfollowUserView.as_view(), name='unfollow'),
    path('<str:username>/followers/', views.UserFollowersView.as_view(), name='followers'),
    path('<str:username>/following/', views.UserFollowingView.as_view(), name='following'),
]

2. 对应的视图实现

python
# users/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import DetailView, ListView, UpdateView
from django.urls import reverse, reverse_lazy
from django.contrib.auth.models import User
from django.http import JsonResponse
from .models import Profile, Follow
from .forms import ProfileForm

class UserDetailView(DetailView):
    """用户详情页"""
    model = User
    template_name = 'users/user_detail.html'
    context_object_name = 'user'
    slug_field = 'username'
    slug_url_kwarg = 'username'
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user = self.get_object()
        
        # 添加用户统计信息
        context.update({
            'article_count': user.article_set.filter(status='published').count(),
            'follower_count': user.followers.count(),
            'following_count': user.following.count(),
            'is_following': self.request.user.is_authenticated and 
                          Follow.objects.filter(
                              follower=self.request.user, 
                              following=user
                          ).exists(),
        })
        return context

class ProfileView(LoginRequiredMixin, DetailView):
    """用户个人资料页"""
    model = User
    template_name = 'users/profile.html'
    context_object_name = 'profile_user'
    slug_field = 'username'
    slug_url_kwarg = 'username'
    
    def get_object(self):
        username = self.kwargs.get('username')
        return get_object_or_404(User, username=username)

class ProfileEditView(LoginRequiredMixin, UpdateView):
    """编辑个人资料"""
    model = Profile
    form_class = ProfileForm
    template_name = 'users/profile_edit.html'
    
    def get_object(self):
        username = self.kwargs.get('username')
        user = get_object_or_404(User, username=username)
        # 确保只能编辑自己的资料
        if user != self.request.user:
            raise PermissionDenied("您只能编辑自己的资料")
        return user.profile
    
    def get_success_url(self):
        return reverse('users:profile', kwargs={'username': self.request.user.username})

class FollowUserView(LoginRequiredMixin, View):
    """关注用户"""
    def post(self, request, username):
        target_user = get_object_or_404(User, username=username)
        
        if target_user == request.user:
            return JsonResponse({'error': '不能关注自己'}, status=400)
        
        follow, created = Follow.objects.get_or_create(
            follower=request.user,
            following=target_user
        )
        
        if created:
            return JsonResponse({
                'status': 'followed',
                'message': f'已关注 {username}',
                'follower_count': target_user.followers.count()
            })
        else:
            return JsonResponse({
                'status': 'already_following',
                'message': f'已经关注了 {username}'
            })

3. 模板中的URL使用

html
<!-- users/templates/users/user_detail.html -->
{% extends 'base.html' %}

{% block content %}
<div class="user-profile">
    <div class="user-header">
        <img src="{{ user.profile.avatar.url }}" alt="{{ user.username }}" class="avatar">
        <div class="user-info">
            <h1>{{ user.get_full_name|default:user.username }}</h1>
            <p>@{{ user.username }}</p>
            
            <div class="user-stats">
                <a href="{% url 'users:articles' username=user.username %}">
                    <strong>{{ article_count }}</strong> 文章
                </a>
                <a href="{% url 'users:followers' username=user.username %}">
                    <strong>{{ follower_count }}</strong> 关注者
                </a>
                <a href="{% url 'users:following' username=user.username %}">
                    <strong>{{ following_count }}</strong> 关注中
                </a>
            </div>
            
            {% if user == request.user %}
            <!-- 用户自己的页面 -->
            <div class="user-actions">
                <a href="{% url 'users:profile' username=user.username %}" class="btn btn-primary">
                    查看资料
                </a>
                <a href="{% url 'users:profile_edit' username=user.username %}" class="btn btn-secondary">
                    编辑资料
                </a>
                <a href="{% url 'users:drafts' username=user.username %}" class="btn btn-outline-primary">
                    草稿箱
                </a>
            </div>
            {% else %}
            <!-- 其他用户的页面 -->
            <div class="user-actions">
                {% if is_following %}
                <button class="btn btn-outline-primary" onclick="unfollowUser('{{ user.username }}')">
                    已关注
                </button>
                {% else %}
                <button class="btn btn-primary" onclick="followUser('{{ user.username }}')">
                    关注
                </button>
                {% endif %}
            </div>
            {% endif %}
        </div>
    </div>
    
    <div class="user-content">
        <nav class="nav nav-tabs">
            <a class="nav-link active" href="{% url 'users:detail' username=user.username %}">
                概览
            </a>
            <a class="nav-link" href="{% url 'users:articles' username=user.username %}">
                文章
            </a>
            <a class="nav-link" href="{% url 'users:comments' username=user.username %}">
                评论
            </a>
            {% if user == request.user %}
            <a class="nav-link" href="{% url 'users:favorites' username=user.username %}">
                收藏
            </a>
            {% endif %}
        </nav>
        
        <div class="tab-content">
            <!-- 用户概览内容 -->
        </div>
    </div>
</div>

<script>
function followUser(username) {
    fetch(`{% url 'users:follow' username='__USERNAME__' %}`.replace('__USERNAME__', username), {
        method: 'POST',
        headers: {
            'X-CSRFToken': '{{ csrf_token }}',
            'Content-Type': 'application/json'
        }
    })
    .then(response => response.json())
    .then(data => {
        if (data.status === 'followed') {
            location.reload();
        }
    });
}

function unfollowUser(username) {
    fetch(`{% url 'users:unfollow' username='__USERNAME__' %}`.replace('__USERNAME__', username), {
        method: 'POST',
        headers: {
            'X-CSRFToken': '{{ csrf_token }}',
            'Content-Type': 'application/json'
        }
    })
    .then(response => response.json())
    .then(data => {
        if (data.status === 'unfollowed') {
            location.reload();
        }
    });
}
</script>
{% endblock %}

4. 在主URLconf中包含

python
# mysite/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('blog.urls')),
    path('users/', include('users.urls')),
    path('api/', include('api.urls')),
]

URL调试和测试

1. URL反向解析测试

python
# tests/test_urls.py
from django.test import TestCase
from django.urls import reverse, resolve
from django.contrib.auth.models import User
from users.views import UserDetailView

class URLTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass'
        )
    
    def test_user_detail_url_reverse(self):
        """测试用户详情页URL反向解析"""
        url = reverse('users:detail', kwargs={'username': 'testuser'})
        self.assertEqual(url, '/users/testuser/')
    
    def test_user_detail_url_resolve(self):
        """测试URL解析到正确的视图"""
        resolver = resolve('/users/testuser/')
        self.assertEqual(resolver.view_class, UserDetailView)
        self.assertEqual(resolver.kwargs, {'username': 'testuser'})
    
    def test_nested_urls(self):
        """测试嵌套URL"""
        url = reverse('users:profile', kwargs={'username': 'testuser'})
        self.assertEqual(url, '/users/testuser/profile/')
        
        url = reverse('users:articles', kwargs={'username': 'testuser'})
        self.assertEqual(url, '/users/testuser/content/articles/')

2. URL性能测试

python
# tests/test_url_performance.py
from django.test import TestCase
from django.urls import reverse
from django.test.utils import override_settings
import time

class URLPerformanceTests(TestCase):
    def test_url_reverse_performance(self):
        """测试URL反向解析性能"""
        start_time = time.time()
        
        for i in range(1000):
            url = reverse('users:detail', kwargs={'username': f'user{i}'})
        
        end_time = time.time()
        duration = end_time - start_time
        
        # 确保1000次反向解析在合理时间内完成
        self.assertLess(duration, 1.0, "URL反向解析性能过慢")

小结

Django的高级URL特性提供了强大而灵活的URL管理机制:

  1. 命名URL:避免硬编码,提高维护性
  2. 反向解析:通过名称生成URL,支持参数传递
  3. 命名空间:避免URL名称冲突,支持应用和实例命名空间
  4. 包含URLconf:实现模块化的URL配置
  5. 条件包含:根据环境动态配置URL

最佳实践:

  • 始终为URL模式命名
  • 使用应用命名空间避免冲突
  • 在模板中使用{% url %}标签
  • 在视图中使用reverse()函数
  • 编写URL测试确保配置正确

下一篇

我们将深入学习Django的模型系统,这是Django应用的数据基础。

4.1 Django 数据模型基础 →

目录

返回课程目录

Released under the Apache 2.0 License.