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

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
在測試時,無論在瀏覽器輸入://127.0.0.1:8001/app01/index/還是輸入//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

瀏覽器輸入: //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_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'))),
]

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

測試

   

Tags: