URL 分組¶
開始之前¶
任務目標
在這個章節中,我們會完成:
- 了解什麼是 URL namespace 及其重要性
- 使用
show_urls觀察專案的 URL 結構 - 為各個 app 設定 URL namespace
- 修正 templates 中的 URL 引用
- 掌握 URL 命名的最佳實踐
什麼是 URL Namespace?¶
URL namespace(命名空間)是 Django 用來組織和管理 URL 的機制。它讓我們可以用 app_name:url_name 的格式來引用 URL。
為什麼需要 Namespace?¶
想像一下這個情境:
# blog/urls.py
urlpatterns = [
path('list/', views.article_list, name='list'),
]
# shop/urls.py
urlpatterns = [
path('list/', views.product_list, name='list'),
]
兩個 app 都使用了 list 這個 URL name,會發生什麼事?
URL name 衝突
當多個 app 使用相同的 URL name 時:
- Django 無法確定
{% url 'list' %}要指向哪個頁面 - 後載入的 app 會覆蓋前面的設定
- 造成 URL 解析錯誤,頁面連結失效
這就是為什麼需要 namespace 來避免衝突!
Namespace 的好處¶
| 好處 | 說明 |
|---|---|
| 避免名稱衝突 | 不同 app 可以使用相同的 URL name |
| 清晰的組織 | 一眼就能看出 URL 屬於哪個 app |
| 易於維護 | 重構或移動 URL 時更容易管理 |
| 符合慣例 | Django 的最佳實踐 |
Django Admin 就是好例子
還記得我們在 template 中使用過的 {% url 'admin:index' %} 嗎?
這裡的 admin 就是 namespace,index 是 URL name。
Django Admin 使用 namespace 來確保它的 URL 不會與你的專案衝突!
觀察當前的 URL 結構¶
在開始設定 namespace 之前,讓我們先用 show_urls 指令來查看專案目前的 URL 結構。
執行 show_urls¶
你會看到類似這樣的輸出:
/ core.views.index index
/admin/ django.contrib.admin.sites.index admin:index
/admin/login/ django.contrib.admin.sites.login admin:login
/admin/logout/ django.contrib.admin.sites.logout admin:logout
/blog/articles/ blog.views.article_list article_list
/blog/articles/<int:article_id>/ blog.views.article_detail article_detail
/practices/hello/ practices.views.hello_world hello_world
/practices/greeting/ practices.views.greeting greeting
/practices/search/ practices.views.search search
分析輸出結果¶
注意到以下幾點:
- Admin 有 namespace:
admin:index、admin:login、admin:logout - Blog 沒有 namespace:只有
article_list、article_detail - Practices 沒有 namespace:只有
hello_world、greeting等
潛在問題
如果未來在其他 app 也使用了 article_list 這個 URL name,就會產生衝突!
例如:
blogapp 有article_listnewsapp 也想使用article_list- 兩者會互相覆蓋
使用 namespace 可以完全避免這個問題。
設定 URL Namespace¶
要為 app 設定 namespace,只需要在 urls.py 中加入 app_name 變數。
設定 Blog App 的 Namespace¶
修改 blog/urls.py:
from django.urls import path
from blog import views
app_name = "blog"
urlpatterns = [
path("articles/", views.article_list, name="article_list"),
path("articles/<int:article_id>/", views.article_detail, name="article_detail"),
]
就這麼簡單!只要加入一行 app_name = "blog"。
設定 Practices App 的 Namespace¶
同樣的,修改 practices/urls.py:
from django.urls import path
from practices import views
app_name = "practices"
urlpatterns = [
path("hello/", views.hello_world, name="hello_world"),
path("greeting/", views.greeting, name="greeting"),
path("search/", views.search, name="search"),
path("products/", views.product_list, name="product_list"),
path("products/filter/", views.filter_products, name="product_filter"),
path("hello/<str:name>/", views.hello_name, name="hello_name"),
path(
"articles/<int:year>/<int:month>/<slug:slug>/",
views.article_detail,
name="article_detail",
),
path(
"users/<str:username>/articles/",
views.user_articles,
name="user_articles",
),
path("advanced-search/", views.advanced_search, name="advanced_search"),
path("color-filter/", views.color_filter, name="color_filter"),
path("contact/", views.contact, name="contact"),
]
Namespace 命名慣例
通常 app_name 會設定成和 app 的資料夾名稱一樣:
blogapp →app_name = "blog"practicesapp →app_name = "practices"shopapp →app_name = "shop"
這樣可以讓程式碼更清晰易懂。
再次觀察 URL 結構¶
設定完 namespace 後,讓我們再次執行 show_urls 來觀察變化:
現在輸出會變成:
/ core.views.index index
/admin/ django.contrib.admin.sites.index admin:index
/admin/login/ django.contrib.admin.sites.login admin:login
/admin/logout/ django.contrib.admin.sites.logout admin:logout
/blog/articles/ blog.views.article_list blog:article_list
/blog/articles/<int:article_id>/ blog.views.article_detail blog:article_detail
/practices/hello/ practices.views.hello_world practices:hello_world
/practices/greeting/ practices.views.greeting practices:greeting
/practices/search/ practices.views.search practices:search
看到變化了嗎?
- 原本:
article_list、article_detail - 現在:
blog:article_list、blog:article_detail
所有 blog 的 URL 都有了 blog: 前綴!
同樣的,practices 的 URL 也都有了 practices: 前綴。
對比觀察
設定 namespace 前後的對比:
| App | 設定前 | 設定後 |
|---|---|---|
| Blog | article_list |
blog:article_list |
| Blog | article_detail |
blog:article_detail |
| Practices | hello_world |
practices:hello_world |
| Practices | search |
practices:search |
現在即使不同 app 使用相同的 URL name,也不會衝突了!
修正 Templates 中的 URL 引用¶
設定了 namespace 後,原本的 URL 引用會失效。我們需要更新所有使用這些 URL 的地方。
重要!
設定 namespace 後,原本的 URL 引用方式會失效:
如果忘記修正,頁面會出現 NoReverseMatch 錯誤!
修正 base.html¶
修改 templates/base.html:
{% load django_bootstrap5 %}
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Django Playground" />
<meta name="keywords" content="Django, Playground" />
<title>
{% block title %}
Django 大冒險
{% endblock title %}
</title>
{% bootstrap_css %}
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" />
{% block extra_head %}
{% endblock extra_head %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<span class="navbar-brand">Django 大冒險</span>
<button class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'blog:article_list' %}">文章列表</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}">管理後台</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container my-4">
{% block content %}
{% endblock content %}
</main>
<footer class="bg-light py-4 mt-5">
<div class="container text-center">
<p class="text-muted mb-0">
Django Playground
</p>
</div>
</footer>
{% bootstrap_javascript %}
{% block extra_scripts %}
{% endblock extra_scripts %}
</body>
</html>
修正 blog/base.html¶
修改 blog/templates/blog/base.html:
{% extends "base.html" %}
{% block content %}
<div class="row">
<aside class="col-md-3 mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">
文章管理
</h5>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<a href="{% url 'blog:article_list' %}" class="text-decoration-none">文章列表</a>
</li>
</ul>
</div>
</div>
</aside>
<div class="col-md-9">
{% block blog_content %}
{% endblock blog_content %}
</div>
</div>
{% endblock content %}
修正 article_detail.html¶
修改 blog/templates/blog/article_detail.html:
{% extends "blog/base.html" %}
{% block title %}
{{ article.title }} - Django 大冒險
{% endblock title %}
{% block blog_content %}
<nav aria-label="breadcrumb" class="d-none d-md-block">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'blog:article_list' %}">文章列表</a>
</li>
<li class="breadcrumb-item active">
{{ article.title }}
</li>
</ol>
</nav>
<article class="card">
<div class="card-body">
<h1 class="card-title fs-3 fs-md-2 fs-lg-1">
{{ article.title }}
</h1>
<div class="d-flex flex-column flex-md-row align-items-md-center mb-3 text-muted">
<span class="me-md-3 mb-2 mb-md-0">
<i class="bi bi-person"></i>
作者:{{ article.author.name }}
</span>
<span>
<i class="bi bi-calendar"></i>
發布時間:{{ article.created_at|date:"Y-m-d H:i" }}
</span>
</div>
{% if article.tags.exists %}
<div class="mb-3">
{% for tag in article.tags.all %}
<span class="badge bg-secondary me-1 mb-1">{{ tag.name }}</span>
{% endfor %}
</div>
{% endif %}
<hr />
<div class="article-content fs-6 fs-md-5">
{{ article.content|linebreaks }}
</div>
</div>
<div class="card-footer bg-light">
<div class="d-flex justify-content-between align-items-center">
<a href="{% url 'blog:article_list' %}"
class="btn btn-outline-secondary btn-sm">
<span class="d-none d-sm-inline">
<i class="bi bi-arrow-left"></i> 返回列表
</span>
<span class="d-inline d-sm-none">
<i class="bi bi-arrow-left"></i>
</span>
</a>
<small class="text-muted d-none d-md-inline">
最後更新:{{ article.updated_at|date:"Y-m-d" }}
</small>
</div>
</div>
</article>
{% endblock blog_content %}
修正 article_card.html¶
修改 blog/templates/blog/components/article_card.html:
<div class="col-12 col-sm-6 col-md-6 col-lg-4 col-xl-3 mb-4">
<div class="card h-100">
<div class="card-body d-flex flex-column">
<h5 class="card-title">
{{ article.title }}
</h5>
<h6 class="card-subtitle mb-2 text-muted">
{{ article.author.name }}
</h6>
<p class="card-text flex-grow-1">
{{ article.content|truncatewords:30 }}
</p>
</div>
<div class="card-footer bg-transparent">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
{{ article.created_at|date:"Y-m-d" }}
</small>
<a href="{% url 'blog:article_detail' article.id %}"
class="btn btn-primary btn-sm">
閱讀更多
</a>
</div>
</div>
</div>
</div>
修改總結
我們修改了四個檔案中的 URL 引用:
templates/base.html:導覽列的文章列表連結blog/templates/blog/base.html:側邊欄的文章列表連結blog/templates/blog/article_detail.html:麵包屑和返回按鈕blog/templates/blog/components/article_card.html:文章卡片的連結
所有的 URL 引用都從 'article_list' 改成 'blog:article_list',從 'article_detail' 改成 'blog:article_detail'。
測試修改結果¶
啟動開發伺服器¶
測試頁面連結¶
- 首頁導覽列:點擊「文章列表」連結
- 文章列表頁面:點擊任一文章的「閱讀更多」按鈕
- 文章詳情頁面:
- 點擊麵包屑中的「文章列表」連結
- 點擊「返回列表」按鈕
- 檢查側邊欄的「文章列表」連結
所有連結都應該正常運作
如果一切正常,所有連結都應該可以正常跳轉,不會出現任何錯誤。
如果出現 NoReverseMatch 錯誤
檢查是否有遺漏的 URL 引用沒有加上 namespace:
這表示某個 template 中還有 {% url 'article_list' %} 沒有改成 {% url 'blog:article_list' %}。
URL Namespace 的最佳實踐¶
1. 一定要使用 Namespace¶
建議做法
所有的 app 都應該設定 namespace,即使目前只有一個 app。
為什麼?
- 專案會成長,未來可能會新增更多 app
- 設定 namespace 是一勞永逸的事
- 避免日後重構的麻煩
- 符合 Django 的最佳實踐
2. Namespace 命名要一致¶
建議做法
使用和 app 名稱相同的 namespace:
避免這樣做
# blog/urls.py
app_name = "articles" # ❌ 和 app 名稱不一致,容易混淆
# shop/urls.py
app_name = "products" # ❌ 和 app 名稱不一致,容易混淆
一致的命名讓程式碼更容易理解和維護。
3. URL Name 也要遵循命名慣例¶
即使有了 namespace,URL name 也應該清楚明確:
# ✅ 好的命名
urlpatterns = [
path('articles/', views.article_list, name='article_list'),
path('articles/<int:article_id>/', views.article_detail, name='article_detail'),
path('articles/create/', views.article_create, name='article_create'),
]
# ❌ 不好的命名
urlpatterns = [
path('articles/', views.article_list, name='list'), # 太簡短
path('articles/<int:article_id>/', views.article_detail, name='detail'), # 太簡短
path('articles/create/', views.article_create, name='new'), # 不明確
]
命名建議
URL name 應該:
- 使用小寫字母和底線
- 包含資源名稱(如
article_) - 包含動作(如
list、detail、create、update、delete) - 清楚表達用途
常見的命名模式:
article_list:列表頁面article_detail:詳情頁面article_create:新增頁面article_update:編輯頁面article_delete:刪除頁面
4. 在 Templates 中一致使用 Namespace¶
建議做法
避免這樣做
常見問題¶
如果不想用 Namespace 可以嗎?¶
技術上可以,但強烈不建議。
不使用 namespace 的問題:
- URL name 容易衝突
- 難以維護大型專案
- 不符合 Django 最佳實踐
- 團隊協作時容易產生問題
可以在不同 app 使用相同的 URL name 嗎?¶
可以!這正是 namespace 的優勢。
# blog/urls.py
app_name = "blog"
urlpatterns = [
path('', views.index, name='index'), # blog:index
]
# shop/urls.py
app_name = "shop"
urlpatterns = [
path('', views.index, name='index'), # shop:index
]
有了 namespace,blog:index 和 shop:index 是完全不同的 URL,不會衝突。
如何在 Python 程式碼中使用 Namespace?¶
使用 reverse() 函式:
from django.urls import reverse
# 在 view 或其他地方
url = reverse('blog:article_list') # 取得 URL
url = reverse('blog:article_detail', kwargs={'article_id': 1}) # 帶參數
在 view 中重導向:
from django.shortcuts import redirect
def my_view(request):
# 重導向到文章列表
return redirect('blog:article_list')
任務結束¶
完成!
恭喜你完成了這個章節!現在你已經:
- 了解什麼是 URL namespace 及其重要性
- 使用
show_urls觀察專案的 URL 結構 - 為各個 app 設定 URL namespace
- 修正 templates 中的 URL 引用
- 掌握 URL 命名的最佳實踐
URL 命名空間的關鍵要點
- 一定要用 Namespace:所有 app 都應該設定
app_name - 命名要一致:
app_name使用和 app 相同的名稱 - URL name 要清楚:使用
resource_action格式(如article_list) - Templates 中使用完整格式:
{% url 'app:name' %} - 避免名稱衝突:這是使用 namespace 的主要目的
從現在開始,在建立新的 app 時,記得:
- 在
urls.py中設定app_name - 在 template 中使用
{% url 'app:name' %}格式 - 在 Python 程式碼中使用
reverse('app:name')
這些最佳實踐會讓你的 Django 專案更加健壯和易於維護!