路由的編寫方式是Django2.0和1.11最大的差別所在。Django官方迫于壓力和同行的影響,不得不将原來的正則比對表達式,改為更加簡單的path表達式,但依然通過re_path()方法保持對1.x版本的相容。
URL是Web服務的入口,使用者通過浏覽器發送過來的任何請求,都是發送到一個指定的URL位址,然後被響應。
在Django項目中編寫路由,就是向外暴露我們接收哪些URL的請求,除此之外的任何URL都不被處理,也沒有傳回。通俗地了解,不恰當的形容,URL路由是你的Web服務對外暴露的API。
Django奉行DRY主義,提倡使用簡潔、優雅的URL,沒有
.php
或
.cgi
這種字尾,更不會單獨使用0、2097、1-1-1928、00這樣無意義的東西,讓你随心所欲設計你的URL,不受架構束縛。
一、概述
URL路由在Django項目中的展現就是
urls.py
檔案,這個檔案可以有很多個,但絕對不會在同一目錄下。實際上Django提倡項目有個根
urls.py
,各app下分别有自己的一個
urls.py
,既集中又分治,是一種解耦的模式。
随便建立一個Django項目,預設會自動為我們建立一個
/project_name/urls.py
檔案,并且自動包含下面的内容,這就是項目的根URL:
"""dj_test URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]
前面一堆幫助性的文字,我們不用管,關鍵是預設導入了path方法和admin子產品,然後有一條指向admin背景的url路徑。
我們自己要編寫的url路由,基本也是這個套路。
二、Django如何處理請求
當使用者請求一個頁面時,Django根據下面的邏輯執行操作:
- 決定要使用的根URLconf子產品。通常,這是
設定的值,但是如果傳入的HttpRequest對象具有urlconf屬性(由中間件設定),則其值将被用于代替ROOT_URLCONF
設定。通俗的講,就是你可以自定義項目入口url是哪個檔案!ROOT_URLCONF
- 加載該子產品并尋找可用的urlpatterns。 它是
或者django.urls.path()
執行個體的一個清單。django.urls.re_path()
- 依次比對每個URL模式,在與請求的URL相比對的第一個模式停下來。也就是說,url比對是從上往下的短路操作,是以url在清單中的位置非常關鍵。
- 導入并調用比對行中給定的視圖,該視圖是一個簡單的Python函數(被稱為視圖函數),或基于類的視圖。 視圖将獲得如下參數:
- 一個HttpRequest 執行個體。
- 如果比對的表達式傳回了未命名的組,那麼比對的内容将作為位置參數提供給視圖。
- 關鍵字參數由表達式比對的命名組組成,但是可以被
的可選參數kwargs覆寫。django.urls.path()
- 如果沒有比對到任何表達式,或者過程中抛出異常,将調用一個适當的錯誤處理視圖。
三、簡單示例
先看一個例子:
from django.urls import path
from . import views
urlpatterns = [
path('articles/2003/', views.special_case_2003),
path('articles/<int:year>/', views.year_archive),
path('articles/<int:year>/<int:month>/', views.month_archive),
path('articles/<int:year>/<int:month>/<slug:slug>/', views.article_detail),
]
注意:
- 要捕獲一段url中的值,需要使用尖括号,而不是之前的圓括号;
- 可以轉換捕獲到的值為指定類型,比如例子中的int。預設情況下,捕獲到的結果儲存為字元串類型,不包含
這個特殊字元;/
- 比對模式的最開頭不需要添加
,因為預設情況下,每個url都帶一個最前面的/
,既然大家都有的部分,就不用浪費時間特别寫一個了。/
比對例子:
- /articles/2005/03/ 将比對第三條,并調用views.month_archive(request, year=2005, month=3);
- /articles/2003/比對第一條,并調用views.special_case_2003(request);
- /articles/2003将一條都比對不上,因為它最後少了一個斜杠,而清單中的所有模式中都以斜杠結尾;
- /articles/2003/03/building-a-django-site/ 将比對最後一個,并調用views.article_detail(request, year=2003, month=3, slug="building-a-django-site"
每當urls.py檔案被第一次加載的時候,urlpatterns裡的表達式們都将被預先編譯,這會大大提高系統處理路由的速度。
四、path轉換器
預設情況下,Django内置下面的路徑轉換器:
-
:比對任何非空字元串,但不含斜杠str
,如果你沒有專門指定轉換器,那麼這個是預設使用的;/
-
:比對0和正整數,傳回一個int類型int
-
:可了解為注釋、字尾、附屬等概念,是url拖在最後的一部分解釋性字元。該轉換器比對任何ASCII字元以及連接配接符和下劃線,比如slug
;building-your-1st-django-site
-
:比對一個uuid格式的對象。為了防止沖突,規定必須使用破折号,所有字母必須小寫,例如uuid
。傳回一個UUID對象;075194d3-6885-417e-a8a8-6c931e272f00
-
:比對任何非空字元串,重點是可以包含路徑分隔符’/‘。這個轉換器可以幫助你比對整個url而不是一段一段的url字元串。要區分path轉換器和path()方法。path
五、自定義path轉換器
其實就是寫一個類,并包含下面的成員和屬性:
- 類屬性regex:一個字元串形式的正規表達式屬性;
- to_python(self, value) 方法:一個用來将比對到的字元串轉換為你想要的那個資料類型,并傳遞給視圖函數。如果轉換失敗,它必須彈出ValueError異常;
- to_url(self, value)方法:将Python資料類型轉換為一段url的方法,上面方法的反向操作。
例如,建立一個converters.py檔案,與urlconf同目錄,寫個下面的類:
class FourDigitYearConverter:
regex = '[0-9]{4}'
def to_python(self, value):
return int(value)
def to_url(self, value):
return '%04d' % value
寫完類後,在URLconf 中注冊,并使用它,如下所示,注冊了一個yyyy:
from django.urls import register_converter, path
from . import converters, views
register_converter(converters.FourDigitYearConverter, 'yyyy')
urlpatterns = [
path('articles/2003/', views.special_case_2003),
path('articles/<yyyy:year>/', views.year_archive),
...
]
六、使用正規表達式
Django2.0的url雖然改‘配置’了,但它依然向老版本相容。而這個相容的辦法,就是用
re_path()
方法代替
path()
方法。
re_path()
方法在骨子裡,根本就是以前的
url()
方法,隻不過導入的位置變了。下面是一個例子,對比一下Django1.11時代的文法,有什麼太大的差别?
from django.urls import path, re_path
from . import views
urlpatterns = [
path('articles/2003/', views.special_case_2003),
re_path(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive),
re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<slug>[\w-]+)/$', views.article_detail),
]
與
path()
方法不同的在于兩點:
- year中比對不到10000等非四位數字,這是正規表達式決定的
- 傳遞給視圖的所有參數都是字元串類型。而不像
方法中可以指定轉換成某種類型。在視圖中接收參數時一定要小心。path()
七、URLconf比對URL中的哪些部分
請求的URL被看做是一個普通的Python字元串,URLconf在其上查找并比對。進行比對時将不包括GET或POST請求方式的參數以及域名。
例如,在
https://www.example.com/myapp/
的請求中,URLconf将查找
myapp/
。
在
https://www.example.com/myapp/?page=3
的請求中,URLconf也将查找
myapp/
URLconf不檢查使用何種HTTP請求方法,所有請求方法POST、GET、HEAD等都将路由到同一個URL的同一個視圖。在視圖中,才根據具體請求方法的不同,進行不同的處理。
八、指定視圖參數的預設值
有一個小技巧,我們可以指定視圖參數的預設值。 下面是一個URLconf和視圖的示例:
# URLconf
from django.urls import path
from . import views
urlpatterns = [
path('blog/', views.page),
path('blog/page<int:num>/', views.page),
]
# View (in blog/views.py)
def page(request, num=1):
# Output the appropriate page of blog entries, according to num.
...
在上面的例子中,兩個URL模式指向同一個視圖
views.page
。但是第一個模式不會從URL中捕獲任何值。 如果第一個模式比對,page()函數将使用num參數的預設值"1"。 如果第二個模式比對,page()将使用捕獲的num值。
九、自定義錯誤頁面
當Django找不到與請求比對的URL時,或者當抛出一個異常時,将調用一個錯誤處理視圖。Django預設的自帶的錯誤視圖包括400、403、404和500,分别表示請求錯誤、拒絕服務、頁面不存在和伺服器錯誤。它們分别位于:
- handler400 —— django.conf.urls.handler400。
- handler403 —— django.conf.urls.handler403。
- handler404 —— django.conf.urls.handler404。
- handler500 —— django.conf.urls.handler500。
這些值可以在根URLconf中設定。在其它app中的二級URLconf中設定這些變量無效。
Django有内置的HTML模版,用于傳回錯誤頁面給使用者,但是這些403,404頁面實在醜陋,通常我們都自定義錯誤頁面。
from django.contrib import admin
from django.urls import path
from app import views
urlpatterns = [
path('admin/', admin.site.urls),
]
# 增加的條目
handler400 = views.bad_request
handler403 = views.permission_denied
handler404 = views.page_not_found
handler500 = views.error
def bad_request(request):
return render(request, '400.html')
def permission_denied(request):
return render(request, '403.html')
def page_not_found(request):
return render(request, '404.html')
def error(request):
return render(request, '500.html')