跳轉到

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

uv run manage.py 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

分析輸出結果

注意到以下幾點:

  1. Admin 有 namespaceadmin:indexadmin:loginadmin:logout
  2. Blog 沒有 namespace:只有 article_listarticle_detail
  3. Practices 沒有 namespace:只有 hello_worldgreeting

潛在問題

如果未來在其他 app 也使用了 article_list 這個 URL name,就會產生衝突!

例如:

  • blog app 有 article_list
  • news app 也想使用 article_list
  • 兩者會互相覆蓋

使用 namespace 可以完全避免這個問題。

設定 URL Namespace

要為 app 設定 namespace,只需要在 urls.py 中加入 app_name 變數。

設定 Blog App 的 Namespace

修改 blog/urls.py

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

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 的資料夾名稱一樣:

  • blog app → app_name = "blog"
  • practices app → app_name = "practices"
  • shop app → app_name = "shop"

這樣可以讓程式碼更清晰易懂。

再次觀察 URL 結構

設定完 namespace 後,讓我們再次執行 show_urls 來觀察變化:

uv run manage.py 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_listarticle_detail
  • 現在:blog:article_listblog: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 引用方式會失效:

{% url 'article_list' %}  <!-- 會出現錯誤! -->
{% url 'blog:article_list' %}  <!-- 正確的方式 -->

如果忘記修正,頁面會出現 NoReverseMatch 錯誤!

修正 base.html

修改 templates/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

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

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

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 引用:

  1. templates/base.html:導覽列的文章列表連結
  2. blog/templates/blog/base.html:側邊欄的文章列表連結
  3. blog/templates/blog/article_detail.html:麵包屑和返回按鈕
  4. blog/templates/blog/components/article_card.html:文章卡片的連結

所有的 URL 引用都從 'article_list' 改成 'blog:article_list',從 'article_detail' 改成 'blog:article_detail'

測試修改結果

啟動開發伺服器

uv run manage.py runserver

測試頁面連結

  1. 首頁導覽列:點擊「文章列表」連結
  2. 文章列表頁面:點擊任一文章的「閱讀更多」按鈕
  3. 文章詳情頁面
  4. 點擊麵包屑中的「文章列表」連結
  5. 點擊「返回列表」按鈕
  6. 檢查側邊欄的「文章列表」連結

所有連結都應該正常運作

如果一切正常,所有連結都應該可以正常跳轉,不會出現任何錯誤。

如果出現 NoReverseMatch 錯誤

檢查是否有遺漏的 URL 引用沒有加上 namespace:

NoReverseMatch at /blog/articles/
Reverse for 'article_list' not found.

這表示某個 template 中還有 {% url 'article_list' %} 沒有改成 {% url 'blog:article_list' %}

URL Namespace 的最佳實踐

1. 一定要使用 Namespace

建議做法

所有的 app 都應該設定 namespace,即使目前只有一個 app。

# ✅ 好的做法
app_name = "blog"

urlpatterns = [
    path('articles/', views.article_list, name='article_list'),
]
# ❌ 不好的做法
urlpatterns = [
    path('articles/', views.article_list, name='article_list'),  # 沒有 namespace
]

為什麼?

  • 專案會成長,未來可能會新增更多 app
  • 設定 namespace 是一勞永逸的事
  • 避免日後重構的麻煩
  • 符合 Django 的最佳實踐

2. Namespace 命名要一致

建議做法

使用和 app 名稱相同的 namespace:

# blog/urls.py
app_name = "blog"  # ✅ 和 app 名稱一致

# shop/urls.py
app_name = "shop"  # ✅ 和 app 名稱一致

避免這樣做

# 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_
  • 包含動作(如 listdetailcreateupdatedelete
  • 清楚表達用途

常見的命名模式:

  • article_list:列表頁面
  • article_detail:詳情頁面
  • article_create:新增頁面
  • article_update:編輯頁面
  • article_delete:刪除頁面

4. 在 Templates 中一致使用 Namespace

建議做法

<!-- ✅ 總是使用完整的 namespace:name 格式 -->
{% url 'blog:article_list' %}
{% url 'blog:article_detail' article.id %}
{% url 'shop:product_list' %}

避免這樣做

<!-- ❌ 不要混用有無 namespace 的方式 -->
{% url 'blog:article_list' %}  <!-- 有 namespace -->
{% url 'admin:index' %}  <!-- 有 namespace -->
{% url 'home' %}  <!-- 沒有 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:indexshop: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 命名空間的關鍵要點

  1. 一定要用 Namespace:所有 app 都應該設定 app_name
  2. 命名要一致app_name 使用和 app 相同的名稱
  3. URL name 要清楚:使用 resource_action 格式(如 article_list
  4. Templates 中使用完整格式{% url 'app:name' %}
  5. 避免名稱衝突:這是使用 namespace 的主要目的

從現在開始,在建立新的 app 時,記得:

  1. urls.py 中設定 app_name
  2. 在 template 中使用 {% url 'app:name' %} 格式
  3. 在 Python 程式碼中使用 reverse('app:name')

這些最佳實踐會讓你的 Django 專案更加健壯和易於維護!