一、什么是路由?
路由,简单来说就是根据用户请求的 URL 链接,判断对应的处理程序,并返回处理结果。也就是建立 URL 与 Django 视图之间的映射关系。
Django 的路由在 urls.py 文件中配置,每一条配置对应一个相应的处理方法。当用户访问某个 URL 时,Django 会从上到下逐条匹配 urlpatterns 列表,找到第一个匹配的规则,调用对应的视图函数。
二、Django 路由的版本差异
Django 不同版本中,urls.py 的配置方式有所不同。这是初学者最容易困惑的地方。
Django 1.1.x 版本:url() 方法
python
from django.conf.urls import url # 用 url 需要引入 urlpatterns = [ url(r'^admin/$', admin.site.urls), url(r'^index/$', views.index), # 普通路径 url(r'^articles/([0-9]{4})/$', views.articles), # 正则路径 ]
特点: 普通路径和正则路径均可使用,需要自己手动添加正则首位限制符号(^ 和 $)。
Django 2.2.x 之后的版本:path 与 re_path
python
from django.urls import path, re_path urlpatterns = [ path('admin/', admin.site.urls), path('index/', views.index), # 普通路径 re_path(r'^articles/([0-9]{4})/$', views.articles), # 正则路径 ]
特点:
-
path:用于普通路径,不需要手动添加正则首位限制符号,底层已自动添加。 -
re_path:用于正则路径,需要手动添加正则首位限制符号。
版本对应关系总结
| Django 版本 | 普通路径 | 正则路径 |
|---|---|---|
| 1.1.x | url() |
url() |
| 2.2.x+ | path() |
re_path() |
结论: Django 1.1.x 中的
url与 Django 2.2.x 中的re_path用法完全相同。新项目一律推荐使用path和re_path。
三、正则路径中的分组传参
正则路径的强大之处在于可以从 URL 中提取参数,传递给视图函数。分组分为两种:无名分组 和有名分组。
1. 无名分组
语法: (正则表达式)
传参方式: 按位置传参,一一对应。
python
urlpatterns = [ path('admin/', admin.site.urls), re_path(r"^index/([0-9]{4})/$", views.index), ]
python
from django.shortcuts import HttpResponse def index(request, year): print(year) # 一个形参代表路径中一个分组的内容,按顺序匹配 return HttpResponse('教程')
访问 /index/2026/,视图函数中的 year 会接收到字符串 "2026"。
注意: 视图函数中除了 request,其他形参的数量必须与 URL 中的分组数量一致。
2. 有名分组
语法: (?P<组名>正则表达式)
传参方式: 按关键字传参,与位置顺序无关。
python
urlpatterns = [ path('admin/', admin.site.urls), re_path(r"^index/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$", views.index), ]
python
from django.shortcuts import HttpResponse def index(request, year, month): print(year, month) # 按关键字对应匹配 return HttpResponse('教程')
访问 /index/2026/09/,year 和 month 分别接收到 "2026" 和 "09"。
有名分组的优势: 视图函数中的形参名称与 URL 中的组名一一对应,即使调整分组顺序,视图函数也无需修改。
3. 两种分组对比
| 对比项 | 无名分组 | 有名分组 |
|---|---|---|
| 语法 | ([0-9]{4}) |
(?P<year>[0-9]{4}) |
| 传参方式 | 位置传参 | 关键字传参 |
| 视图形参要求 | 数量一致即可 | 数量一致且名称对应 |
| 可读性 | 较差 | 较好 |
| 推荐度 | 一般 | 推荐 |
四、路由分发(include)
1. 为什么需要路由分发?
当项目中有多个 app 时,如果所有路由都写在一个 urls.py 中,会造成:
-
命名混淆:不同 app 的路由混在一起,难以区分。
-
维护困难:任何一个 app 的路由改动,都要修改同一个文件。
-
协作冲突:多人开发时容易产生冲突。
解决方案: 使用路由分发(include),让每个 app 拥有自己独立的 urls.py。
2. 实现步骤
Step 1:在每个 app 目录下创建 urls.py 文件。
Step 2:在项目级 urls.py 中统一分发。
python
from django.contrib import admin from django.urls import path, include # 引入 include urlpatterns = [ path('admin/', admin.site.urls), path("app01/", include("app01.urls")), path("app02/", include("app02.urls")), ]
Step 3:在各自 app 的 urls.py 中定义自己的路由。
app01/urls.py:
python
from django.urls import path, re_path from app01 import views urlpatterns = [ re_path(r'^login/(?P<m>[0-9]{2})/$', views.index), ]
app02/urls.py:
python
from django.urls import path, re_path from app02 import views urlpatterns = [ re_path(r"^xxx/(?P<year>[0-9]{4})/$", views.xxx), ]
Step 4:在各自的 views.py 中编写视图函数。
匹配流程: 访问 /app01/login/12/ 时,项目级 urls.py 先匹配到 app01/,然后交给 app01/urls.py,再匹配 login/12/,最终调用 app01.views.index。
五、反向解析
1. 为什么需要反向解析?
随着功能增加,如果路由层的 URL 发生变化,就需要去修改视图层和模板层中所有硬编码的 URL,非常麻烦,不便维护。
反向解析的核心思想: 给路由起一个别名(name),在视图层和模板层通过别名动态解析出 URL。这样当 URL 规则改变时,只需修改 urls.py 一处,其他位置自动生效。
反向解析的典型应用场景:
-
模板中的超链接(
<a href="...">) -
表单的
action属性 -
视图中的重定向(
redirect)
2. 普通路径的反向解析
python
path("login1/", views.login, name="login")
python
from django.urls import reverse from django.shortcuts import redirect def login(request): return redirect(reverse("login"))
模板中:
html
<form action="{% url 'login' %}" method="post">
3. 无名分组的反向解析
python
re_path(r"^login/([0-9]{2})/$", views.login, name="login")
python
return redirect(reverse("login", args=(10,)))
模板中:
html
<form action="{% url 'login' 10 %}" method="post">
4. 有名分组的反向解析
python
re_path(r"^login/(?P<year>[0-9]{4})/$", views.login, name="login")
python
return redirect(reverse("login", kwargs={"year": 3333}))
模板中:
html
<form action="{% url 'login' year=3333 %}" method="post">
5. 三种情况对比
| 情况 | urls.py | views.py | 模板 |
|---|---|---|---|
| 普通路径 | name="login" |
reverse("login") |
{% url 'login' %} |
| 无名分组 | name="login" |
reverse("login", args=(10,)) |
{% url 'login' 10 %} |
| 有名分组 | name="login" |
reverse("login", kwargs={"year":3333}) |
{% url 'login' year=3333 %} |
六、命名空间
1. 为什么需要命名空间?
路由别名 name 没有作用域,Django 在反向解析 URL 时,会在项目全局顺序搜索。当查找到第一个匹配的路由别名时,立即返回。
问题: 如果不同 app 下定义了相同的路由别名 name="login",反向解析可能得到错误的 URL。
示例:
python
# app01/urls.py path("login/", views.login, name="login") # app02/urls.py path("login/", views.login, name="login")
此时 reverse("login") 只会返回第一个匹配的结果,无法区分是哪个 app 的。
解决方案: 使用命名空间(Namespace)。
2. 定义命名空间
include 中传入一个元组,格式为 ("app名称.urls", "app名称"):
python
urlpatterns = [ path("app01/", include(("app01.urls", "app01"))), path("app02/", include(("app02.urls", "app02"))), ]
3. 在 views.py 中使用命名空间
语法: reverse("app名称:路由别名")
python
return redirect(reverse("app01:login"))
4. 在模板中使用命名空间
语法: {% url "app名称:路由别名" %}
html
<form action="{% url 'app01:login' %}" method="post">
5. 命名空间的效果
| 调用方式 | 解析结果 |
|---|---|
reverse("app01:login") |
/app01/login/ |
reverse("app02:login") |
/app02/login/ |
即使两个 app 都定义了 name="login",通过命名空间也能准确区分,彻底解决别名冲突问题。
七、路由匹配流程图
八、路由设计最佳实践
-
使用 path 而非 re_path :除非确实需要正则匹配,否则优先使用
path,代码更简洁易读。 -
URL 以斜杠结尾 :Django 默认行为是
/index会重定向到/index/,保持统一风格。 -
使用路由分发:每个 app 管理自己的路由,项目级只负责分发。
-
给每条路由起 name:便于反向解析,避免硬编码 URL。
-
使用命名空间:多 app 项目中,命名空间能有效避免别名冲突。
-
优先使用有名分组:可读性更好,视图函数参数与 URL 组名对应清晰。
-
URL 命名规范 :使用小写字母和连字符,如
user-profile而非userProfile。
九、常见问题排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 404 错误 | URL 规则不匹配 | 检查 urlpatterns 中是否定义了该路径 |
NoReverseMatch |
反向解析时别名不存在或参数不匹配 | 检查 name 是否定义,参数数量与类型是否正确 |
| 反向解析结果错误 | 别名冲突 | 使用命名空间区分 |
re_path 不匹配 |
正则表达式缺少 ^ 和 $ |
补全首位限制符号 |
| 无名分组参数数量不符 | 视图形参数量与分组数量不一致 | 检查视图函数参数 |
include 报错 |
模块路径错误 | 检查 include() 中的字符串是否与目录结构一致 |
十、小结
本篇系统讲解了 Django 路由系统的核心内容:
-
版本差异 :
url()对应re_path(),新项目使用path+re_path。 -
正则分组:无名分组按位置传参,有名分组按关键字传参,推荐后者。
-
路由分发 :
include()让每个 app 管理自己的路由,项目结构更清晰。 -
反向解析 :通过
name别名动态解析 URL,避免硬编码,便于维护。 -
命名空间 :解决多 app 别名冲突问题,语法为
reverse("app:name")。
掌握路由系统后,你已经能够设计出清晰、可维护的 URL 结构。下一篇我们将进入 模板进阶,学习模板继承、包含、自定义标签与过滤器的更高级用法。