Django RESTful 系列教程(三)(上)

你好 Django REST Framework

在第二章,我們學習了 REST 開發(fā)的基本知識,并且在沒有借助任何框架的情況下
完成了我們的 RESTful APP 的開發(fā),雖然我們已經考慮到了許多的情況,但是我們的 APP
依然有許多的漏洞。在本章,我們將會進入 VueDjango REST framework
的學習。本章將會分為三個部分,分別是:

  • 你好 Django REST Framework
  • 你好 Vue
  • 重構 APP

這就是我們的三個部分了。第一個部分學習 DRF ,第二個部分學習 Vue ,最后一個部分為實戰(zhàn)部分。在上部分,我們會學習以下知識點:

  1. 了解 DRF 的基本使用。
  2. 了解并能靈活使用序列化器。

這個部分的知識點看起來很少,其實等大家真正進入他們的學習中時,會發(fā)現其中的知識點也不少。當然,這是一個教程,不是 DRF 官方文檔復讀機,所以一旦在看教程的過程中有什么不懂的地方,去查詢 DRF 文檔是個好習慣。同時,本章也會涉及 python 的編程知識,由此可見,對于 web 后端的開發(fā)來說,語言的基礎是多么重要。同樣的,如果遇到在自己查資料之后還不懂的地方,評論留言或者提 ISSUE

準備工作

首先,我們需要安裝 DRF ,在終端中運行:

pip install djangorestframework

創(chuàng)建一個新的項目:

python django-admin.py startproject api_learn

把路徑切換到項目路徑內,創(chuàng)建一個新的 APP :

python manage.py startapp rest_learn

編輯你的 settings.py 文件,把我們的 APP 和 DRF 添加進去:

INSTALLED_APPS = [
    ...
    'rest_framework',
    'rest_learn'
]

編輯 rest_learnmodels.py

from django.db import models
class TestModel(models.Model):
    name = models.CharField(max_length=20)
    code = models.TextField()
    created_time = models.DateTimeField(auto_now_add=True)
    changed_time = models.DateTimeField(auto_now=True)

    def __str__(self): 
        return self.name

DateTimeField 是時間與日期字段,其中的參數意思是:

  • auto_now: 在實例每次被保存的時候就更新一次值,在這里,我們把它作為修改值。所以 changed_time 字段的值會隨著實例的每次修改和保存而發(fā)生變化,這樣就可以記錄實例的修改時間。
  • auto_now_add: 在實例被創(chuàng)建的時候就會自動賦值。created_time 的值就會在實例被創(chuàng)建時自動賦值,這樣我們就可以記錄實例是在什么時候被創(chuàng)建的。

將我們的模型添加進管理站點。
編輯 rest_learn 下的 admin.py:

from django.contrib import admin
from .models import TestModel

@admin.register(TestModel)
class TestModelAdmin(admin.ModelAdmin):
    pass

admin.register 是一個將 ModelAdmin 快速注冊模型到管理站點的裝飾器,其中的參數
是需要被注冊的模型。

編輯 api_learn 下的 urls.py

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

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^drf-auth/',include('rest_framework.urls'))
]

第二個 url 配置用于提供登錄的功能。

在項目路徑下運行:

python manage.py makemigrations
python migrate

生成我們需要的數據庫文件。

創(chuàng)建管理員。在終端中運行:

(root) λ python manage.py createsuperuser
Username (leave blank to use 'administrator'): admin
Email address:
Password:
Password (again):
Superuser created successfully.

在項目路徑下創(chuàng)建兩個空文件,分別是 data.pyrest_test.py

data.py 主要用來為我們提供一些虛擬數據。

編輯 data.py

from django import setup
import os
os.environ.setdefault('DJAGNO_SETTINGS_MODULE','api_learn.settings') # 在環(huán)境變量中設置配置文件
setup() # 加載配置文件

from rest_learn.models import TestModel

data_set = {
    'ls':"""import os\r\nprint(os.listdir())""",
    'pwd':"""import os\r\nprint(os.getcwd())""",
    'hello world':"""print('Hello world')"""
}
for name, code in data_set.items():
    TestModel.objects.create(name=name,code=code)

print('Done')

直接運行 data.py ,當看到 Done 的時候就說明數據寫入已經完成了。細心的同學已經發(fā)現了,這和我們第一章的 單文件 Django 很相似。記得我們在第一章說過的嗎:

使用前先配置

我們需要使用 Django 的模型,所以需要先配置它,在這里我們選擇了使用第一章中的第二種方法來配置。如果你忘了第一章講了些什么,快回去看看吧。這也是我們先講 單文件 Django 原因所在,知道了配置方法之后,我們就不需要再打開 shell 十分不方便的編寫代碼了。

為了確保萬無一失,大家可以選擇登錄進后臺看看我們的數據是否寫入了數據庫。

本節(jié)剩下所有的代碼都會在 rest_test.py 中進行,所以先編寫好配置。

編輯 rest_test.py

from django import setup
import os

# 加載配置
os.environ.setdefault('DJAGNO_SETTINGS_MODULE','api_learn.settings')
setup()


