Django 之路由層
一、Django 請求周期生命流程圖
首先,用戶在瀏覽器中輸入URL,發送一個GET 或 POST 方法的request 請求。
Django 中封裝了socket 的WSGI 伺服器,監聽埠接受這個request 請求。
再進行初步封裝,然後傳送到中間件中,這個request 請求再依次經過中間件。
對請求進行校驗或處理,再傳輸到路由系統中進行路由分發,匹配相對應的視圖函數(FBV)。
再將request 請求傳輸到views 中的這個視圖函數中,進行業務邏輯的處理。
調用modles 中表對象,通過ORM 拿到資料庫(DB)的數據。
同時拿到templates 中相應的模板進行渲染,然後將這個封裝了模板response 響應傳輸到中間件中。
依次進行處理,最後通過WSGI 再進行封裝處理,響應給瀏覽器展示給用戶。
二、Django 路由配置
路由簡單的來說就是根據用戶請求的 URL 連接來判斷對應的處理程式,並返回處理結果,也就是 URL 與 Django 的視圖建立映射關係。
Django 路由在 urls.py 配置,urls.py 中的每一條配置對應相應的處理方法。
urls.py 文件
from django.conf.urls import url
# 由一條條映射關係組成的urlpatterns這個列表稱之為路由表
urlpatterns = [
url(regex, view, kwargs=None, name=None), # url本質就是一個函數
]
#函數url關鍵參數介紹
# regex:正則表達式,用來匹配url地址的路徑部分,
# 例如url地址為://127.0.0.1:8001/index/,正則表達式要匹配的部分是index/
# view:通常為一個視圖函數,用來處理業務邏輯
# kwargs:略(用法詳見有名分組)
# name:略(用法詳見反向解析)
案例:
urls.py 文件
from django.conf.urls import url
from django.contrib import admin
from app import views # 導入模組views.py
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^index/$', views.index),
]
views.py 文件
# 導入HttpResponse,用來生成響應資訊
from django.shortcuts import render, HttpResponse
# 新增視圖函數index
def index(request):
return HttpResponse('index page')
測試
python manage.py runserver # 在瀏覽器輸入://127.0.0.1:8000/index/ 會看到 index page
1 注意一
在瀏覽器輸入:http: //127.0.0.1:8001/index/
,Django 會拿著路徑部分 index/ 去路由表(urls.py文件)中自上而下匹配正則表達式,一旦匹配成功,則會立即執行該路徑對應的視圖函數,也就是上面路由表(urls.py文件)中的 uelspatterns 列表中的url(‘^index/$’,views.index) 也就是views.py 視圖函數文件的index函數。
2 注意二
在瀏覽器中輸入:http: //127.0.0.1:8001/index
,Django 同樣會拿著路徑部分 index 去路由表中自上而下匹配正則表達式,看起來好像是匹配不到正則表達式(r’^index/$’ 匹配的是必須以/結尾,所以必會匹配到成功index),但是實際上我們依然在瀏覽器寬口中看到了 『index page』,其原因如下:
在配置文件 settings.py 中有一個參數 APPEND_SLASH,該參數有連個值True/False。
當APPEND_SLASH = True(如果配置文件中沒有該配置,則默認值為 True),並且用戶請求的 URL 地址的路徑部分不是以 / 結尾
例如請求的 URL 地址為:http: //127.0.0.1:8001/index,Django也會拿著部分地址 index 去路由表中匹配正則表達式,發現匹配不成功,那麼Django 會在路徑後加/(index/)在去路由表中匹配,去過還匹配不到,會返迴路徑找不到,如果匹配成功,則會返回重定向資訊給瀏覽器,要求瀏覽器重新向 http: //127.0.0.1:8001/index/ 地址返送請求。
當APPEND_SLASH = False時,則不會執行上述過程,即以但 URL 地址的路徑部分匹配失敗就立即返迴路勁未找到,不會做任何的附加操作。
# settings.py
APPEND_SLASH = False
三、路由分組
1 無名分組
urls.py 文件
from django.conf.urls import url
from django.contrib import admin
from app import views
urlpatterns = [
url(r'^admin/', admin.site.urls),
# 下述正則表達式會匹配url地址的路徑部分為:article/數字/
# 匹配成功的分組部分會以位置參數的形式傳給視圖函數,有幾個分組就傳幾個位置參數
url(r'^aritcle/(\d+)/$', views.article),
]
views.py 文件
from django.shortcuts import render, HttpResponse
# 需要額外增加一個形參用於接收傳遞過來的分組數據
def article(request,article_id):
return HttpResponse('id為 %s 的文章內容...' %article_id)
測試
python manage.py runserver
在瀏覽器輸入: //127.0.0.1:8000/article/1/ 會看到: id為 1 的文章內容…
2 有名分組
urls.py 文件
from django.conf.urls import url
from django.contrib import admin
from app import views
urlpatterns = [
url(r'^admin',admin.site.urls),
# 下面的正則表達式會匹配url地址的路徑部分為:article/數字/
# 匹配成功的分組部分會以 關鍵字參數(article_id=..)的形式傳給視圖函數
# 有幾個分組就傳幾個關鍵字參數
# (\d+)代表匹配數字1-無窮個
url(r'aritcle/(?P<article_id>\d+)/$', view.article),
]
views.py 文件
from django.shortcuts import render, HttpResponse
# 需要額外增加一個形參,形參名必須為article_id
def article(request, article_id):
return HttpResponse('id為 %s 的文章內容...' %article_id)
測試
python manage.py runserver
在瀏覽器輸入://127.0.0.1:8000/article/1/ 會看到: id為 1 的文章內容…
總結:有名分組和無名分組都是為了獲取路徑中的參數,並傳遞給視圖函數,區別在於無名分組是以位置參數的形式傳遞,有名分組是以關鍵字參數的形式傳遞。
強調:無名分組和有名分組不要混合使用。
四、路由分發
隨著項目功能的增加,app會越來越多,路由也越來越多,每個app都會有屬於自己的路由,如果再將所有的路由都放到一張路由表中,會導致結構不清晰,不便於管理,所以我們應該將app 自己的路由交由自己管理,然後在總路由表中做分發。
1 創建兩個 app,記得註冊
# 新建項目mystie
G:\src\django>django-admin startproject mysite
# 切換到項目目錄下
G:\src\django>cd mysite
# 創建app01和app02
G:\src\django\mysite>python manage.py startapp app01
G:\src\django\mysite>python manage.py startapp app02
2 在每個app下手動創建urls.py 來存放自己的路由
app01 下的urls.py 文件
from django.conf.urls import url
# 導入app01 的views
from app01 import views
urlpatterns = [
url(r'^index/$',views.index),
]
app01 下的views.py文件
from django.shortcuts import render, HttpResponse
def index(request):
return HttpResponse('我是app01 的index頁面...')
app02下 的urls.py文件
from django.conf.urls import url
# 導入app02的views
from app02 import views
urlpatterns = [
url(r'^index/$',views.index),
]
app02 下的views.py文件
from django.shortcuts import render, HttpResponse
def index(request):
return HttpResponse('我是app02 的index頁面...')
3 在總路由表的 urls.py 文件中(mysite文件夾下的 urls.py)
注意:總路由中,一級路由的後面千萬不加$符號,不然不能進行分發路由的操作,表示結束匹配。
from django.conf.urls import url, include
from django.contrib import admin
# 總路由表
# from app01 import urls as app01_urls
# from app02 import urls as app02_urls
urlpatterns = [
url(r'^admin/', admin.site.urls),
# 1.路由分發
# url(r'^app01/',include(app01_urls)), # 只要url前綴是app01開頭 全部交給app01處理
# url(r'^app02/',include(app02_urls)) # 只要url前綴是app02開頭 全部交給app02處理
# 新增兩條路由,注意不能以$結尾
# include 函數就是做分發操作的,當在瀏覽器輸入 //127.0.0.1:8001/app01/index/ 時
# 會先進入到總路由表中進行匹配,正則表達式 r'^app01/' 會先匹配成功路徑app01/
# 然後 include 功能會去 app01 下的urls.py 中繼續匹配剩餘的路徑部分
# 推薦使用
url(r'^app01/', include('app01.urls')),
url(r'^app02/', include('app02.urls')),
]
4 測試
python manage.py runserver
在瀏覽器輸入: //127.0.0.1:8000/app01/index/ 會看到:我是app01 的index頁面…
在瀏覽器輸入: //127.0.0.1:8000/app02/index/ 會看到:我是app02 的index頁面…
五、反向解析
在軟體開發初期,URL 地址的路徑設計可能並不完美,後期需要進行調整,如果項目中很多地方使用了該路徑,一旦該路徑發生變化,就意味著所有使用該路徑的地方都需要進行修改,這是一個非常繁瑣的操作。
解決方案就是在編寫一條 url(regex, view, kwargs=None, name=None) 時,可以通過參數name為 URL 地址的路徑部分起一個別名,項目中就可以通過別名來獲取這個路徑。以後無論路徑如何變化別名與路徑始終保持一致。
上述方案中通過別名獲取路徑的過程稱為反向解析。
案例:登錄成功跳轉到 index.html 頁面。
1 在mysite/urls.py文件
from django.conf.urls import url
from django.contrib import admin
from app01 import views
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^login/$', views.login, name='login_page'), # 路徑login/的別名為login_page
url(r'^index/$', views.index, name='index_page'), # 路徑index/的別名為index_page
]
2 在app01/views.py 文件
from django.shortcuts import render, HttpResponse, redirect, reverse # 用於反向解析
def login(request):
if request.method == 'GET':
# 當為get 請求時,返回login.html頁面,頁面中的 {% url 'login_page' %} 會被反向解析成路徑:/login/
return render(request, 'login.html')
# 當為post 請求時,可以從 request.POST 中取出請求體的數據
name = request.POST.get('name')
pwd = request.POST.get('pwd')
if name == 'xyz' and pwd == '123':
url = reverse('index_page') # reverse 會將別名 'index_page' 反向解析成路徑:/index/
return redirect(url) # 重定向到/index/
else:
return HttpResponse('用戶名或密碼錯誤')
def index(request):
return render(request, 'index.html')
3 templates\login.html 文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<!--強調: login_page 必須加引號-->
<form action="{% url 'login_page' %}" method="post">
<p>用戶名: <input type="text" name="name"></p>
<p>密 碼: <input type="password" name="pwd"></p>
<p><input type="submit" value="提交"></p>
</form>
</body>
</html>
4 templates\index.html 文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
<h3>index page</h3>
</body>
</html>
5 測試
python manage.py runserver
在瀏覽器輸入: //127.0.0.1:8000/login/ 會看到登錄頁面,輸入正確的用戶名密碼會跳轉到 index.html
當我們修改路由表中匹配路徑的正則表達式時,程式其餘部分均無需修改
6 總結
- 在 views.py 文件中,反向解析的使用:url = reverse(‘index_page’)
- 在模版 login.html 文件中,反向解析的使用:{% url ‘login_page’ %}
7 如果路徑存在分組的反向解析使用
from django.shortcuts import HttpResponse, render, reverse
from django.conf.urls import url
from django.contrib import admin
def index(request, args):
return HttpResponse('index page')
def user(request, uid):
return HttpResponse('user page')
def home(request):
# 無名
index = reverse('index_page', args=(1,))
# 有名
user = reverse('user_page', kwargs={'uid': 12})
# 簡寫
# user = reverse('user_page', args=(12,))
return render(request, 'home.html', locals())
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^$', home), # 首頁
url(r'^index/(\d+)/$', index, name='index_page'), # 無名分組
url(r'^user/(?P<uid>\d+)/$', user, name='user_page'), # 有名分組
]
8 templates\home.html 文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>home</title>
</head>
<body>
<h3>home page</h3>
<p>{{index}}</p>
<p>{{user}}</p>
<p>{% url 'index_page' 1%}</p>
<p>{% url 'user_page' 12%}</p>
<p>{% url 'user_page' uid=1 %}</p>
</body>
</html>
無名分組
反向解析出:/index/1/ 這種路徑,寫法如下在 views.py 中。
反向解析的使用
url = reverse('index_page', args=(1,))
在模版 login.html 文件中,反向解析的使用(1 是匹配\d+)
{% url 'index_page' 1 %}
有名分組
反向解析出:/user/1/ 這種路徑,寫法如下在 views.py 中。
反向解析的使用
url = reverse('user_page', kwargs={'uid':1})
在模版 login.html 文件中,反向解析的使用
{% url 'user_page' uid=1 %}
六、名稱空間
當我們的項目下創建了多個app,並且每個app下都針對匹配的路徑起了別名,如果別名存在重複,那麼在反向解析時則會出現覆蓋
from django.conf.urls import url
from app01 import views
urlpatterns = [
# 為匹配的路徑 app01/index/ 起別名 'index_page'
url(r'^index/$', views.index, name='index_page'),
]
from django.conf.urls import url
from app02 import views
urlpatterns = [
# 為匹配的路徑 app02/index/ 起別名 'index_page',與app01 中的別名相同
url(r'^index/$', views.index, name='index_page'),
]
2 在每個app下的view.py 中編寫視圖函數,在視圖函數中針對別名 ‘index_page‘ 做反向解析
app01 下的 views.py 文件
from django.shortcuts import render, HttpResponse, reverse
def index(request):
url = reverse('index_page')
return HttpResponse('app01 的index頁面,反向解析結果為%s' %url)
from django.shortcuts import render, HttpResponse, reverse
def index(request):
url = reverse('index_page')
return HttpResponse('app02 的index頁面,反向解析結果為%s' %url)
from django.conf.urls import url, include
from django.contrib import admin
# 總路由表
urlpatterns = [
url(r'^admin/', admin.site.urls),
# 新增兩條路由,注意不能以$結尾
url(r'^app01/', include('app01.urls')),
url(r'^app02/', include('app02.urls')),
]
python manage.py runserver
1 總urls.py 在路由分發時,指定名稱空間
from django.conf.urls import url, include
from django.contrib import admin
# 總路由表
urlpatterns = [
url(r'^admin/', admin.site.urls),
# 傳給include功能一個元組,元組的第一個值是路由分發的地址,第二個值則是我們為名稱空間起的名字
# url(r'^app01/', include(('app01.urls', 'app01'))),
# url(r'^app02/', include(('app02.urls', 'app02'))),
url(r'^app01/', include(('app01.urls', 'app01'), namespace='app01')),
url(r'^app02/', include(('app02.urls', 'app02'), namespace='app02'))
]
2 修改每個app下的view.py 中視圖函數,針對不同名稱空間中的別名 ‘index_page‘ 做反向解析
app01 下的views.py 文件
from django.shortcuts import HttpResponse
def index(request):
# 解析的是名稱空間app01 下的別名 'index_page'
url = reverse('app01:index_page')
return HttpResponse('app01 的index頁面,反向解析結果為%s' %url)
app02 下的views.py 文件
from django.shortcuts import HttpResponse
def index(request):
# 解析的是名稱空間app02下的別名'index_page'
url = reverse('app02:index_page')
return HttpResponse('app02 的index頁面,反向解析結果為%s' %url)
3 測試
python manage.py runserver
瀏覽器輸入: //127.0.0.1:8000/app01/index/ 反向解析的結果是 /app01/index/
瀏覽器輸入: //127.0.0.1:8000/app02/index/ 反向解析的結果是 /app02/index/
總結 + 補充
# 在視圖函數中基於名稱空間的反向解析,用法如下
url = reverse('名稱空間的名字:待解析的別名')
# 在模版里基於名稱空間的反向解析,用法如下
<a href="{% url '名稱空間的名字:待解析的別名'%}">index page</a>
# 其實只要保證名字不衝突 就沒有必要使用名稱空間
4 namespace 參數
在根目錄下的 urls.py 中使用了 include 方法,並且使用了 namespace 參數,如下圖
在啟動項目時,會報錯:’Specifying a namespace in include() without providing an app_name ‘
這是因為Django2 相對於Django1 做了改動,在include 函數里增加了參數 app_name,表示app的名字。
解決方法:
在 include 中傳入該app的名字(第二個參數),即
七、Django2.0 版的re_path與path
1 re_path
Django2.0 中的re_path與Django1.0 的URL一樣,傳入的第一個參數都是正則表達式
from django.urls import re_path # Django3.2 中的re_path
from django.conf.urls import url # Django3.2 中同樣可以導入1.0中的url
urlpatterns = [
# 用法完全一致
url(r'^app01/', include(('app01.urls','app01'))),
re_path(r'^app02/', include(('app02.urls','app02'))),
]
2 path
在Django2.0 中新增了一個path功能,用來解決:數據類型轉換問題與正則表達式冗餘問題
from django.shortcuts import HttpResponse,
from django.urls import path, re_path
urlpatterns = [
# 雖然path 不支援正則 但是它的內部支援五種轉換器
# 將第二個路由裡面的內容先轉成整型然後以 關鍵字 的形式傳遞給後面的視圖函數
path('index/<int:id>/', index)
]
# id 關鍵字參數
def index(request, id):
print(id, type(id))
return HttpResponse('index page')
強調
- path與re_path或者1.0中的url的不同之處是,傳給path的第一個參數不再是正則表達式,而是一個完全匹配的路徑。相同之處是第一個參數中的匹配字元均無需加前導斜杠。
- 使用尖括弧(<>)從url中捕獲值,相當於有名分組。
- <>中可以包含一個轉化器類型(converter type),比如使用 <int:name> 使用了轉換器int若果沒有轉化器,將匹配任何字元串,當然也包括了 / 字元。
- str 匹配除了路徑分隔符(/)之外的非空字元串,這是默認的形式0。
- int 匹配正整數,包含0。
- slug 匹配字母、數字以及橫杠、下劃線組成的字元串。
- uuid 匹配格式化的uuid,如 075194d3-6885-417e-a8a8-6c931e272f00。
- path 匹配任何非空字元串,包含了路徑分隔符(/)(不能用?)0。
例如
path('articles/<int:year>/<int:month>/<slug:other>/', views.article_detail)
針對路徑 //127.0.0.1:8000/articles/2009/123/info/
path 會匹配出參數 year=2009,month=123,other=’info’ 傳遞給函數 article_detail。
很明顯針對月份 month,轉換器int是無法精準匹配的,如果我們只想匹配兩個字元,那麼轉換器slug也無法滿足需求,針對等等這一系列複雜的需要,我們可以定義自己的轉化器。轉化器是一個類或介面,它的要求有三點:
- regex 類屬性,字元串類型。
- to_python(self, value) 方法,value是由類屬性 regex 所匹配到的字元串,返回具體的Python 變數值,以供Django 傳遞到對應的視圖函數中。
- to_url(self, value) 方法,和 to_python 相反,value是一個具體的Python 變數值,返回其字元串,通常用於 URL 反向引用。
自定義轉換器示例
在app01 下新建文件 path_ converters.py,文件名可以隨意命名
class MonthConverter:
regex = '\d{2}' # 屬性名必須為regex
def to_python(self, value):
return int(value)
def to_url(self, value):
return value # 匹配的regex是兩個數字,返回的結果也必須是兩個數字
在urls.py中,使用 register_converter 將其註冊到 URL 配置中
from django.urls import path,register_converter
from app01.path_converts import MonthConverter
register_converter(MonthConverter, 'mon')
from app01 import views
urlpatterns = [
path('articles/<int:year>/<mon:month>/<slug:other>/', views.article_detail, name='xxx'),
]
views.py 文件中的視圖函數 article_detail
from django.shortcuts import HttpResponse
def article_detail(request,year,month,other):
print(year, type(year))
print(month, type(month))
print(other, type(other))
print(reverse('xxx', args=(1988, 12, 'info'))) # 反向解析結果/articles/1988/12/info/
return HttpResponse('article detail page')
測試
- 在瀏覽器輸入 //127.0.0.1:8000/articles/2009/12/info/ path 會成功匹配出參數 year=2009,month=12(month必須兩位數),other=’info’ 傳遞給函數 article_detail。
- 在瀏覽器輸入 //127.0.0.1:8000/articles/2009/123/info/ path 會匹配失敗,因為我們自定義的轉換器mon只匹配兩位數字,而對應位置的123超過了2位。