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_idredirect("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 都可能有 loginindexdetail 之类的路由名称。若不使用名称空间,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()

六、自定义路径转换器

自定义转换器需要提供 regexto_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,不是 regto_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 等更严格的依赖锁定方案。

八、实践要点

  1. 给每条可复用的路由设置唯一的 name,并在模板与视图中使用反向解析。
  2. App 内维护自己的 urls.py,项目级 urls.py 只负责挂载和全局入口。
  3. 存在同名路由时必须声明 app_name,使用 "app_name:url_name" 引用。
  4. 新项目优先使用 path() 转换器,复杂规则才使用 re_path()
  5. 每个项目使用独立虚拟环境,提交依赖清单,不提交虚拟环境目录和密钥文件。

掌握这些约定后,URL 的组织方式会随项目规模增长而保持清晰,也能让开发环境在不同机器上更稳定地复现。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