Django 路由组织、名称空间与虚拟环境
Django 路由组织、名称空间与虚拟环境
本章围绕 Django 项目的 URL 设计展开:如何为动态路由反向生成地址,如何将不同 App 的路由拆分管理,如何用名称空间消除同名路由冲突,以及如何使用路径转换器约束参数。最后介绍虚拟环境与依赖清单,保证不同项目可以使用各自独立的 Python 包版本。
一、带参数路由的反向解析
动态路由会从 URL 中提取参数,例如 /students/5/ 中的 5。反向解析则根据“路由名称 + 参数”生成 URL,避免在模板或视图中硬编码地址。动态路由有参数时,反向解析必须提供所有必需参数。
新项目应优先使用 path() 的命名参数和转换器;re_path() 主要用于需要正则表达式的复杂规则。
# user/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("students/<int:student_id>/", views.student_detail, name="student-detail"),
]
# user/views.py
from django.http import HttpResponse
from django.shortcuts import redirect
from django.urls import reverse
def student_detail(request, student_id):
return HttpResponse(f"学生编号:{student_id}")
def go_to_student(request):
url = reverse("student-detail", kwargs={"student_id": 5})
return redirect(url)
代码说明:路由参数名是 student_id,因此使用 kwargs 反向解析时键也必须是 student_id。redirect("student-detail", student_id=5) 是更简洁的等价写法;reverse() 适合需要先获得 URL 字符串的场景。
模板中的参数反向解析
<!-- student 是模板上下文中的对象 -->
<a href="{% url 'student-detail' student_id=student.id %}">
查看学生
</a>
<!-- 也可以使用位置参数,但命名参数更清晰 -->
<a href="{% url 'student-detail' student.id %}">查看学生</a>
代码说明:若缺少动态参数,Django 会抛出 NoReverseMatch。模板中应使用 {% url %},而不是手写 /students/{{ student.id }}/,这样修改 URL 结构后引用位置不必同步改动。
二、re_path() 的命名与非命名捕获组
使用正则路由时,捕获组会作为参数传给视图。无名组按位置参数传递,命名组按关键字参数传递。为了可读性和维护性,通常应优先使用命名捕获组。
# user/urls.py
from django.urls import re_path
from . import views
urlpatterns = [
re_path(r"^legacy-pages/(\d+)/$", views.legacy_page, name="legacy-page"),
re_path(r"^named-pages/(?P<page>\d+)/$", views.named_page, name="named-page"),
]
# user/views.py
from django.http import HttpResponse
def legacy_page(request, page_number):
return HttpResponse(f"无名组参数:{page_number}")
def named_page(request, page):
return HttpResponse(f"命名组参数:{page}")
反向解析时,无名组使用 args,命名组可使用 args 或匹配名称的 kwargs。实际开发中建议命名组统一用 kwargs。
from django.urls import reverse
legacy_url = reverse("legacy-page", args=(5,))
named_url = reverse("named-page", kwargs={"page": 5})
print(legacy_url) # /legacy-pages/5/
print(named_url) # /named-pages/5/
代码说明:kwargs={"number": 5} 会失败,因为正则中定义的参数名是 page,不是 number。不要在同一条 re_path() 规则中混用命名和非命名捕获组,这会使视图调用方式不直观。
三、按 App 分发路由
小项目可以把所有路由写在项目的 urls.py 中,但随着 App 和页面增多,主路由文件会变得难以维护。推荐做法是:每个 App 管理自己的 urls.py,项目级路由用 include() 挂载 App 路由并统一添加前缀。
demo01/
├── demo01/
│ └── urls.py
├── shop/
│ └── urls.py
└── user/
└── urls.py
# demo01/urls.py:项目级路由
from django.contrib import admin
from django.urls import include, path
from . import views
urlpatterns = [
path("admin/", admin.site.urls),
path("", views.index, name="index"),
path("shop/", include("shop.urls")),
path("users/", include("user.urls")),
]
# shop/urls.py:商品或订单相关路由
from django.urls import path
from . import views
urlpatterns = [
path("orders/", views.order_list, name="order-list"),
path("buy/", views.buy, name="buy"),
]
# user/urls.py:用户相关路由
from django.urls import path
from . import views
urlpatterns = [
path("login/", views.login, name="login"),
path("register/", views.register, name="register"),
]
代码说明:include("shop.urls") 会把 shop/ 前缀与子路由拼接,因此 path("orders/", ...) 最终地址是 /shop/orders/。App 内部路由不应重复写项目级前缀,否则会形成 /shop/shop/orders/ 这类冗余路径。
四、应用名称空间
多个 App 都可能有 login、index、detail 之类的路由名称。若不使用名称空间,reverse("login") 无法明确指向哪一个 App,甚至可能因加载顺序而得到意外结果。
解决方法是在每个 App 的 urls.py 中声明 app_name,然后通过 "应用名:路由名" 引用。
# user/urls.py
from django.urls import path
from . import views
app_name = "user"
urlpatterns = [
path("login/", views.login, name="login"),
path("register/", views.register, name="register"),
]
# admin_portal/urls.py
from django.urls import path
from . import views
app_name = "admin_portal"
urlpatterns = [
path("login/", views.login, name="login"),
]
# demo01/urls.py
from django.urls import include, path
urlpatterns = [
path("users/", include("user.urls")),
path("admin-portal/", include("admin_portal.urls")),
]
from django.shortcuts import redirect
from django.urls import reverse
user_login_url = reverse("user:login")
admin_login_url = reverse("admin_portal:login")
# 也可直接重定向到命名空间路由
return redirect("user:login")
代码说明:名称空间解决的是 URL 名称冲突,不是 Python 模块或视图函数名称冲突。业务项目中,App 内路由名称可以保持简洁,例如都命名为 login,再用名称空间表达所属业务域。
指定实例名称空间
同一份 URLconf 被挂载多次时,可以使用实例名称空间区分不同挂载位置。普通项目中只需 app_name 即可;以下写法用于理解 include() 的三元组形式。
# demo01/urls.py
from django.urls import include, path
urlpatterns = [
path("staff/", include(("user.urls", "user"), namespace="staff")),
path("customers/", include(("user.urls", "user"), namespace="customers")),
]
from django.urls import reverse
reverse("staff:login")
reverse("customers:login")
代码说明:实例名称空间适用于同一个应用以不同前缀或配置重复挂载的情况。对于每个 App 只挂载一次的项目,使用 app_name 和常规 include("app.urls") 更简单。
五、path() 路径转换器
路径转换器让 path() 同时完成匹配和类型转换,比手写简单正则更易读。转换器参数都会按关键字传给视图函数,参数名必须与视图形参匹配。
| 转换器 | 匹配规则 | 传给视图的类型 |
|---|---|---|
str |
不含 / 的非空字符串 |
str |
int |
0 或正整数 | int |
slug |
ASCII 字母、数字、-、_ |
str |
uuid |
带连字符的 UUID | uuid.UUID |
path |
可包含 / 的非空字符串 |
str |
# order/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("names/<str:name>/", views.name_detail, name="name-detail"),
path("orders/<int:order_id>/", views.order_detail, name="order-detail"),
path("articles/<slug:slug>/", views.article_detail, name="article-detail"),
path("files/<path:file_path>/", views.file_detail, name="file-detail"),
]
# order/views.py
from django.http import HttpResponse
def order_detail(request, order_id):
assert isinstance(order_id, int)
return HttpResponse(f"订单编号:{order_id}")
def name_detail(request, name):
return HttpResponse(f"名称:{name}")
代码说明:path 转换器会匹配斜杠,因此应将含有 <path:...> 的路由放在更具体的路由之后,避免它过早吞掉后续路径。int 只匹配非负整数;若需要负数、固定长度或其他复杂格式,可使用自定义转换器或 re_path()。
六、自定义路径转换器
自定义转换器需要提供 regex、to_python() 和 to_url()。前者定义 URL 中允许出现的文本,to_python() 将匹配结果转为视图使用的值,to_url() 则在反向解析时把 Python 值转为 URL 字符串。
# order/converters.py
class FourDigitYearConverter:
regex = r"[0-9]{4}"
def to_python(self, value):
return int(value)
def to_url(self, value):
return f"{int(value):04d}"
# order/urls.py
from django.urls import path, register_converter
from . import views
from .converters import FourDigitYearConverter
register_converter(FourDigitYearConverter, "year")
urlpatterns = [
path("reports/<year:report_year>/", views.report, name="report"),
]
# order/views.py
from django.http import HttpResponse
def report(request, report_year):
return HttpResponse(f"报告年份:{report_year}")
from django.urls import reverse
print(reverse("report", kwargs={"report_year": 2024}))
# /reports/2024/
代码说明:转换器属性名必须是 regex,不是 reg。to_url() 需要返回与正则规则匹配的字符串;例如年份传入 42 时会格式化为 0042,仍符合四位数字规则。
七、虚拟环境与依赖管理
虚拟环境不是对现有环境的“备份”,而是为项目创建隔离的 Python 解释器与第三方包目录。它允许项目 A 使用 Django 3.2,而项目 B 使用 Django 5.x,彼此不互相卸载或覆盖依赖。
使用 venv 创建环境
# 在项目根目录创建虚拟环境
python -m venv .venv
# Windows PowerShell 激活
.venv\Scripts\Activate.ps1
# macOS / Linux 激活
source .venv/bin/activate
# 确认当前解释器与 Django 来源
python -c "import sys; print(sys.executable)"
python -m django --version
代码说明:激活后再使用 python -m pip install ... 安装包,可确保依赖进入当前项目的虚拟环境。.venv/ 通常应加入 .gitignore,不要提交整个环境目录。
导出与安装依赖
将可复现的依赖版本写入 requirements.txt,其他开发者或部署环境即可使用同一份清单安装依赖。
# 导出当前虚拟环境中已安装的包与版本
python -m pip freeze > requirements.txt
# 根据清单安装依赖
python -m pip install -r requirements.txt
一个简化的依赖文件示例如下:
Django==3.2.12
PyMySQL==1.1.1
代码说明:pip freeze 会列出当前环境中的全部包,适合学习项目或简单部署。生产项目还应定期审查依赖、修复安全漏洞,并根据团队实践选择 pip-tools、Poetry、uv 等更严格的依赖锁定方案。
八、实践要点
- 给每条可复用的路由设置唯一的
name,并在模板与视图中使用反向解析。 - App 内维护自己的
urls.py,项目级urls.py只负责挂载和全局入口。 - 存在同名路由时必须声明
app_name,使用"app_name:url_name"引用。 - 新项目优先使用
path()转换器,复杂规则才使用re_path()。 - 每个项目使用独立虚拟环境,提交依赖清单,不提交虚拟环境目录和密钥文件。
掌握这些约定后,URL 的组织方式会随项目规模增长而保持清晰,也能让开发环境在不同机器上更稳定地复现。
更多推荐
所有评论(0)