Django 之路由層

語言: CN / TW / HK

一、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地址為:http://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 # 在瀏覽器輸入:http://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

在瀏覽器輸入: http://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

在瀏覽器輸入: http://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 函式就是做分發操作的,當在瀏覽器輸入 http://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

在瀏覽器輸入: http://127.0.0.1:8000/app01/index/ 會看到:我是app01 的index頁面...
在瀏覽器輸入: http://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')

3templates\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>

4templates\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

在瀏覽器輸入: http://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'),   # 有名分組
]

8templates\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下都針對匹配的路徑起了 別名 ,如果別名存在 重複 ,那麼在反向解析時則會出現 覆蓋

1 在每個app下手動建立urls.py 來存放自己的路由,並且為匹配的路徑起 別名

app01 下的urls.py 檔案

from django.conf.urls import url
from app01 import views

urlpatterns = [
    # 為匹配的路徑 app01/index/ 起別名 'index_page'
    url(r'^index/$', views.index, name='index_page'), 
]

app02 下的urls.py 檔案

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)

app02 下的views.py 檔案

from django.shortcuts import render, HttpResponse, reverse

def index(request):
    url = reverse('index_page')
    return HttpResponse('app02 的index頁面,反向解析結果為%s' %url)

3 在總的urls.py 檔案中(mysite 資料夾下的 urls.py)

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

在測試時,無論在瀏覽器輸入: http://127.0.0.1:8001/app01/index/ 還是輸入 http://127.0.0.1:8001/app02/index/  針對別名 ' index_page ' 反向解析的結果都是 /app02/index/ ,覆蓋了 app01 下別名的解析。

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

瀏覽器輸入: http://127.0.0.1:8000/app01/index/ 反向解析的結果是 /app01/index/

瀏覽器輸入: http://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

1re_path

Django2.0 中的 re_pathDjango1.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'))),
]

2path

在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)

針對路徑 http://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')

測試