準備工作已經完成了,讓我們正式的開始學習。

你好 Django REST Framework

在上一章的結尾我們知道我們的 APP 其實是不安全的,因為我們并沒有對上傳的數據進行任何的檢查,這使得我們的應用隨時暴露在被攻擊的安全隱患之下。同時,由于我們的應用十分的小,我們并沒有考慮到其它的“內容協(xié)商”,如果在今后的應用中我們需要用到 xml 格式的數據,那么我們又需要重新編寫一次我們的代碼。我們的應用代碼不僅不安全,同時也不靈活。這一次,我們需要解決這個問題。

序列化器

剛才我們說道,我們需要對上傳的數據進行檢查,按照一般的思路,我們一般會編寫一大堆的 if 語句來判斷上傳的數據是否符合要求。用腳指頭都可以知道這么做是最最笨的方法。再好一點的辦法是編寫一些工具函數來檢查數據是否符合要求,比如我們的 name 字段的長度是小于 20 個字符的,數據類型是字符串。那我可以單獨編寫一個這樣的函數:

def validate_name(post_data):
    name = post_data.get('name')
    if isinstance(name, str):
        if len(name) <= 20:
            return name
    raise Exception('Invalid name data.')

這樣我們直接在視圖邏輯中直接調用這個函數就可以了。這個比單獨寫 if 語句要好一些了。但是依然會有不少問題,如果中途我們的 name 字段的長度被修改為 30 個字符了,那我們是不是要再改一次我們的 validate_name 函數呢?要是我們的 name 字段被修改成了 code_name ,那我們是不是還要再改一次呢?每一次的改動都會牽扯到 validate_name 的改動。 更要命的是,如果我們的數據類型發(fā)生了變化,由于前端傳過來的數據都是字符串,我們需要對數據進行轉換才可以保存到數據庫中,這樣就加大了我們的工作量。那有沒有更好的辦法呢?有,那就是我們的 Serializer ,也就是序列化器。

序列化器是什么?看它的名字我們就知道了,它是用來序列化數據的,我們在第二章知道了什么是數據的“序列化”,同時它也提供了對數據的驗證功能,更棒的是,這個數據驗證是雙向的。也就是說,它既可以驗證前端上傳到后端的數據,它也可以驗證后端傳到前端的數據。前者比較好理解,那后者怎么理解呢?比如,我們前端需要的 created_time 字段的日期的格式為 '月-日-年' ,那么我們就可以在序列化器中寫好,提前做好格式轉換的工作,把驗證通過數據傳給前端。所以,我們序列化器的處理流程大概是這樣的:

client ----> views <-----> serializer <-----> model

以及,序列化器還可以規(guī)定前端可以修改哪些字段,前端可以知道哪些字段。我們只希望客戶端修改 namecode 兩個字段,有的人可能會偷偷上傳 created_time 字段,要是我們沒有對它做攔截,我們的字段就會被隨意修改啦,這樣可不太好。

說的很抽象,我們來實際練習一下。接下來的所有代碼都是在 rest_test.py 中進行的,大家接著剛才的代碼寫就行了。如果你對這些代碼有任何的別扭的感覺,或者是“心頭堵的慌”的感覺,或者是產生了任何“這樣做好麻煩啊”之類的想法,請忍住,到后面你就明白了。

from rest_framework import serializers

class TestSeriOne(serializers.Serializer):
    name = serializers.CharField(max_length=20)

這樣我們就創(chuàng)建好了一個序列化器。對 Django Form 很熟悉的同學或許已經發(fā)現了,這不就很像是 Django 表單的寫法嗎?是的,事實上,序列化器用的就是表單的邏輯,所以如果你熟悉 Django Form 的 API ,那你上手序列化器也會很快。同時,序列化器和表單一樣,擁有很多的字段,在之后的章節(jié)中我們會慢慢學習到它們,現在我們對字段的了解就先知道一點是一點。我們來使用一下我們的序列化器。
接著在下面寫:

frontend_data = {
        'name':'ucag',
        'age':18
    }

test = TestSerilOne(data=frontend_data)
if test.is_valid():
    print(test.validated_data)

我們假設有一個前端上傳的數據為 frontend_data ,然后我們使用序列化器來驗證上傳的數據。它的使用方法和表單一樣,想要獲得合法的數據需要先運行 .is_valid() 方法,在運行這個方法后,如果驗證通過,合法的數據就會被保存在 .validated_data 屬性中。現在直接運行我們的 rest_test.py 腳本試試。不出意外的話,你會看到這個結果:

OrderedDict([('name', 'ucag')])

我們可以看到,age 字段被序列化器給過濾掉了,這樣就可以防止前端上傳一些奇奇怪怪的字段了。我們把剛才的序列化器修改一下,改成這個樣子:

class TestSerilOne(serializers.Serializer):
    name = serializers.CharField(max_length=20)
    age = serializers.IntegerField()

我們新增加了一個字段。把我們的 frontend_data 改成這個樣子:

