大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
URL 路由
这一课你将能做到
- 用
path()写出带类型转换器、可选参数和正则匹配的路由 - 用
name给路由命名,并在视图/模板中通过reverse()和{% url %}反查 URL - 用
include()把 URL 按 app 拆分,让项目根路由保持干净
为什么要学
URL 路由是 Web 框架的「门面」。用户输入什么地址,服务器执行什么代码,全由路由决定。Django 的路由系统虽然不像 Flask/FastAPI 那样用装饰器直接注册,但借助 urls.py 的显式声明,反而在大项目里更容易定位、排查和做 URL 全局替换。
对比 Spring Boot:Spring 用 @GetMapping("/users/{id}") 注解在控制器上声明;Django 把路由抽成独立的 urls.py,控制器(View)只写业务逻辑。两种思路:Java 是「控制器在哪里,路由就在哪里」;Django 是「路由集中管理,视图只管响应」。
先建立图像
请求 GET /articles/2024/python-orm/
│
▼
urls.py 逐条匹配
path("articles/<int:year>/<slug:slug>/", views.article_detail)
│
▼
views.article_detail(request, year=2024, slug="python-orm")
跟着做
步骤 1:基础 path 与转换器
blog/urls.py:
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.index, name="index"),
path("<int:year>/", views.year_archive, name="year_archive"),
path("<int:year>/<int:month>/", views.month_archive, name="month_archive"),
path("<int:year>/<int:month>/<slug:slug>/", views.article_detail, name="detail"),
]
blog/views.py:
from django.http import HttpResponse
def index(request):
return HttpResponse("首页")
def year_archive(request, year):
return HttpResponse(f"{year} 年归档")
def month_archive(request, year, month):
return HttpResponse(f"{year} 年 {month} 月归档")
def article_detail(request, year, month, slug):
return HttpResponse(f"文章:{year}/{month}/{slug}")
int 和 slug 是 Django 自带的 path converter:int 匹配正整数;slug 匹配 ASCII 字母/数字/连字符/下划线,常用于 URL 友好的文章标题缩写。
步骤 2:自定义转换器
如果内置转换器不够用,你可以自己写。例如:匹配 4 位年份:
# blog/converters.py
class FourDigitYearConverter:
regex = r"[0-9]{4}"
def to_python(self, value):
return int(value)
def to_url(self, value):
return f"{value:04d}"
注册:
# blog/urls.py
from django.urls import path, register_converter
from . import converters, views
register_converter(converters.FourDigitYearConverter, "yyyy")
app_name = "blog"
urlpatterns = [
path("archives/<yyyy:year>/", views.year_archive, name="year_custom"),
]
to_python 把 URL 段转成 Python 值;to_url 在反向解析时把值转成字符串。
步骤 3:命名 URL 与 reverse
路由一旦命名,你就可以在代码里不硬编码 URL:
from django.urls import reverse
from django.http import HttpResponseRedirect
def redirect_to_home(request):
# /blog/ 这个 URL 将来改成 /news/ 也不怕
url = reverse("blog:index")
return HttpResponseRedirect(url)
带参数:
reverse("blog:detail", kwargs={"year": 2024, "month": 8, "slug": "django-urls"})
# /blog/2024/8/django-urls/
模板里反查:
<a href="{% url 'blog:index' %}">首页</a>
<a href="{% url 'blog:detail' year=2024 month=8 slug='django-urls' %}">文章</a>
步骤 4:re_path 与正则路由
旧项目或需要复杂匹配时用 re_path():
from django.urls import re_path
urlpatterns = [
re_path(r"^articles/(?P<year>[0-9]{4})/$", views.year_archive),
re_path(r"^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$", views.month_archive),
]
注意 ^ 和 $ 不可省略,否则 /articles/2024/ 也会匹配 /articles/2024/extra-stuff/。
步骤 5:路由分发与 include
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")),
]
被 include 的 blog/urls.py 会获得 /blog/ 作为前缀。这样 app 只管自己的子路由,项目只管分发前缀。
include() 的第二个参数传 namespace 可以定义反向解析时的命名空间,但在被 include 的文件里定义 app_name 是更推荐的方式。
讲清楚要点
path()vsre_path():Django 2.0 起推荐path(),更直观、转换器自带类型检查。只有复杂正则需求才用re_path()。- 五个内置转换器:
str(默认,非空字符串不含/)、int、slug、uuid、path(匹配任意字符含/)。 - 命名空间避免撞车:
blog:index和shop:index不会冲突。不写app_name也能跑,但写项目级的include(namespace="...")做反向解析容易混淆。 reverse()不只是省事:它是 Django 路由「单一事实来源」思想的体现。URL 地址变一处,所有引用自动跟着变,不遗漏。- 路由匹配是「先来先服务」:
urlpatterns是有序列表,把更具体的路由放上面,通用路由放下面。放反了会导致特定路由永远匹配不上。 - 404 不是路由的责任:如果路由匹配成功但视图里查不到数据(比如 id=999 的文章),视图应该返回
Http404而非让路由背锅。
你可能会懵的地方
-
<int:pk>和<str:name>的顺序? Django 从上到下匹配,先匹配到的优先。如果同时有<str:name>/和<int:pk>/,把int放前面,否则int的值会被str吞掉。 -
reverse和resolve区别?reverse:给名字 + 参数 → 生成 URL 字符串。resolve:给 URL 字符串 → 反查哪个视图处理。一个正向生成,一个反向诊断。 -
path("", views.index)里的空字符串是什么意思? 表示「当前前缀到此结束」。例如被path("blog/", include("blog.urls"))分发后,blog.urls里的path("", ...)对应/blog/。 -
正则路由捕获的值都是字符串吗? 对。
re_path捕获后不会自动转 int,要在视图里自己int(year)。而path("<int:year>")会自动转。 -
能不能给路由加 HTTP 方法限制? Django 路由本身不限方法,在视图里根据
request.method分发。需要装饰器风格限制可用@require_http_methods(["GET", "POST"])(从django.views.decorators.http引入)。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
path("<int:year>/<slug:slug>") 无尾部 / | Django 默认重定向补 /,导致 301 | URL 统一以 / 结尾 |
reverse("detail", args=[2024, 8]) 少传参数 | TypeError | 用 kwargs 或确保 args 数量和顺序对 |
urlpatterns 里把通用路由放最前面 | 具体路由永远匹配不到 | 从具体到抽象,从窄到宽 |
re_path(r"articles/(?P<year>[0-9]+)") 没写 ^$ | 只匹配前缀,会吞子路径 | 始终加 ^ 和 $ |
app_name = "blog" 加在项目根 urls.py | app_name 属于 app 级路由 | 放在被 include 的 app/urls.py 里 |
卡点急救
NoReverseMatch异常:通常是名字拼错、参数缺失、或namespace没对上。先python manage.py show_urls(装django-extensions)查看所有路由和名字。- 访问带参数的 URL 返回 404:转换器没匹配上。比如
/blog/abc/走<int:year>匹配失败。检查 URL 实际输入的值。 include()之后子路由的app_name不生效:include()时传了namespace参数,但子 urls.py 没定义app_name,Django 会抛异常。两者必须配对使用。
面试常问
-
Django 路由和 Spring Boot 的路由注解有什么区别? Django 是集中式配置(
urls.py),路由与视图解耦,利于全局审计和批量替换。Spring Boot 是注解式(@GetMapping),路由跟着控制器走,直观但散落在类里。Django 更利于 URL 全局重构,Spring Boot 更利于读单个文件时一目了然。 -
如何实现一个必须匹配 4 位年份的路由? 最推荐自定义 converter(
FourDigitYearConverter),次选re_path(r"^articles/(?P<year>[0-9]{4})/$")。converter 的好处是可以在多个地方复用,且to_python保证类型安全。 -
Django 路由匹配的顺序是什么? 按
urlpatterns列表顺序从上到下,第一个匹配的 view 被调用。因此需要把更具体的规则放在前面,兜底规则(如 catch-all)放最后。
自测三问
- 内置的五个 path converter 分别是什么,适用场景?
- 为什么推荐给路由命名?
reverse()在项目中解决了什么问题? - 自定义 converter 时,
to_python和to_url分别在哪一步被调用?
小结一句话
Django 路由系统用显式的 urls.py 集中管理所有 URL 映射,配合命名、反查和 include() 分发,让大项目的 URL 全局替换和跨 app 复用变得安全。
延伸
- Django 官方文档 - URL dispatcher
- 下一课:视图层——FBV、CBV、Generic Views 的取舍。