1. 程式人生 > Django入門教學 >10 Django 中傳遞引數給檢視函式

10 Django 中傳遞引數給檢視函式

Django 框架中推薦使用一個單獨的 python 模組配置 URL 和檢視函式或者檢視類的對映關係,通常稱這個配置模組為 URLconf,該 Python 模組通常命名為 urls.py。一般而言,每個應用目錄下都會有一個 urls.py 檔案,總的對映關係入口在專案目錄下的 urls.py 中,而這個位置又是在 settings.py 檔案中指定的。

本小節中將會學習 Django 中的路由系統、URLconf 的配置,以及如何將請求引數,如表單資料、檔案等傳遞給檢視函式。

1. 測試環境準備

這裡的實驗環境會採用前面建立的第一個 Django 工程(first_django_app) 來進行測試。在 first_django_app 中,我們建立了第一個 django app 應用:hello_app。現在按照如下步驟準備實驗環境:

在 hello_app 應用目錄下的 views.py 中新增一個檢視函式:

from django.shortcuts import render
from django.http import HttpResponse

# Create your views here.

def hello_view(request, *args, **kwargs):
    return HttpResponse('hello, world!')

在 hello_app 應用目錄下新建 urls.py 檔案,裡面內容如下:

from django.urls import path

from .
import views urlpatterns = [ # 資產查詢介面,根據我們自己的機器命名 path('world/', views.hello_view, name='hello_view'), ]

settings.py 檔案中註冊應用:

# first_django_app/settings.py
...

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes'
, 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 註冊應用 'hello_app' ] ...

在 URLconf 的總入口位置使用 django 提供的 include 方法將應用中的 urls.py 中的 urlpatterns 新增進來:

from django.contrib import admin
from django.conf.urls import include, url

# 所有url入口
urlpatterns = [
    url('admin/', admin.site.urls),
    url('hello/', include('hello_app.urls')),
]

這樣之後,我們請求地址 /hello/world/ 時,就能看到頁面顯示 “hello, world!” 字元了。後面的測試將在應用的 urls.pyviews.py 中進行。

2. Django 中的傳參方式

Django 中傳遞引數給檢視函式的方式主要可分為以下兩種形式:URL 傳參和非 URL 傳參兩種。第一種基於 Django 中的 URLconf 配置,可以通過 URL 路徑將對應匹配的引數傳到檢視函式中;而另外一種就是屬於HTTP 請求攜帶的引數了,請求引數可以放到 URL 中以 格式加到 URL 的最後面,也可以將引數放到請求 body 中,最後統一由檢視函式中的 request 引數儲存並傳到檢視函式中。

2.1 動態 URL 傳參

在 url 的路徑 (path)部分可以作為動態引數,傳遞給檢視函式,如下面幾種寫法:

# hello_app/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('articles/<int:year>/', views.year_archive),
    path('articles/<int:year>/<int:month>/', views.month_archive),
    path('articles/<int:year>/<int:month>/<slug:title>/', views.article_title),
]

注意上面的定義了匹配三個動態 URL 的對映,每個動態 URL 會匹配一個至多個引數,每個動態值使用 <> 符號匹配,採用 <type:name> 這樣的形式。我們對應的檢視函式如下:

from django.shortcuts import render
from django.http import HttpResponse

# Create your views here.

def year_archive(request, year, *args, **kwargs):
    return HttpResponse('hello, {} archive\n'.format(year))
    
def month_archive(request, year, month, *args, **kwargs):
    return HttpResponse('hello, month archive, year={}, moth={}!\n'.format(year, month))

def article_title(request, year, month, title, *args, **kwargs):
    return HttpResponse('hello, title archive, year={}, month={}, title={}!\n'.format(year, month, title))

對於動態的 URL 表示式中,匹配到的值,比如上面的 year,month 和 title 可以作為函式的引數放到對應的檢視函式中,Django 會幫我們把匹配到的引數對應的放到函式的引數上。這裡引數的位置可以任意寫,但是名字必須和 URL 表示式中的對應。

[root@server first_django_app]# curl http://127.0.0.1:8881/hello/articles/1998/
hello, 1998 archive
[root@server first_django_app]# curl http://127.0.0.1:8881/hello/articles/1998/12/
hello, month archive, year=1998, moth=12!
[root@server first_django_app]# curl http://127.0.0.1:8881/hello/articles/1998/12/test/
hello, title archive, year=1998, month=12, title=test

比如 URL 中有 3 個動態引數,在檢視函式中只寫上兩個引數接收也是沒問題的,因為剩下的引數會被傳到 kwargs 中以 key-value 的形式儲存:

(django-manual) [root@server first_django_app]# cat hello_app/views.py
...