frontend_data = {
        'name':'ucag',
        'age':'18'
    }

其它什么都不變,運行 rest_test.py ,輸出變成了這樣:

OrderedDict([('name', 'ucag'), ('age', 18)])

好像沒什么不一樣。。。再仔細看看?看看 age 變成了什么?我們傳進去的數據是個字符串,但是在經過驗證之后,它的類型就變成了整數型!讓我們來看看,故意給它傳錯誤的數據會發(fā)生什么。
frontend_data 改成這樣:

frontend_data = {
        'name':'ucag',
        'age':'ucag'
    }

把之前的測試改成這樣:

test = TestSerilOne(data=frontend_data)
if not test.is_valid():
    print(test.errors)

輸出應該是這樣的:

{'age': ['A valid integer is required.']}

序列化器把不符合要求的字段的錯誤信息給放在了 .errors 屬性里。我們可以通過這個屬性來查看相應的錯誤信息,在前端上傳的數據出錯的時候我們就可以直接把這個錯誤直接發(fā)送給前端,而不用自己手寫錯誤信息了。

剛剛體驗的是驗證前端的數據,現在我們來看看序列化器是怎么驗證后端數據的。假設前端現在只想知道 name 字段的信息,比如我們之前 APP 項目的代碼列表,我們需要顯示的僅僅就是代碼片段的名字。現在就需要對后端數據做驗證了。

注釋掉剛才做實驗的代碼,接著在下面再創(chuàng)建一個序列化器:

# test = TestSerilOne(data=frontend_data)
# if not test.is_valid():
#     print(test.errors)

class TestSerilTwo(serializers.Serializer):
    name = serializers.CharField(max_length=20)

現在我們來使用它來驗證后端的數據,在下面接著寫:

from rest_learn.models import TestModel
code = TestModel.objects.get(name='ls')
test = TestSerilTwo(instance=code)
print(test.data)

運行 rest_test.py ,你的輸出會是這樣的:

{'name': 'ls'}

我們從模型中獲取了一個模型實例,然后通過 instance 參數把它放進了序列化器里,然后,我們通過 .data 屬性來訪問驗證之后的數據。可以看到,只有 name 字段被提取了出來,codecreated_timechanged_time 字段都沒有出現在提取之后的數據里。真的是很方便呀,那我想提取所有的模型實例該怎么辦呢?因為前端的代碼列表需要的是所有實例的名字信息啊。把我們之前做驗證的代碼改成這樣:

from rest_learn.models import TestModel
codes = TestModel.objects.all()
test = TestSerilTwo(instance=codes,many=True)
print(test.data)

你會看到輸出是這個樣子的:

[OrderedDict([('name', 'hello world')]), OrderedDict([('name', 'pwd')]), OrderedDict([('name', 'ls')])]

此時的 .data 屬性變成了一個列表。我們提取了所有的模型實例,通過 instance 參數傳遞進了序列化器,通過 many=True 參數設置告訴序列化器我們傳進去的是一個查詢集實例,這樣序列化器就會自己做相應的處理了。是不是特別方便?

到目前為止,我們的序列化器都是一個個字段手寫出來的,通常,我們序列化的字段和模型的字段是統(tǒng)一的,那能不能通過模型來生成我們的序列化器呢,就像模型表單那樣?當然是可以的。
注釋掉之前的驗證代碼,接著在后面寫:

# from rest_learn.models import TestModel
# codes = TestModel.objects.all()
# test = TestSerilTwo(instance=codes,many=True)
# print(test.data)

from rest_learn.models import TestModel
class TestSerilThree(serializers.ModelSerializer):
    class Meta:
        model = TestModel
        fields = ['name','code','created_time','changed_time','id']
        read_only_fields = ['created_time','changed_time']

這一次,我們繼承的是 DRF 的模型序列化器,通過 Meta 給模型序列化器傳模型,通過 fields 來告訴序列化器我們需要序列化哪些字段。那 read_only_fields 又是用來干什么的呢?

剛才我們說過,序列化器是雙向驗證的,對前端和后端都有驗證。有時后端不希望某些字段被前端修改該,這就導致了我們對前端和后端的序列化字段會有所不同。一旦字段發(fā)生了變化,也就意味著序列化器也會發(fā)生變化,那該怎么辦呢?那就是把我們不希望前端修改的字段放在 read_only_fields 選項里,這樣,當序列化器在序列化前端的字段時,即便是前端有這些字段,序列化器也會忽略這些字段,這樣就可以防止別有用心的人暴力修改我們的字段。

好像還不是很懂?別著急,我們先用它試試看,接著在下面寫:

code = TestModel.objects.get(name='ls')
codes = TestModel.objects.all()

# 前端寫入測試
frontend_data = {
    'name':'ModelSeril',
    'code':"""print('frontend test')""",
    'created_time':'2107-12-16'
}
test1 = TestSerilThree(data=frontend_data)
if test1.is_valid():
    print('Frontend test:',test1.validated_data)
