1. 程式人生 > Django入門教學 >09 Django 檢視函式

09 Django 檢視函式

Django 中的檢視是 MTV 架構模式中的 V 層,主要處理客戶端的請求並生成響應資料返回。它其實類似於 MVC 架構模式中的 C 層,用於處理專案的業務邏輯部分。Django 的檢視函式就是 Django 專案中專門處理對應 path 的 python 函式,這也是整個專案開發中最核心的業務處理部分。當然,在 Django 中處理 path 路徑的不只有檢視函式(FBV),還有檢視類(CBV)。

1. Django 檢視中的 FBV 和 CBV

FBV 全稱是 function base views, 即在檢視中是使用函式來對應處理請求。如下示例:

# 在django中會對跨域請求做攔截處理,這裡使用@csrf_exempt註解表示不作處理,主要是針對POST請求
@csrf_exempt def hello_world(request, *args, **kwargs): if request.method == 'GET': return HttpResponse('Hello, get request', content_type="text/plain") elif request.method == 'POST': return HttpResponse('Hello, post request', content_type="text/plain") return HttpResponse(
"Hello, world.", content_type="text/plain") urlpatterns = [ path('admin/', admin.site.urls), path('hello/', hello_world), ]

注意: 檢視函式可以接受 request 引數,用於存放瀏覽器傳遞過來的所有資料。它是 WSGIRequest 類的一個例項,而 WSGIRequest 類又繼承自 HttpRequest 。我們通過 request 這個例項可以拿到客戶端 http 請求的方法,請求路徑、傳遞的引數以及上傳的檔案等等。

CBV 全稱是 class base views 就是在檢視中使用類處理請求。在 Django 中加入了 CBV 的模式,讓我們用類去處理響應,這樣做有以下幾個好處:

  • 可以使用面嚮物件的技術,比如 Mixin(多繼承),這樣可以有效複用增刪改查資料庫的程式碼;

  • 在檢視類中我們定義如 get 方法就能處理對應的 GET 請求,這樣無需使用 if 再進行判斷,一方面提高了程式碼的可讀性,另一方面也規範了 web api 的介面設計。

示例程式碼:

from django.http import HttpResponse
from django.views import View

class HelloView(View):
    
    def get(self, request, *args, **kwargs):  
        return HttpResponse('get\n')
      
    def post(self, request, *args, **kwargs):  
        return HttpResponse('post\n')
    
    def put(self, request, *args, **kwargs):  
        return HttpResponse('put\n')
    
    def delete(self, request, *args, **kwargs):  
        return HttpResponse('delete\n')
    
    # 注意,給CBV加上@csrf_exempt註解,需要加到dispatch方法上,後續會詳解介紹這個函式的作用
    @csrf_exempt
    def dispatch(self, request, *args, **kwargs):
        return super(HelloView, self).dispatch(request, *args, **kwargs)
    
urlpatterns = [
    path('admin/', admin.site.urls),
    path('hello/', HelloView.as_view()),
]

將包含這個檢視層的 django demo 工程啟動後,請求 /hello/ 路徑:

[root@server ~]# curl http://127.0.0.1:8881/hello/
get
[root@server ~]# curl -XPOST http://127.0.0.1:8881/hello/
post
[root@server ~]# curl -XPUT http://127.0.0.1:8881/hello/
put
[root@server ~]# curl -XDELETE http://127.0.0.1:8881/hello/
delete

可以看到,這樣封裝的檢視類對應的 web api 介面具有良好的 restful 風格,而且程式碼可讀性也非常好。 後面我們會深入學習這種檢視模式以及 Django 內部封裝的各種 view 類。

2. Django 中的 HttpRequest 類

上面我們初步接觸到了 HttpRequest 類,現在來詳細介紹下這個類及其相關屬性和方法。當 URLconf 檔案匹配到客戶端的請求路徑後,會呼叫對應的 FBV 或者 CBV,並將 HttpRequest 類的例項作為第一個引數傳入對應的處理函式中。那麼這個 HttpRequest 類有哪些常用的屬性和方法呢?