def article_title(request, year, month, *args, **kwargs):
    return HttpResponse('hello, title archive, year={}, month={}, kwargs={}\n'.format(year, month, kwargs))

# 啟動服務,再次請求後
[root@server first_django_app]# curl http://127.0.0.1:8881/hello/articles/1998/12/test/
hello, title archive, year=1998, month=12, kwargs={'title': 'test'}

上述介紹的動態 URL 匹配格式 <type:name> 中,Django 會對捕捉到的 URL 引數進行強制型別裝換,然後賦給 name 變數,再傳到檢視函式中。其中 Django 框架中支援的轉換型別有:

  • str:匹配任意非空字元,不能匹配分隔符 “/”;

  • int:匹配任意大於0的整數;

  • slug:匹配任意 slug 字串, slug 字串可以包含任意的 ASCII 字元、數字、連字元和下劃線等;

  • uuid:匹配 UUID 字串;

  • path:匹配任意非空字串,包括 URL 的分隔符 “/”。

2.2 自定義URL引數型別轉換器

除了 Django 定義的簡單型別,我們還可以自定義引數型別轉換器來支援更為複雜的 URL 場景。比如前面的 int 型別並不支援負整數,我希望開發一個能匹配正負數的型別,具體的步驟如下:

hello_app/urls.py 中定義一個 SignInt 類。該類有一個固定屬性 regex,用於匹配動態值;兩個固定方法:to_python() 方法和 to_url() 方法:

# hello_app/urls.py

class SignInt:
    regex = '-*[0-9]+'

    def to_python(self, value):
        # 將匹配的value轉換成我們想要的型別
        return int(value)

    def to_url(self, value):
        # 反向生成url時候回用到
        return value

註冊該定義的轉換器,並給出一個簡短的名字,比如 sint:

# hello_app/urls.py

from django.urls import converters, register_converter

register_converter(SignInt, 'sint')

...

最後,我們就可以在 URLconf 中使用該型別來配置動態的 URL 表示式:

# hello_app/urls.py

urlpatterns = [
    path('articles/<sint:signed_num>/', views.signed_convert),
]

# hello_app/views.py
def signed_convert(request, signed_num, **kwargs):
    return HttpResponse('hello, 自定義型別轉換器,獲取引數={}\n'.format(signed_num))

啟動 Django 服務後,執行相關請求,可以看到 Django 的路由系統能成功匹配到帶負數和正數的 URL:

[root@server ~]# curl http://127.0.0.1:8881/hello/articles/1998/
hello, 自定義型別轉換器,獲取引數=1998
[root@server ~]# curl http://127.0.0.1:8881/hello/articles/-1998/
hello, 自定義型別轉換器,獲取引數=-1998

2.3 使用正則表示式

上面是比較簡單的 URLconf 配置形式,Django 框架中可以使用正則表示式來進一步擴充套件動態 URL 的配置,此時 urlpatterns 中的不再使用 path 方法而是支援正則表示式形式的 re_path 方法。此外,在 Python 的正則表示式中支援對匹配結果進行重新命名,語法格式為:(?P<name>pattern),其中 name 為該匹配的名稱,pattern 為匹配的正則表示式。 這樣我們可以有如下的 URLconf 配置:

# hello_app/urls.py
from django.urls import re_path

from . import views

urlpatterns = [
    re_path('articles/(?P<year>[0-9]{4})/', views.year_archive),
    re_path('articles/(?P<year>[0-9]{4})/(?P<month>0[1-9]|1[0-2])/', views.month_archive),
    re_path('articles/(?P<year>[0-9]{4})/(?P<month>0[1-9]|1[0-2])/(?P<title>[a-zA-Z0-9-_]+)/', views.article_title),
]

注意:這裡使用正則表示式的 URL 匹配和前面的普通的動態 URL 匹配有一個非常重要的區別,基於正則表示式的URL 匹配一旦匹配成功就會直接跳轉到檢視函式進行處理,而普通的動態 URL 匹配則會找到最長匹配的動態 URL,然後再進入相應的檢視函式去處理:

[root@server ~]# curl http://127.0.0.1:8881/hello/articles/1998/12/test
hello, 1998 archive

可以看到,這裡並沒有匹配到第三個 re_path 的 URL 配置,而是直接由第一個 re_path 的檢視函式進行了處理。

2.4 URLconf 傳遞額外引數

在前面的 URLconf 配置中,我們的 re_path 方法中只傳遞兩個引數,分別是設計的路由以及對應的檢視函式。我們可以看看 Django-2.2.10 中的 path 和 re_path 方法的原始碼:

# django/urls/conf.py
# ...