# 后端傳出測試:
test2 = TestSerilThree(instance=code)
print('Backend single instance test:',test2.data)
test3 = TestSerilThree(instance=codes,many=True)
print('Backend multiple instances test',test3.data)

輸出應該是這樣的:

Frontend test: OrderedDict([('name', 'ModelSeril'), ('code', "print('frontend test')")])

Backend single instance test: {'created_time': '2017-12-16T05:16:12.846759Z', 'name': 'ls', 'code': 'import os\r\nprint(os.listdir())', 'id': 3, 'changed_time': '2017-12-16T05:16:12.846759Z'}

Backend multiple instances test [OrderedDict([('name', 'hello world'), ('code', "print('Hello world')"), ('created_time', '2017-12-16T05:16:12.815559Z'), ('changed_time', '2017-12-16T05:16:12.815559Z'), ('id', 1)]), OrderedDict([('name', 'pwd'), ('code', 'import os\r\nprint(os.getcwd())'), ('created_time', '2017-12-16T05:16:12.831159Z'), ('changed_time', '2017-12-16T05:16:12.831159Z'), ('id', 2)]), OrderedDict([('name', 'ls'), ('code', 'import os\r\nprint(os.listdir())'), ('created_time', '2017-12-16T05:16:12.846759Z'), ('changed_time', '2017-12-16T05:16:12.846759Z'), ('id', 3)])]

我們可以看到,模型序列化器正確的序列化了我們的模型實例,包括其中的 DateTimeField 字段,如果是我們手寫來處理,不知道會有多麻煩。

我們先看前端寫入的測試的輸出,雖然我們的 frontend_data 有一個 created_time 字段,但是在最后的 .validated_data 中根本就沒有它的身影,我們的序列化器成功的過濾掉了這個非法字段。

再看后端傳出測試輸出,模型實例和查詢集實例的輸出結果都很正常。最重要的是,created_timechanged_time 兩個字段是被正常序列化了的,這兩個字段并沒有受到 read_only_fields 的影響,所以前端只能看到這個字段,不能修改這個字段。

這樣就方便許多了!接下來我們進入序列化器的進階學習。

剛剛的序列化器結構都很簡單,使用起來也很簡單,要是有關系字段該怎么處理呢?我并不打算直接用模型序列化器來講解,因為模型序列化器都幫我們把工作都完成了,我們最后什么都看不到。所以然我們來手寫一個能處理關系字段的序列化器。在開始之前,注釋掉之前的實驗代碼:

# code = TestModel.objects.get(name='ls')
# codes = TestModel.objects.all()

# 前端寫入測試
# frontend_data = {
#     'name':'ModelSeril',
#     'code':"""print('frontend test')""",
#     'created_time':'2107-12-16'
# }
# test1 = TestSerilThree(data=frontend_data)
# if test1.is_valid():
#     print('Frontend test:',test1.validated_data)
# 后端傳出測試:
# test2 = TestSerilThree(instance=code)
# print('Backend single instance test:',test2.data)
# test3 = TestSerilThree(instance=codes,many=True)
# print('Backend multiple instances test',test3.data)

在開始編寫之前,我們需要搞懂一個問題,序列化器到底是什么?它用起來的確很方便,但是當我們遇到問題時卻不知道從何下手,就像剛才的問題,如何利用序列化器處理關系字段?如果你去查看官方文檔,官方文檔會告訴你,使用 PrimaryKeyRelatedField ,我相信第一次看到這個答案的你一定是一臉懵逼,為什么???為什么我的關系模型就成了一個字段了????我明明想要的是關系模型相關聯的實例對象啊。。。你知道 PrimaryKeyRelatedField 是關系模型的主鍵。比如我們的 TestModelUser 表是關聯的,如果我使用的是 PrimaryKeyRelatedField 字段,那序列化的結果出來就會是類似這樣的:

{
    user:1,
    code:'some code',
    name:'script name' 
}

TestModel 相關聯的 User 實例就變成了一個主鍵,我們可以通過訪問這個主鍵來訪問 UserTestModel 相關聯的實例。但是一般,我們想要的效果是這樣的:

{
    user:{
        'id':1,
        'email':'email@example.com',
        'name':'username'
    },
    code:'some code',
    name:'script name'
}

我們想要的是 User 實例的詳細信息,而不是再麻煩一次,用 PrimaryKeyRelatedField 的值再去查詢一次。而且更頭痛的是,如果使用 PrimaryKeyRelatedField, 在創(chuàng)建實例的時候,你必須要先有一個相關聯的 User ,在創(chuàng)建 TestModel 時候再把這個 User 的主鍵給傳進去。也就是說,你不能一次性就創(chuàng)建好 TestModelUser ,要先創(chuàng)建 User 再創(chuàng)建 TestModel,這個流程簡直是讓人頭皮發(fā)麻。如果我們想一次性創(chuàng)建好他們該怎么辦呢?如果有心的同學去看看 DRF 的 release note ,就會知道,把 User 模型的序列化器當作一個字段就行了。什么???序列化器當成一個字段???這種操作也可以??從來沒見過這種操作啊。。在 Django 表單中也沒有見過這種操作啊。。怎么回事啊??

