Django URLs 路由設計與配置完全指南 | Django 教學
當使用者在瀏覽器輸入一個網址並按下 Enter,Django 如何知道該呼叫哪個 View 來處理這個請求?答案就是 URL 路由系統(URLconf)。URLconf 是 Django 請求處理流程的關鍵環節,它負責將傳入的 URL 對應到正確的 View 函數或類別。本篇文章將從 urlpatterns 基礎、path() 與 re_path() 函數、路徑轉換器(Path Converters)、include() 模組化路由、URL 命名 與 reverse() 反向解析(Reverse Resolution)、Namespace 命名空間 到 RESTful 風格 的 URL 設計最佳實踐,帶你全面掌握 Django 路由系統。
URL 配置基礎(URLconf)
Django 的路由核心是一個名為 urlpatterns 的 Python 列表,定義在專案的 urls.py 檔案中。當 HTTP 請求進來時,Django 從 settings.py 的 ROOT_URLCONF 設定所指定的模組載入 urlpatterns,然後由上而下依序嘗試匹配每一條路由規則。第一個匹配成功的規則會生效,後續規則不再比對。
# myproject/urls.py(ROOT_URLCONF 指向的模組)
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
path('hello/', views.hello_world),
]
這段程式碼做了兩件事:
- 將
/admin/開頭的 URL 交給 Django 內建的管理後台處理 - 將
/hello/這個 URL 對應到views.hello_world這個 View 函數
Django 如何匹配 URL?
Django 匹配 URL 時有幾個重要規則:
- 不匹配域名和 GET 參數:對
https://example.com/articles/?page=2這個請求,Django 只會拿articles/來比對 - 由上而下依序比對:匹配到第一個符合的規則就停止,因此路由的順序很重要
- 開頭不含斜線:
urlpatterns中的路由路徑不需要加上前導斜線,例如寫articles/而非/articles/
path() 函數詳解
path() 是 Django 路由系統最核心的函數,它接受四個參數:
path(route, view, kwargs=None, name=None)
| 參數 | 說明 | 是否必填 |
|---|---|---|
route | URL 匹配模式,例如 'articles/<int:pk>/' | 必填 |
view | 匹配成功後呼叫的 View callable | 必填 |
kwargs | 額外傳遞給 View 的關鍵字參數(dict) | 選填 |
name | URL 的命名,用於反向解析 | 選填 |
from django.urls import path
from . import views
urlpatterns = [
# 基本用法:route + view
path('articles/', views.article_list),
# 帶路徑參數:<int:pk> 會自動轉換為 int 型別
path('articles/<int:pk>/', views.article_detail),
# 帶額外參數:透過 kwargs 傳遞固定值給 View
path('blog/', views.article_list, kwargs={'blog_mode': True}),
# 帶命名:方便反向解析
path('articles/<int:pk>/', views.article_detail, name='article-detail'),
]
路徑轉換器(Path Converters)
path() 函數的一大優勢是內建的路徑轉換器,能自動將 URL 字串轉換為對應的 Python 型別。Django 提供了五種內建轉換器:
| 轉換器 | 匹配規則 | Python 型別 | 範例 |
|---|---|---|---|
str | 非 / 的任何非空字串(預設) | str | <str:name> |
int | 零或正整數 | int | <int:pk> |
slug | ASCII 字母、數字、連字號、底線 | str | <slug:slug> |
uuid | UUID 格式字串 | uuid.UUID | <uuid:uid> |
path | 包含 / 的任意非空字串 | str | <path:file_path> |
from django.urls import path
from . import views
urlpatterns = [
# int:匹配整數,自動轉為 Python int
path('users/<int:user_id>/', views.user_detail),
# slug:匹配 slug 格式的字串(如 my-first-post)
path('posts/<slug:post_slug>/', views.post_detail),
# uuid:匹配 UUID 格式(如 075194d3-6885-417e-a8a8-6c931e272f00)
path('items/<uuid:item_uuid>/', views.item_detail),
# path:匹配含有斜線的完整路徑
path('files/<path:file_path>/', views.serve_file),
]
自訂路徑轉換器
如果內建的轉換器無法滿足需求,你可以自訂。一個轉換器需要定義 regex 屬性、to_python() 方法和 to_url() 方法:
# converters.py
class FourDigitYearConverter:
regex = '[0-9]{4}' # 匹配規則(正規表達式)
def to_python(self, value):
return int(value) # URL 字串 → Python 型別
def to_url(self, value):
return '%04d' % value # Python 型別 → URL 字串
# urls.py
from django.urls import path, register_converter
from . import converters, views
# 註冊自訂轉換器,命名為 'yyyy'
register_converter(converters.FourDigitYearConverter, 'yyyy')
urlpatterns = [
# 使用自訂轉換器
path('articles/<yyyy:year>/', views.year_archive, name='year-archive'),
]
re_path() 正則表達式路由
對於 path() 無法處理的複雜匹配邏輯,Django 提供了 re_path(),它使用正規表達式(Regular Expression)來定義匹配規則:
from django.urls import re_path
from . import views
urlpatterns = [
# 匹配兩位數的月份(01-12)
re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>0[1-9]|1[0-2])/$',
views.month_archive),
# 匹配特定格式的產品編號(如 PRD-001)
re_path(r'^products/(?P<product_code>[A-Z]{3}-[0-9]{3})/$',
views.product_detail),
]
需要注意的是,re_path() 擷取到的參數一律為字串,不會自動做型別轉換,你需要在 View 中自行處理型別轉換。
原則:優先使用 path(),只在需要複雜匹配邏輯時才使用 re_path()。
include() 模組化路由
隨著專案規模成長,把所有路由擠在一個 urls.py 會變得難以維護。include() 函數讓你將路由拆分到各個 App 的 urls.py 中,實現模組化管理:
# myproject/urls.py(專案級別)
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', include('blog.urls')), # 將 /blog/ 開頭的 URL 交給 blog App 處理
path('accounts/', include('accounts.urls')), # 將 /accounts/ 開頭的 URL 交給 accounts App 處理
path('api/', include('api.urls')), # 將 /api/ 開頭的 URL 交給 api App 處理
]
# blog/urls.py(App 級別)
from django.urls import path
from . import views
app_name = 'blog' # 設定 App 命名空間
urlpatterns = [
path('', views.post_list, name='list'), # /blog/
path('<int:pk>/', views.post_detail, name='detail'), # /blog/1/
path('create/', views.post_create, name='create'), # /blog/create/
]
使用 include() 時,Django 會將已匹配的 URL 前綴去掉,把剩餘的部分傳給被引入的 URLconf 繼續匹配。例如對 /blog/1/ 這個請求,Django 先在專案級別匹配到 blog/,然後將 1/ 傳給 blog/urls.py 去比對。
URL 命名(name 參數)
為 URL 設定名稱是 Django 路由設計的重要實踐。有了名稱,你就不需要在程式碼中硬編碼 URL 字串,當路由路徑改變時,只需修改 urls.py 中的定義即可:
# 定義命名 URL
path('articles/<int:pk>/', views.article_detail, name='article-detail'),
命名的好處在下面的反向解析段落中會更加明顯。
reverse() 與 reverse_lazy()
反向解析(Reverse Resolution) 是指透過 URL 的名稱來取得實際的 URL 路徑,而不是硬編碼。Django 提供了 reverse() 函數來實現這個功能:
from django.urls import reverse
from django.shortcuts import redirect
def my_view(request):
# 基本用法
url = reverse('article-detail', kwargs={'pk': 1})
# 結果:'/articles/1/'
# 使用位置參數
url = reverse('article-detail', args=[1])
# 結果:'/articles/1/'
# 帶命名空間的反向解析
url = reverse('blog:detail', kwargs={'pk': 1})
# 結果:'/blog/1/'
# 常見用途:重導向到另一個頁面
return redirect(reverse('blog:list'))
reverse_lazy() 是 reverse() 的延遲求值(Lazy Evaluation)版本,它不會在匯入時就執行,而是在實際存取值時才進行解析。這在類別屬性中特別有用,因為類別定義時 URLconf 可能尚未載入完成:
from django.urls import reverse_lazy
from django.views.generic import CreateView
class ArticleCreateView(CreateView):
model = Article
fields = ['title', 'content']
# 使用 reverse_lazy() 而非 reverse()
# 因為類別屬性在模組載入時就會被求值
success_url = reverse_lazy('blog:list')
模板中的 {% url %} 標籤
在 Django 模板(Template)中,使用 {% url %} 標籤來做反向解析,效果等同於 Python 中的 reverse():
<!-- 基本用法 -->
<a href="{% url 'blog:list' %}">文章列表</a>
<!-- 帶關鍵字參數 -->
<a href="{% url 'blog:detail' pk=article.pk %}">{{ article.title }}</a>
<!-- 帶位置參數 -->
<a href="{% url 'blog:detail' article.pk %}">{{ article.title }}</a>
<!-- 搭配表單 action -->
<form method="post" action="{% url 'blog:create' %}">
{% csrf_token %}
<!-- 表單欄位 -->
</form>
使用 {% url %} 標籤而非硬編碼 URL,是 Django 開發的基本功,能確保模板中的連結在路由變更後仍然正確。
Namespace 命名空間(app_name)
當專案包含多個 App 時,不同 App 中可能出現相同名稱的 URL(例如 blog 和 shop 都有 detail)。命名空間(Namespace) 透過前綴來解決這個衝突問題。
Django 的命名空間分為兩種:
- App Namespace(應用命名空間):在 App 的
urls.py中用app_name變數宣告,是 App 的固定識別符 - Instance Namespace(實例命名空間):在
include()時透過namespace參數指定,用於同一個 App 被 include 多次的場景
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog' # App Namespace
urlpatterns = [
path('', views.post_list, name='list'),
path('<int:pk>/', views.post_detail, name='detail'),
]
# shop/urls.py
from django.urls import path
from . import views
app_name = 'shop' # App Namespace
urlpatterns = [
path('', views.product_list, name='list'),
path('<int:pk>/', views.product_detail, name='detail'),
]
# myproject/urls.py
from django.urls import path, include
urlpatterns = [
path('blog/', include('blog.urls')), # 反向解析:blog:list、blog:detail
path('shop/', include('shop.urls')), # 反向解析:shop:list、shop:detail
]
現在即使兩個 App 都有名為 list 和 detail 的 URL,透過命名空間也能明確區分。
URL 設計最佳實踐(RESTful 風格)
良好的 URL 設計能讓 API 和頁面路徑直觀、易讀、可預測。以下是遵循 RESTful 風格的設計原則:
- 使用名詞而非動詞:
/articles/而非/get-articles/ - 使用複數名詞:
/articles/而非/article/ - 利用 HTTP 方法表達操作:GET 取得、POST 建立、PUT/PATCH 更新、DELETE 刪除
- 使用階層結構表達資源關係:
/articles/1/comments/表示文章 1 的所有留言 - 結尾統一加斜線:Django 預設行為
APPEND_SLASH = True - 使用連字號而非底線:
/my-article/而非/my_article/
URL 路由效能考量
urlpatterns 列表由上而下匹配,有幾個效能相關的建議:
- 高頻路由放在前面:將最常被存取的路由放在列表開頭,減少不必要的比對
- 使用 include() 分組:可以提前排除不匹配的路由群組,減少逐一比對的數量
- 避免頂層使用 re_path():正規表達式的比對效能低於
path()的路徑轉換器
不過不需要過度擔心 — Django 會在首次請求時編譯所有 URLconf 並快取結果,後續請求不會重複編譯。
完整的 myblog URL 配置範例
讓我們用一個完整的部落格專案 myblog 來整合以上所有知識:
# myblog/urls.py(專案級別 ROOT_URLCONF)
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
# 首頁
path('', include('pages.urls')),
# 部落格文章(最頻繁存取,放在前面)
path('posts/', include('posts.urls')),
# 使用者帳號相關
path('accounts/', include('accounts.urls')),
# 管理後台(最少使用,放在最後)
path('admin/', admin.site.urls),
]
# 開發環境下提供媒體檔案服務
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# posts/urls.py(App 級別)
from django.urls import path
from . import views
app_name = 'posts'
urlpatterns = [
# 文章列表 — GET /posts/
path('', views.PostListView.as_view(), name='list'),
# 建立文章 — GET /posts/create/(表單)、POST /posts/create/(送出)
path('create/', views.PostCreateView.as_view(), name='create'),
# 文章詳情 — GET /posts/1/
path('<int:pk>/', views.PostDetailView.as_view(), name='detail'),
# 更新文章 — GET /posts/1/edit/(表單)、POST /posts/1/edit/(送出)
path('<int:pk>/edit/', views.PostUpdateView.as_view(), name='edit'),
# 刪除文章 — POST /posts/1/delete/
path('<int:pk>/delete/', views.PostDeleteView.as_view(), name='delete'),
# 依 slug 存取文章 — GET /posts/my-first-post/
path('<slug:slug>/', views.PostBySlugView.as_view(), name='detail-by-slug'),
# 依年份篩選文章 — GET /posts/archive/2026/
path('archive/<int:year>/', views.PostArchiveView.as_view(), name='archive-year'),
# 文章留言(巢狀路由)— /posts/1/comments/
path('<int:post_pk>/comments/', include('comments.urls')),
]
# comments/urls.py
from django.urls import path
from . import views
app_name = 'comments'
urlpatterns = [
# 留言列表 — GET /posts/1/comments/
path('', views.CommentListView.as_view(), name='list'),
# 新增留言 — POST /posts/1/comments/create/
path('create/', views.CommentCreateView.as_view(), name='create'),
# 刪除留言 — POST /posts/1/comments/5/delete/
path('<int:pk>/delete/', views.CommentDeleteView.as_view(), name='delete'),
]
# accounts/urls.py
from django.urls import path
from . import views
app_name = 'accounts'
urlpatterns = [
path('login/', views.LoginView.as_view(), name='login'),
path('logout/', views.LogoutView.as_view(), name='logout'),
path('register/', views.RegisterView.as_view(), name='register'),
path('profile/<int:pk>/', views.ProfileView.as_view(), name='profile'),
]
在模板中使用這些命名路由:
<!-- 導航列 -->
<nav>
<a href="{% url 'posts:list' %}">所有文章</a>
<a href="{% url 'posts:create' %}">撰寫新文章</a>
<a href="{% url 'accounts:profile' pk=request.user.pk %}">個人檔案</a>
<a href="{% url 'accounts:logout' %}">登出</a>
</nav>
<!-- 文章列表中的連結 -->
{% for post in posts %}
<article>
<h2><a href="{% url 'posts:detail' pk=post.pk %}">{{ post.title }}</a></h2>
<a href="{% url 'posts:edit' pk=post.pk %}">編輯</a>
</article>
{% endfor %}
在 View 中使用反向解析做重導向:
from django.urls import reverse
from django.shortcuts import redirect
def create_post(request):
if request.method == 'POST':
# 處理表單資料並儲存文章...
post = form.save()
# 重導向到新建立的文章頁面
return redirect(reverse('posts:detail', kwargs={'pk': post.pk}))
# ...
總結
Django 的 URL 路由系統(URLconf)是整個請求處理流程的核心環節。本篇文章涵蓋了路由設計的方方面面:urlpatterns 列表的匹配機制、path() 函數的四個參數、五種內建路徑轉換器與自訂轉換器、re_path() 正則表達式路由、include() 模組化拆分、URL 命名與 reverse() / reverse_lazy() 反向解析、模板中的 {% url %} 標籤、以及 Namespace 命名空間的應用。
掌握這些知識後,你應該能夠為任何規模的 Django 專案設計出清晰、可維護、符合 RESTful 風格的 URL 架構。記住幾個核心原則:優先使用 path() 而非 re_path()、永遠為 URL 命名並使用反向解析、善用 include() 和 Namespace 做模組化管理,以及遵循 RESTful 設計風格讓 URL 直觀可預測。