def _path(route, view, kwargs=None, name=None, Pattern=None):
    if isinstance(view, (list, tuple)):
        # For include(...) processing.
        pattern = Pattern(route, is_endpoint=False)
        urlconf_module, app_name, namespace = view
        return URLResolver(
            pattern,
            urlconf_module,
            kwargs,
            app_name=app_name,
            namespace=namespace,
        )
    elif callable(view):
        # view是函式
        pattern = Pattern(route, name=name, is_endpoint=True)
        return URLPattern(pattern, view, kwargs, name)
    else:
        raise TypeError('view must be a callable or a list/tuple in the case of include().')


path = partial(_path, Pattern=RoutePattern)
re_path = partial(_path, Pattern=RegexPattern)

可以看到,除了route 和 view 外,我們還有 name、kwargs、Pattern 引數(比較少用)。其中 name 引數表示的是 route 匹配到的 URL 的一個別名,而 kwargs 是我們可以額外傳給檢視函式的引數:

# hello_app/urls.py
...

urlpatterns = [
    re_path('articles/(?P<year>[0-9]{4})/', views.year_archive, {'hello': 'app'}),
]

# hello_app/views.py
def year_archive(request, *args, **kwargs):
    return HttpResponse('hello, year archive, 額外引數={}\n'.format(kwargs))

啟動 Django 服務後,我們請求對應的服務,可以看到除了 URL 中匹配的 year 引數外,還有 re_path 中額外傳遞的引數,最後都被檢視函式中的 **kwargs 接收:

[root@server ~]# curl http://127.0.0.1:8881/hello/articles/1998/
hello, year archive, 額外引數={'year': '1998', 'hello': 'app'}

2.5 從 HttpRequest 中獲取引數

從 HttpRequest 中獲取引數是我們進行 Web 開發中最常用的一種方式。對於 Django 的檢視函式來說,HTTP 請求的資料被 HttpRequest 例項化後傳到了檢視函式的第一個引數中。為了能觀察相關資訊,我們修改請求的檢視函式:

@csrf_exempt
def hello_view(request, *args, **kwargs):
    # 在第三次使用表單上傳包括檔案資料時,需要request.GET和request.POST操作,不然會拋異常
    params = "request.GET={}\n".format(request.GET)
    params += "request.POST={}\n".format(request.POST)
    params += "request.body={}\n".format(request.body)
    params += "request.FILES={}\n".format(request.FILES)

    return HttpResponse(params)

我們測試如下 3 種 HTTP 請求,分別為 GET 請求、POST 請求 和帶檔案引數的請求,結果如下:

[root@server ~]# curl -XGET "http://127.0.0.1:8881/hello/world/?a=xxxx&b=yyyy" 
request.GET=<QueryDict: {'a': ['xxxx'], 'b': ['yyyy']}>
request.POST=<QueryDict: {}>
request.body=b''
request.FILES=<MultiValueDict: {}>

[root@server ~]# curl -XPOST -d "username=shen&password=shentong" "http://127.0.0.1:8881/hello/world/?a=xxxx&b=yyyy" 
request.GET=<QueryDict: {'a': ['xxxx'], 'b': ['yyyy']}>
request.POST=<QueryDict: {'username': ['shen'], 'password': ['shentong']}>
request.body=b'username=shen&password=shentong'
request.FILES=<MultiValueDict: {}>

# 本次請求中,需要去掉request.GET和request.POST操作語句,不然請求會報錯
[root@server ~]# curl -XPOST -F "username=shen&password=shentong" "http://127.0.0.1:8881/hello/world/?a=xxxx&b=yyyy" -F "files=@/root/upload_file.txt"
request.body=b'------------------------------68c9ede00e93\r\nContent-Disposition: form-data; name="username"\r\n\r\nshen&password=shentong\r\n------------------------------68c9ede00e93\r\nContent-Disposition: form-data; name="files"; filename="upload_file.txt"\r\nContent-Type: text/plain\r\n\r\nupload file test\n\r\n------------------------------68c9ede00e93--\r\n'
request.FILES=<MultiValueDict: {'files': [<InMemoryUploadedFile: upload_file.txt (text/plain)>]}>

可以看到,跟在 “?” 後的引數資料會儲存到 request.GET 中,這也是 GET 請求帶引數的方式。對於 POST 請求的傳參,資料一般會儲存在 request.POSTrequest.body 中。對於最後傳送的上傳檔案請求,可以看到,檔案內容的內容資料是儲存到了 request.body 中。

3. 小結

本節內容我們主要介紹瞭如何向檢視函式傳送引數,包括兩種方式:

  1. 通過動態的 URLconf 配置傳遞引數到檢視函式中;

  2. 通過 http 請求帶引數傳遞給檢視函式。至此,Django 中的路由系統和檢視函式已經介紹完畢。接下來會介紹 Django 中的模板系統。