淡定,同樣的,我們先來做個實驗,先體驗下“序列化器當作字段”是怎么回事。假設我們希望能在創(chuàng)建 User 的同時也能夠同時創(chuàng)建Profile。 在 rest_test.py 下面接著寫:

class ProfileSerializer(serializers.Serializer):
    tel = serializers.CharField(max_length=15)
    height = serializers.IntegerField()

class UserSerializer(serializers.Serializer):
    name = serializers.CharField(max_length=20)
    qq = serializers.CharField(max_length=15)
    profile = ProfileSerializer()

我們可以看到,UserSerializerprofile 字段是 ProfileSerializer 。現在我們使用下這個序列化器。接著在下面寫:

frontend_data = {
    'name':'ucag',
    'qq':'88888888',
    'profile':{
        'tel':'66666666666',
        'height':'185'
    }
}

test = UserSerializer(data=frontend_data)
if test.is_valid():
    print(test.validated_data)

我們可以看到輸出是這樣的:

OrderedDict([('name', 'ucag'), ('qq', '88888888'), ('profile', OrderedDict([('tel', '66666666666'), ('height', 185)]))])

可以看到,我們的字段都被正確的序列化了。我們同時創(chuàng)建了 UserProfile 。并且他們也是正確的關聯在了一起。

現在可以問,這是怎么回事呢?這是因為序列化器其實就是一個特殊的“序列化器字段”。怎么理解呢?再說的容易懂一點,因為序列化器和序列化字段都相當于 python 的同一種數據結構——描述符。那描述符又是什么東西呢?官方文檔是這么說的:

In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol. Those methods are __get__(), __set__(), and __delete__(). If any of those methods are defined for an object, it is said to be a descriptor.

一般地,描述符是擁有“綁定行為”的對象屬性,當這個屬性被訪問時,它的默認行為會被描述符內的方法覆蓋。這些方法是 __get__(), __set__(), __delete__() 。任何一個定義的有以上方法的對象都可以被稱為描述符。

說的太繞了,我們來簡化一下。

  1. 描述符是有默認行為的屬性。
  2. 描述符是擁有 __get__(), __set__(), __delete__() 三者或者三者之一的對象。

所以,描述符是屬性,描述符也是對象。

我們先來理解第一條。描述符是屬性。什么是屬性呢?對于 a.b 來說,b 就是屬性。這個屬性可以是任何東西,可以是個方法,也可以是個值,也可以是任何其它的數據結構。當我們寫 a.b 時就是在訪問 b 這個屬性。
再理解第二條。描述符是對象。對象是什么呢?通常,我們都是使用 class 來定義一個對象。根據描述符定義,有 __get__(), __set__(), __delete__() 之一或全部方法的對象都是描述符。

滿足以上兩個條件,就可以說這個對象是描述符。

一般地,__get__(), __set__(), __delete__() 應該按照如下方式編寫:

descr.__get__(self, obj, type=None) --> value

descr.__set__(self, obj, value) --> None

descr.__delete__(self, obj) --> None

一般地,描述符是作為對象屬性來使用的。

當描述符是一個對象的屬性時,如 a.bb 為一個描述符,則執(zhí)行a.b 相當于執(zhí)行b.__get__(a)。 而 b.__get__(a) 的具體實現為 type(a).__dict__['b'].__get__(a, type(a)) 。以上這個過程沒有為什么,因為 python 的實現就是這樣的。我們唯一需要理解的就是,為什么會這樣實現。首先我們需要讀懂這個實現。

假設,a.b 中,aA 的實例,b 是描述符 B 的實例。

  1. type(a) 返回的是 a 的類型 A。那就變成了 A.__dict__['b'].__get__(a, type(a))

  2. A.__dict__['b'] 返回的是 A類屬性 b 的值。假設 A.__dict__['b'] 的值為 Ab,那么就變成了 Ab.__get__(a, type(a))

    我們在這里暫停一下。注意 A.__dict__['b'] 返回的是 A 的類屬性b 的值是一個描述符,也就是說,Ab 是個描述符。那么連起來,就變成了:

    • Ab ,也就是 b ,是一個類屬性,這個類屬性是個描述符。也就是描述符 B 的實例 A類屬性
  3. 最后一步就很簡單了,就是調用描述符的 __get__() 方法,也就是 Ab.__get__(a, A),也就是 b.__get__(a, A) 。到這里,大家可能會問一個問題,__get__ 的參數也就是 aA 是誰傳進去的呢?,答案說出來很簡單,但是很多時候有的同學容易繞進去就出不來了。答案就是:
    python 解釋器自己傳進去的。就像是類方法的 self 一樣,沒誰手動傳 self 進去,這都是 python 的設計者這樣設計的。

一句話總結一下。 bA 類屬性且為描述符時,A 的實例 a 對于 b 訪問也就是a.b 就相當于 b.__get__(a, A)所以,此時,對于 b 屬性的訪問結果就取決于 b__get__() 返回的結果了。