常用屬性

  • HttpRequest.scheme:請求的協議,一般為 http 或者 https;

  • HttpRequest.body:請求主體;

  • HttpRequest.path: 所請求 URL 的完整路徑,即去掉協議,主機地址和埠後的路徑;

  • HttpRequest.method:客戶端 HTTP 請求方法,如 GET、POST、PUT、DELETE等;

  • HttpRequest.GET: 返回一個 querydict 物件,該物件包含了所有的 HTTP 請求中 GET 請求的引數;

  • HttpRequest.POST: 返回一個 querydict 物件,該物件包含了所有的 HTTP 請求中 POST 請求的引數;

  • HttpRequest.COOKIES:返回一個包含了所有 cookies 的字典;

  • HttpRequest.FILES:返回一個包含所有檔案物件的字典。

常用方法

  • HttpRequest.get_host():返回客戶端發起請求的 IP + 埠;

  • HttpRequest.get_port():返回客戶端請求埠;

  • HttpRequest.get_full_path():返回請求的完整路徑,包括 “?” 後面所帶引數;

  • HttpRequest.get_raw_uri():返回完整的 uri 地址,包括了協議、主機和埠以及完整請求路徑;

  • HttpRequest.build_absolute_uri():通過 request 例項中的地址和變數生成絕對的 uri 地址。

示例程式碼

# 省略了import內容

def hello_world(request, *args, **kwargs):
    request_info = ""
    request_info += "request.scheme={}\n".format(request.scheme)
    request_info += "request.body={}\n".format(request.body)
    request_info += "request.path={}\n".format(request.path)
    request_info += "request.method={}\n".format(request.method)
    request_info += "request.GET={}\n".format(request.GET)
    request_info += "request.FILES={}\n".format(request.FILES)

    request_info += "request.get_host={}\n".format(request.get_host())
    request_info += "request.get_port={}\n".format(request.get_port())
    request_info += "request.get_full_path={}\n".format(request.get_full_path())
    request_info += "request.get_raw_uri={}\n".format(request.get_raw_uri())
    request_info += "request.build_absolute_uri={}\n".format(request.build_absolute_uri())

    return HttpResponse(request_info, content_type="text/plain")

urlpatterns = [
    path('admin/', admin.site.urls),
    path('hello/', hello_world),
]

我們啟動 Django 服務後,我們使用 curl 命令傳送 HTTP 請求如下:

# 準備一個新的檔案
[root@server ~]# cat upload_file.txt 
upload file test
[root@server ~]# curl -XPOST "http://127.0.0.1:8881/hello/?a=xxx&a=yyy&b=zzz" -F 'data={"name": "join", "age": 28}' -F "file=@/root/upload_file.txt"
request.scheme=http
request.body=b'------------------------------c28860e155fe\r\nContent-Disposition: form-data; name="data"\r\n\r\n{"name": "join", "age": 28}\r\n------------------------------c28860e155fe\r\nContent-Disposition: form-data; name="file"; filename="upload_file.txt"\r\nContent-Type: text/plain\r\n\r\nupload file test\n\r\n------------------------------c28860e155fe--\r\n'
request.path=/hello/
request.method=POST
request.GET=<QueryDict: {'a': ['xxx', 'yyy'], 'b': ['zzz']}>
request.FILES=<MultiValueDict: {'file': [<InMemoryUploadedFile: upload_file.txt (text/plain)>]}>
request.get_host=127.0.0.1:8881
request.get_port=8881
request.get_full_path=/hello/?a=xxx&a=yyy&b=zzz
request.get_raw_uri=http://127.0.0.1:8881/hello/?a=xxx&a=yyy&b=zzz
request.build_absolute_uri=http://127.0.0.1:8881/hello/?a=xxx&a=yyy&b=zzz

通過測試結果可以更容易理解 HttpRequest 類屬性和方法的含義。其中,上述 curl 請求中 -F 表示帶表單資料。

3. Django 檢視函式的返回值

對於檢視函式的返回值,往往有如下幾種方式:

3.1 直接返回字串

直接返回字串是非常常見的一種方式,不過我們需要將字串作為引數傳到 HttpResponse 類中例項化後返回:

def hello_world(request, *args, **kwargs):
    return HttpResponse('要返回的字串')

Tips:HttpResponse:是 Django 中封裝的用於返回響應的類 。

3.2 返回 json 型別

檢視函式直接返回 json 資料是在微服務架構中常見的套路。這裡 Django 程式只提供後端資料並不提供靜態資源。針對返回 json 資料,在 Django 中專門定義了一個 JsonResponse 類用來生成 json 資料。它實際上是繼承自 HttpResponse 類:

# django/http/response.py

# 忽略其他內容