我們稍微再推理一下,就可以知道,如果一個對象的屬性是描述符對象,而且這個對象本身也是描述符的話,那么,這個對象的各種子類就可以相互作為彼此的屬性。說的很復雜,舉個簡單的例子。

我們來簡單的運用下剛才學到的知識,在解釋器里輸入以下代碼:

In [1]: class Person: # 定義 Person 描述符
   ...:     def __init__(self, name=None):
   ...:         self.name = name
   ...:     def __set__(self, obj, value):
   ...:         if isinstance(value, str):
   ...:             self.name = value
   ...:         else:
   ...:             print('str is required!')
   ...:     def __get__(self, obj, objtype):
   ...:         return 'Person(name={})'.format(s
   ...: elf.name)
   ...: class Dad(Person):
   ...:     kid = Person('Son')
   ...: class Grandpa(Person):
   ...:     kid = Dad('Dad')
   ...:
   ...: dad = Dad('Dad')
   ...: gp = Grandpa('Granpa')
   ...:
   ...: print("Dad's kid:",dad.kid)
   ...: print("Grandpa's kid:",gp.kid)
   ...:
Dad's kid: Person(name=Son)
Grandpa's kid: Person(name=Dad)

In [2]: dad.kid = 18
str is required!

In [3]: dad.kid
Out[3]: 'Person(name=Son)'

可以看到,我們在定義描述符之后,除了直接實例化使用他們之外,還把他們作為其它描述符的屬性。描述符 Dad 的屬性 kid 也是一個描述符。 我們的對 kid 的賦值成功被 __set__ 攔截,并在賦值類型不規(guī)范時給出了我們事先寫好的警告,并且原來的值也沒有被改變。

現在我們回到序列化器中來。序列化器和序列化器字段就是像這樣的描述符,他們完全是同一種東西。所以他們完全可以作為彼此的類屬性來使用。一旦明白了這一點,就可以有各種“騷操作”了。序列化器最基本的字段描述符定義了字段的操作,所以不用我們自己重新去編寫 __get__ __set__ __delete__ ,DRF 已經編寫好基本的邏輯,我們只需要調用現成的接口就可以實現自定義字段。在簡單的繼承 serializers.Field 后就可以使用這些現成的接口了,這個接口是:
.to_representation(obj).to_internal_value(data)

  • .to_representation(obj): 它決定在訪問這個字段時的返回值應該如何展示,obj 是 to_internal_value 返回的對象。 作用如同描述符的__get__。應該返回能夠被序列化的數據結構。如數字,字符串,布爾值 date/time/datetime 或者 None
  • .to_internal_value(data): 它決定在對這個字段賦值時應該進行的操作,data 是前端傳過來的字段值。作用如同描述符的__set__ 操作,應該返回一個 python 數據結構。在發(fā)生錯誤時,應拋出 serializers.ValidationError

我們現在可以自己定義一個字段試試看,注釋掉之前的測試,接著 rest_test.py 寫:

# frontend_data = {
#     'name':'ucag',
#     'qq':'88888888',
#     'profile':{
#         'tel':'66666666666',
#         'height':'185'
#     }
# }

# test = UserSerializer(data=frontend_data)
# if test.is_valid():
#     print(test.validated_data)
class TEL(object):
    """電話號碼對象"""
    def __init__(self, num=None):
        self.num = num
    def text(self, message):
        """發(fā)短信功能"""
        return self._send_message(message)
    def _send_message(self,message):
        """發(fā)短信"""
        print('Send {} to {}'.format(message[:10], self.num))
class TELField(serializers.Field):
    def to_representation(self, tel_obj):
        return tel_obj.num
    def to_internal_value(self, data):
        data = data.lstrip().rstrip().strip()
        if 8 <= len(data) <=11:
            return TEL(num=data)
        raise serializers.ValidationError('Invalid telephone number.')

這樣就完成了我們的“騷操作”字段。我們就可以這樣使用它,接著在下面寫:

class ContactSerializer(serializers.Serializer):
    name = serializers.CharField(max_length=20)
    tel = TELField()

frontend_data = {
    'name':'ucag',
    'tel':'88888888'
}
test = ContactSerializer(data=frontend_data)
if test.is_valid():
    tel = test.validated_data['tel']
    print('TEL',tel.num)
    tel.text('這是一個騷字段')

直接運行 rest_test.py,輸出如下:

TEL 88888888
Send 這是一個騷字段 to 88888888

我們自定義的字段就完成了。

以上就是我們對序列化器的學習。目前我們就學習到這個程度,序列化器剩下知識的都是一些 API 相關的信息,需要用到的時候直接去查就是了。我們已經明白了序列化器的原理。以后遇到什么樣的數據類型處理都不怕了,要是遇到太奇葩的的需求,大不了我們自己寫一個字段。相關的細節(jié)我們在以后的學習中慢慢學習。

API View 與 URL 配置

這是 DRF 的又一個很重要的地方,在第二章,我們自己編寫了 APIView ,并且只支持一種內容協(xié)商,DRF 為我們提供了功能更加完備的 APIView, 不僅支持多種內容協(xié)商,還支持對 API 訪問頻率的控制,對查詢結果過濾等等。

DRF 的 API 視圖有兩種使用方式,一種是利用裝飾器,一種是使用類視圖。

我們主要講類視圖 API ,裝飾器放在后面作為補充。

我們知道 Django 的視圖返回的是 HttpResponse 對象,并且默認接收一個 HttpRequest 對象,我們可以通過這個請求對象訪問到請求中響應的數據。同樣的,DRF 的 APIView 也是接收一個默認的請求對象,返回一個響應對象。只是在 APIView 中的請求和響應對象變成了 DRF 的請求和響應對象。

DRF 的請求對象功能比 Django 自帶的要完備很多,也強大很多。不僅原生支持 PUT 方法,還支持對 POST URL 的參數解析等眾多功能。

我們來看看 DRF 的請求對象都有哪些功能:

  1. .data: DRF 請求對象的 data 屬性包含了所有的上傳對象,甚至包括文件對象!也就是說,我們可以只通過訪問 resquest.data 就能得到所有的上傳數據,包括 PUT 請求的!還支持多種數據上傳格式,前端不僅可以以 form 的形式上傳,還可以以 json 等眾多其它形式上傳數據!
  2. .query_params: query_params 屬性包含了所有的 URL 參數,不僅僅是 GET 請求的參數,任何請求方法 URL 參數都會被解析到這里。
  3. .user: 和原生的 user 屬性作用相同。
  4. .auth: 包含額外的認證信息。

當然,DRF 的請求對象不止有這些功能,還有許多其它的功能,大家可以去文檔里探索一下。

DRF 的響應對象:

DRF 響應對象接收以下參數:

  1. data: 被序列化之后的數據。將被用作響應數據傳給前端。
  2. status: 狀態(tài)碼。
  3. headers: 響應頭。
  4. content_type: 響應類型。一般不需要我們手動設置這個字段。

讓我們來看看 DRF 的 APIView 具體的應用方法:

from rest_framework.views import APIView
from rest_framework.response import Response
from django.contrib.auth import get_user_model
User = get_user_model()
class ListUsers(APIView):
    def get(self, request, format=None):
        usernames = [user.username for user in User.objects.all()]
        return Response(usernames)

這就是最簡單的 APIView 了。我們的 get 函數的 format 參數是用于控制和前端的內容協(xié)商的,我們可以通過判斷這個值來決定返回什么樣類型的數據。同時,APIView 還有許多其它的參數供我們使用,但是目前我們就暫時先了解到這里。

別忘了,我們學習的是 REST 開發(fā),對應的請求有對應普適的規(guī)則。所以,在 APIView 基礎之上的類視圖是十分有用的——GenericView,它就相當與我們之前編寫的加了各種 Mixin 操作的 APIView,只不過 GenericView 提供的操作和功能比我們自己編寫的要豐富很多。同樣的,GenericView 也是通過提供各種通用使用各種 Mixin 的類屬性和方法來提供不同的功能。所以我們就在這里簡單的介紹一下這些類屬性和方法:

GenericView 提供的屬性有:

  1. queryset: 和我們的 APIView 中的 queryset 作用是相同的。
  2. serializer_class: 序列化器,GenericView 將會自動應用這個序列化器進行相應的數據處理工作。
  3. lookup_field: 和我們之前編寫的 lookup_args 作用相同,只是它只有一個值,默認為 'pk' 。
  4. lookup_url_kwarg: 在 URL 參數中用于查詢實例的參數,在默認情況下,它的值等于 lookup_field
  5. pagination_class: 用于分頁的類,默認為 rest_framework.pagination.PageNumberPagination,如果使它為 None ,就可以禁用分頁功能。
  6. filter_backends: 用于過濾查詢集的過濾后端,可以在 DEFAULT_FILTER_BACKENDS 中配置。

提供的方法有:

  1. get_queryset(self): 獲取查詢集,默認行為和我們編寫的 get_queryset 相同。
  2. get_object(self): 獲取當前實例對象。
  3. filter_queryset(self, queryset): 在每一次查詢之后都使用它來過濾一次查詢集。并返回新的查詢集。
  4. get_serializer_class(self): 獲取序列化器,可以通過編寫這個方法做到動態(tài)的序列化器的更換。
  5. get_serializer_context(self): 返回一個作用于序列化器的上下文字典。默認包含了 request, view,format 鍵。如果這里不懂沒關系,我們后面還會講到。

當然,GenericView 還提供了許多其它的功能,所以想要更多了解的同學可以去查閱官方文檔。沒看的也不用擔心,我們在之后會慢慢的涉及到更多的知識點。

以上都介紹的很簡單,我們要重點介紹的是下面的 ViewSet
什么是 ViewSetViewSet 顧名思義就是一大堆的視圖集合。為什么要把一大推的視圖集合到一起呢?因為他們都是通用的。具體體現在哪些地方呢?他們都是符合 REST 規(guī)范的視圖,所以只需要按照 REST 規(guī)范就可以使用這些視圖了。