class JsonResponse(HttpResponse):
    """
    忽略註釋部分內容
    """
    def __init__(self, data, encoder=DjangoJSONEncoder, safe=True,
                 json_dumps_params=None, **kwargs):
        if safe and not isinstance(data, dict):
            raise TypeError(
                'In order to allow non-dict objects to be serialized set the '
                'safe parameter to False.'
            )
        if json_dumps_params is None:
            json_dumps_params = {}
        kwargs.setdefault('content_type', 'application/json')
        data = json.dumps(data, cls=encoder, **json_dumps_params)
        super().__init__(content=data, **kwargs)

JsonResponse 類的使用和 HttpResponse 類一樣簡單,我們只需要把字典資料傳給 JsonResponse 類進行例項化即可。但是字典資料中存在中文時候,會出現亂碼,我們只需要在例項化 JsonResponse 時,多傳入一個引數即可:

# 在頁面上會出現亂碼
def hello_world(request, *args, **kwargs):
    data = {'code': 0, "content": "返回中文字串", "err_msg": ""}
    return JsonResponse(data)

# 經過處理後的JsonResponse
def hello_world(request, *args, **kwargs):
    data = {'code': 0, "content": "返回中文字串", "err_msg": ""}
    return JsonResponse(data, json_dumps_params={'ensure_ascii': False})

請求結果:

# 第一個不處理的 JsonResponse 返回
[root@server ~]# curl "http://127.0.0.1:8881/hello/"
{"code": 0, "content": "\u8fd4\u56de\u4e2d\u6587\u5b57\u7b26\u4e32", "err_msg": ""}

# 使用第二個資料處理後的HttpResponse返回
[root@server ~]# curl "http://127.0.0.1:8881/hello/"
{"code": 0, "content": "返回中文字串", "err_msg": ""}

另外一種比較好的方式是,仿照 JsonResponse 類,定義一個支援返回包含中文 json 資料的 Response 類:

# 忽略匯入模組

# 將原來支援的json_dumps_params引數固定寫死成{'ensure_ascii':False}
class JsonResponseCn(HttpResponse):
    """
    忽略註釋部分內容
    """
    def __init__(self, data, encoder=DjangoJSONEncoder, safe=True, **kwargs):
        if safe and not isinstance(data, dict):
            raise TypeError(
                'In order to allow non-dict objects to be serialized set the '
                'safe parameter to False.'
            )
        kwargs.setdefault('content_type', 'application/json')
        data = json.dumps(data, cls=encoder, **{'ensure_ascii':False})
        super().__init__(content=data, **kwargs)

這樣處理後,我們在和原來一樣使用 JsonResponseCn 類來返回 json 資料即可,不用考慮返回的字典資料中是否包含中文字元:

def hello_world(request, *args, **kwargs):
    data = {'code': 0, "content": "返回中文字串", "err_msg": ""}
    return JsonResponseCn(data)

3.3 呼叫 render 函式返回

HTML 文字或者模板檔案。其實,通過檢視 Django 的原始碼,可以看到 render 函式會呼叫 loader.render_to_string() 方法將 html 檔案轉成 string,然後作為 content 引數傳遞給 HttpResponse 類進行例項化:

# django/shortcuts.py

def render(request, template_name, context=None, content_type=None, status=None, using=None):
    """
    Return a HttpResponse whose content is filled with the result of calling
    django.template.loader.render_to_string() with the passed arguments.
    """
    content = loader.render_to_string(template_name, context, request, using=using)
    return HttpResponse(content, content_type, status)

它的用法如下:

# 第一步,在django工程中準備一個靜態檔案,放到templates目錄下
(django-manual) [root@server first_django_app]# cat templates/index.html 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>這是首頁</h1>
</body>
</html>

# 第二步,在first_django_app/setting.py檔案,指定靜態資源的目錄
(django-manual) [root@server first_django_app]# cat first_django_app/settings.py
...

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        # 指定專案目錄下的templates目錄
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

...

# 第三步,新增檢視函式以及URLconf,位置first_django_app/urls.py

def index(request, *args, **kwargs):
    return render(request, "index.html")

urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', index),
]

就這樣一個簡單的配置,我們請求 /index/ 路徑時,會返回 index.html 檔案內容:

[root@server ~]# curl "http://127.0.0.1:8881/index/"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>這是首頁</h1>
</body>
</html>

另一方面,index.html 還可以是一個模板檔案,我們通過 render 函式最後會將該模板檔案轉成一個完整的 HTML 檔案,返回給客戶端:

# index.html
(django-manual) [root@server first_django_app]# cat templates/index.html 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</body>
</html>

# 修改 urls.py 中的檢視函式
(django-manual) [root@server first_django_app]# cat first_django_app/urls.py
...

def index(request, *args, **kwargs):
    return render(request, "index.html", {"title":"首頁", "content": "這是正文"})
    
...

最後請求結果可以看到完整的 HTML 文字:

[root@server ~]# curl "http://127.0.0.1:8881/index/"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>首頁</h1>
<p>這是正文</p>
</body>
</html>

3.4 呼叫 redirect 函式

實現重定向功能,它的常用形式如下:

from django.shortcuts import redirect

# 指定完整的網址
def redirect_view1(request):
    # 忽略其他
    return redirect("http://www.baidu.com")

# 指定內部path路徑
def redirect_view2(request):
    # 忽略其他
  return redirect("/index/")

def redirect_view3(request):
    # 忽略其他
    return redirect(reverse('blog:article_list'))

def redirect_view4(request):
    # 忽略其他
    return redirect('some-view-name', foo='bar')

4. 給檢視加裝飾器

在 Django 工程中,由於檢視有兩種:FBV 和 CBV,因此檢視加裝飾器的情況也分為兩種:給檢視函式加裝飾器和給檢視類加裝飾器。由於檢視函式是普通的 Python 函式,因此給檢視函式加裝飾器和給普通函式加裝飾器方式一致。下面程式碼中我們給檢視函式 index 加了一個簡單的裝飾器,用於在執行檢視函式前和後各列印一段字元:

from django.shortcuts import render, HttpResponse, redirect

def wrapper(f):
    def innser(*args, **kwargs):
        print('before')
        ret = f(*args, **kwargs)
        print('after')
        return ret
    return innser
 
@wrapper
def index(request):
  return render(request, 'index.html')

由於類中的方法與普通函式不完全相同,因此不能直接將函式裝飾器應用於類中的方法 ,我們需要先將其轉換為方法裝飾器。Django 中提供了 method_decorator 裝飾器用於將函式裝飾器轉換為方法裝飾器:

# 定義函式裝飾器
def wrapper(f):
    def innser(*args, **kwargs):
        print('before')
        ret = f(*args, **kwargs)
        print('after')
        return ret
    return innser


# 另一個程式碼檔案中使用wrapper裝飾器
from django.views import View
from django.utils.decorators import method_decorator

@method_decorator(wrapper, name='get')
class HelloView(View):
    # @method_decorator(wrapper)
    def get(self, request, *args, **kwargs):  
        print('get')
        return HttpResponse('get\n')
      
    def post(self, request, *args, **kwargs):
        print('post')
        return HttpResponse('post\n')
    
    def put(self, request, *args, **kwargs): 
        print('put')
        return HttpResponse('put\n')
    
    def delete(self, request, *args, **kwargs): 
        print('delete')
        return HttpResponse('delete\n')
    
    @csrf_exempt
    # @method_decorator(wrapper)
    def dispatch(self, request, *args, **kwargs):
        return super(HelloView, self).dispatch(request, *args, **kwargs)

對於給 View 類新增裝飾器,我們有如下幾種性質:

  • method_decorator 裝飾器直接新增到 View 類上。第一個引數指定需要新增的裝飾器,如 wrapper,name 引數可以選擇將裝飾器 wrapper 作用到 View 類中的哪個函式。給類新增裝飾器時,必須要指定 name 引數,不然執行 Django 服務時會報錯

  • method_decorator 裝飾器可以直接作用於 View 類中的函式上。如果是在 get、post 這類 HTTP 請求方法的函式上,則裝飾器只會作用於呼叫該函式的 http 請求上;

  • 如果 method_decorator 裝飾器作用於 dispatch 函式上。由於對於檢視中所有的 HTTP 請求都會先呼叫 dispatch 方法找到對應請求方法的函式,然後再執行。所以這樣對應檢視內的所有 HTTP 請求方式都先經過裝飾器函式處理;

5. 小結

本小節中,我們介紹了檢視的兩種形式 FBV 和 CBV,並進行了簡單的說明。接下來我們詳細描述了 Django 中的 HttpRequest 和 HttpResponse 物件,並進行了程式碼說明。最後我們介紹了給 FBV 和 CBV 加上裝飾器的方法。