比如,像這樣,這是官方文檔的例子:

# views.py
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from myapps.serializers import UserSerializer
from rest_framework import viewsets
from rest_framework.response import Response

class UserViewSet(viewsets.ViewSet):
    def list(self, request):
        queryset = User.objects.all()
        serializer = UserSerializer(queryset, many=True)
        return Response(serializer.data)

    def retrieve(self, request, pk=None):
        queryset = User.objects.all()
        user = get_object_or_404(queryset, pk=pk)
        serializer = UserSerializer(user)
        return Response(serializer.data)

# urls.py
user_list = UserViewSet.as_view({'get': 'list'})
user_detail = UserViewSet.as_view({'get': 'retrieve'})

看,我們使用了 ViewSet 之后就不用手動的編寫 get 等等方法了,只需要編寫對應的操作函數就可以了。更讓人驚喜的是,ViewSet 的使用方法和我們之前使用了 MethodMapMixinAPIView 是一模一樣的。通過方法映射到具體的操作函數上來。但是含有比這樣寫更酷的方法:

# urls.py
from myapp.views import UserViewSet
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'users', UserViewSet, base_name='user')
urlpatterns = router.urls

通過使用 Router 來自動生成我們需要的 API 接口。這個等會兒再說。我們先說說 GenericViewSetModelViewSet
GenericViewSet: 只是簡單的添加上了 GenericView 的功能。我們重點說 ModelViewSet
如果我們想要提供的 API 功能就是默認符合 REST 規(guī)范的 API ,要是使用 ModelViewSet 的話,我們就只需要提供一個參數就可以解決所有問題:

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()

是的,我們的視圖就這樣就寫完了。簡化到兩行,要是你愿意,也可以簡化到一行。再配合 Router 使用,一共不超過十行代碼就可以完成我們之前寫了好幾百行的代碼完成的功能。

這里我們只是做簡單的了解,等到真正需要用的時候大家才可以學習到其中的奧妙。我們接下來說說 Router

Router 是用來幫我們自動生成 REST API 的。就像這種:

url(r'^users/$', name='user-list'),
url(r'^users/{pk}/$', name='user-detail'),
url(r'^accounts/$', name='account-list'),
url(r'^accounts/{pk}/$', name='account-detail')

自動生成這些 API ,這些 API 都符合 REST 規(guī)范。

Router 的使用要結合我們上面學到的知識,本節(jié)我們就以 Roter 的使用收尾。

注釋掉之前的驗證代碼,接著在后面寫:

# frontend_data = {
#     'name':'ucag',
#     'tel':'88888888'
# }
# test = ContactSerializer(data=frontend_data)
# if test.is_valid():
#     tel = test.validated_data['tel']
#     print('TEL',tel.num)
#     tel.text('這是一個騷字段')

from rest_framework.viewsets import ModelViewSet
class TestViewSet(ModelViewSet):
    queryset = TestModel.objects.all()

from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'codes', TestViewSet)
urlpatterns = router.urls
print(urlpatterns)

使用過程不多說,都是機械式的使用,先使用 register 注冊 url,第一個參數是 url 的前綴,就是想用什么開頭,比如 url(r'^users/$', name='user-list') 就是以 users 開頭。視圖的名字 Router 會自己幫你加上,就兩種名字。一個是 <prefix>-list,一個是<prefix>-detail 。當然,如果你想改也是可以改的。這個留到我們以后說。

直接運行 rest_test.py ,你應該會看到以下輸出。

[<RegexURLPattern testmodel-list ^codes/$>, 
<RegexURLPattern testmodel-list ^codes\.(?P<format>[a-z0-9]+)/?$>, 
<RegexURLPattern testmodel-detail ^codes/(?P<pk>[^/.]+)/$>, 
<RegexURLPattern testmodel-detail ^codes/(?P<pk>[^/.]+)\.(?P<format>[a-z0-9]+)/?$>, 
<RegexURLPattern api-root ^$>, <RegexURLPattern api-root ^\.(?P<format>[a-z0-9]+)/?$>]

這就是 Router 為我們生成的 API 了。細心的同學或許已經發(fā)現了,還有個 api-root 的視圖,訪問這個 API 會返回所有的 list 視圖的 API 。可以通過這些鏈接訪問到所有的實例。


我們對 DRF 的初步學習就到這里。很明顯,我們的本節(jié)的重點就是序列化器,所以大家務必掌握序列化器的相關知識點,對視圖和 URL 配置不是怎么懂都沒有什么大的問題,這些都只是 DRF API 調用的問題,唯獨序列化器的使用和原理需要大家十分扎實的掌握。所以,最低的要求是起碼在本節(jié)結束后看到使用 DRF 的代碼,能夠明白它是什么意思,能夠模仿著寫出東西,最好能夠舉一反三。本節(jié)涉及的知識點的確有些難,不過一個星期理解這些知識點的時間也應該足夠了。下一節(jié)我們就要開始 Vue 的學習,相對來說會輕松一些了。大家加油

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容