項(xiàng)目開發(fā)
在上一章我們了解了 REST 和 Mixin 以及 UI 狀態(tài)的概念佳谦、API 設(shè)計(jì)相關(guān)的一些知識(shí)凤壁,現(xiàn)在我們將會(huì)使用這些概念來真正編寫一個(gè) REST 項(xiàng)目。在本章尝江,我們將會(huì)涵蓋以下知識(shí)點(diǎn):
- Mixin 的編寫涉波,掌握 Mixin 的最基本編寫原則
- Store 與 state 的編寫。理解并能應(yīng)用 UI 的狀態(tài)概念炭序。
- 了解 API 的基本編寫規(guī)范和原則
本章的一些代碼會(huì)涉及到元編程的一點(diǎn)點(diǎn)知識(shí),還有裝飾器的知識(shí)苍日,這些都會(huì)在我們的教程中有所提及惭聂。但是由于我們的主要目標(biāo)是開發(fā)應(yīng)用,而不是進(jìn)行編程教學(xué)相恃,所以如果有碰到不懂的地方辜纲,大家可以先自行查找資料,如果還是不懂拦耐,可以留言提 issue 耕腾,我將會(huì)在教程中酌情補(bǔ)充講解。同樣的杀糯,本章的完整代碼在這里扫俺,別忘了 star 喲~
設(shè)計(jì)項(xiàng)目
在第一章,不管是在前端還是在后端開發(fā)固翰,我們?cè)趯懘a之前都有設(shè)計(jì)的過程狼纬,同樣的,在這里我們也需要設(shè)計(jì)好我們的項(xiàng)目才可以開始寫代碼骂际。
需求分析
后端開發(fā)的主職責(zé)是提供 API 服務(wù)疗琉,同時(shí),我們不能再把 javascript 寫在 html 里了歉铝,因?yàn)檫@次的 javascript 代碼會(huì)有點(diǎn)多盈简,所以我們要提供靜態(tài)文件服務(wù)。一般來說太示,靜態(tài)文件服務(wù)都是由專門的靜態(tài)文件服務(wù)器來完成的柠贤,比如說 CDN ,也可以用 Nginx 先匪。在這一章种吸,我們的項(xiàng)目非常小,所以就使用 Django 來提供靜態(tài)文件服務(wù)呀非。我們計(jì)劃自己編寫一個(gè)簡(jiǎn)易的靜態(tài)文件服務(wù)坚俗。
項(xiàng)目結(jié)構(gòu)
我們的項(xiàng)目結(jié)構(gòu)如下:
online_intepreter_project/
frontend/ # 前端目錄
index.html
css/
...
js/
...
online_intepreter_project/ # 項(xiàng)目配置文件
settings.py
urls.py
...
online_intepreter_app/ # 我們真正的應(yīng)用在這里
...
manage.py
大家可以看到镜盯,其實(shí)這一次,我們還是以后端為主猖败,前端并沒有獨(dú)立出后端的項(xiàng)目結(jié)構(gòu)速缆,就像剛才所說,靜態(tài)文件恩闻,或者說是前端文件艺糜,應(yīng)該盡量由專門的服務(wù)器來提供服務(wù),后端專門負(fù)責(zé)數(shù)據(jù)處理就可以了幢尚。我們將會(huì)在之后的章節(jié)中使用這種模式破停,使用 Nginx 作為靜態(tài)文件服務(wù)器。不熟悉 Nginx ? 沒關(guān)系尉剩,我們會(huì)有專門的一章講解 Nginx 真慢,以及有相應(yīng)的練習(xí)項(xiàng)目。
做個(gè)深呼吸理茎,開始動(dòng)手了黑界。
后端開發(fā)
在終端中新建一個(gè)項(xiàng)目:
python django-admin.py startproject online_intepreter_project
在這之前,我們使用的都是單文件的 Django 皂林,這一次我們需要使用 Django 的 ORM 朗鸠,所以需要按照標(biāo)準(zhǔn)的 Django 項(xiàng)目結(jié)構(gòu)來構(gòu)建我們的項(xiàng)目。然后切換到項(xiàng)目路徑內(nèi)础倍,建立我們的 app:
python manage.py startapp online_intepreter_app
同時(shí)烛占,將不需要的文件刪除,并且再新建幾個(gè)空文件著隆。按照如下來修改我們的項(xiàng)目結(jié)構(gòu):
online_intepreter_project/
frontend/ # 前端目錄
index.html
css/
bootstrap.css
main.css
js/
main.js
bootstrap.js
jquery.js
online_intepreter_project/ # 項(xiàng)目配置文件
__init__.py
settings.py # 項(xiàng)目配置
urls.py # URL 配置
online_intepreter_app/ # 我們真正的應(yīng)用在這里
__init__.py
views.py # 視圖
models.py # 模型
middlewares.py # 中間件
mixins.py # mixin
manage.py
編輯項(xiàng)目的 settings.py
:
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = '=@_j0i9=3-93xb1_9cr)i!ra56o1f$t&jhfb&pj(2n+k9ul8!l'
DEBUG = True
INSTALLED_APPS = ['online_intepreter_app']
MIDDLEWARE = ['online_intepreter_app.middlewares.put_middleware']
ROOT_URLCONF = 'online_intepreter_project.urls'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
INSTALLED_APPS
: 安裝我們的應(yīng)用扰楼。Django 會(huì)遍歷這個(gè)列表中的應(yīng)用,并在使用 makemigrations
這個(gè)命令時(shí)才會(huì)自動(dòng)的搜尋并創(chuàng)建我們應(yīng)用的模型美浦。
MIDDLEWARE
: 我們需要使用的中間件弦赖。由于 Django 不支持對(duì) PUT 方法的數(shù)據(jù)處理,所以我們需要寫一個(gè)中間件來給它加上這個(gè)功能浦辨。之后我們會(huì)更加詳細(xì)的了解中間件的寫法蹬竖。
DATABASES
: 配置我們的數(shù)據(jù)庫(kù)。在這里流酬,我們只是簡(jiǎn)單的使用了 sqlite3
數(shù)據(jù)庫(kù)币厕。
以上便是所有的配置了。
現(xiàn)在我們先來編寫 PUT 中間件芽腾,來讓 Django 支持 PUT 請(qǐng)求旦装。我們可以使用 POST 方法向 Django 應(yīng)用上傳數(shù)據(jù),并且可以使用 request.POST 來訪問 POST 數(shù)據(jù)摊滔。我們也想像使用 POST 一樣來使用 PUT 阴绢,利用 request.PUT 就可以訪問到 PUT 請(qǐng)求的數(shù)據(jù)店乐。
中間件是 django 很重要的一部分,它在請(qǐng)求和響應(yīng)之間充當(dāng)預(yù)處理器的角色呻袭。
很多通用的邏輯可以放到這里眨八,django 會(huì)自動(dòng)的調(diào)用他們。
在這里左电,我們寫了一個(gè)簡(jiǎn)單的中間件來處理 PUT 請(qǐng)求廉侧。只要是 PUT 請(qǐng)求,我們就對(duì)它作這樣的處理篓足。所以段誊,當(dāng)你對(duì)某個(gè)請(qǐng)求都有相同的處理操作時(shí),可以把它寫在中間件里栈拖。所以枕扫,中間件是什么呢?
中間件只是視圖函數(shù)的公共部分辱魁。你把中間件的核心處理邏輯復(fù)制粘貼到視圖函數(shù)中也是能夠正常運(yùn)行的。
打開你的 middlewares.py
:
from django.http import QuerDict
def put_middleware(get_response):
def middleware(request):
if request.method == 'PUT': # 如果是 PUT 請(qǐng)求
setattr(request, 'PUT', QueryDict(request.body)) # 給請(qǐng)求設(shè)置 PUT 屬性诗鸭,這樣我們就可以在視圖函數(shù)中訪問這個(gè)屬性了
# request.body 是請(qǐng)求的主體染簇。我們知道請(qǐng)求有請(qǐng)求頭,那請(qǐng)求的主體就是
# request.body 了强岸。當(dāng)然锻弓,你一定還會(huì)問,為什么這樣就可以訪問 PUT 請(qǐng)求的相關(guān)
# 數(shù)據(jù)了呢蝌箍?這涉及到了 http 協(xié)議的知識(shí)青灼,這里就不展開了,有興趣的同學(xué)可以自行查閱資料
response = get_response(request) # 使用 get_response 返回響應(yīng)
return response # 返回響應(yīng)
return middleware # 返回核心的中間件處理函數(shù)
QueryDict
是 django 專門為請(qǐng)求的查詢字符串做的數(shù)據(jù)結(jié)構(gòu)妓盲,它類似字典杂拨,但是又不是字典。
request
對(duì)象的 POST
GET
屬性都是這樣的字典悯衬。類似字典弹沽,是因?yàn)?QueryDict
和 python 的 dict
有相似的 API 接口,所以你可以把它當(dāng)字典來調(diào)用筋粗。
不是字典策橘,是因?yàn)?QueryDict
允許同一個(gè)鍵有多個(gè)直。比如 {'a':[‘1’,‘2’]}娜亿,a 同時(shí)有值 1 和 2丽已,所以,一般不要用 QueryDict[key]
的形式來訪問相應(yīng) key 的值买决,因?yàn)槟愕玫降臅?huì)是一個(gè)列表沛婴,而不是一個(gè)單一的值吼畏,應(yīng)該用 QueryDict.get(key)
來獲取你想要的值,除非你知道你在干什么瘸味,你才能這樣來取值宫仗。為什么會(huì)允許多個(gè)值呢,因?yàn)?GET
請(qǐng)求中旁仿,常常有這種參數(shù)http://www.example.com/?action=search&achtion=filter
藕夫,
action
在這里有兩個(gè)值,有時(shí)候我們需要對(duì)這兩個(gè)值都作出響應(yīng)枯冈。但是當(dāng)你用 .get(key)
方法取值的時(shí)候毅贮,只會(huì)取到最新的一個(gè)值。如果確實(shí)需要訪問這個(gè)鍵的多個(gè)值尘奏,應(yīng)該用 .getList(key)
方法來訪問滩褥,比如剛才的例子應(yīng)該用 request.GET.getList('action')
來訪問 action
的多個(gè)值。
同理炫加,對(duì)于 POST
請(qǐng)求也應(yīng)該這么做瑰煎。
接下來要說說 request.body
。做過爬蟲的同學(xué)一定都知道俗孝,請(qǐng)求有請(qǐng)求頭酒甸,那這個(gè) body
就是我們的請(qǐng)求體了。嚴(yán)格的講赋铝,這個(gè)“請(qǐng)求體”應(yīng)該叫做“載荷”插勤,用英文來講,這就叫做“payload”革骨。載荷里又有許多的學(xué)問了农尖,感興趣的同學(xué)可以自己去了解相關(guān)的資料。只需要知道一件很簡(jiǎn)單的事情良哲,就是把 request.body
放進(jìn) QueryDict
就可以把上傳的字段轉(zhuǎn)換為我們需要的字典了盛卡。
由于原生的 request
對(duì)象并沒有 PUT 屬性,所以我們需要在中間件中加上這個(gè)屬性臂外,這樣我們就可以在視圖函數(shù)中用 request.PUT
來訪問 PUT 請(qǐng)求中的參數(shù)值了窟扑。
中間件在 1.11 版本里是一個(gè)可調(diào)用對(duì)象,和之前的類中間件不同漏健。既然是可調(diào)用對(duì)象嚎货,那就有兩種寫法,一種是函數(shù)蔫浆,因?yàn)楹瘮?shù)就是一個(gè)可調(diào)用對(duì)象殖属;一種是自己用類來寫一個(gè)可調(diào)用對(duì)象,也就是包含 __call__()
方法的類瓦盛。
在 1.11 版本中洗显,中間件對(duì)象應(yīng)該接收一個(gè) get_response
的參數(shù)外潜,這個(gè)參數(shù)用來獲取上一個(gè)中間件處理之后的響應(yīng),每個(gè)中間件處理完請(qǐng)求之后都應(yīng)該用這個(gè)函數(shù)來返回一個(gè)響應(yīng)挠唆,我們不需要關(guān)心這個(gè) get _response
函數(shù)是怎么寫的处窥,是什么東西,只需要記得在最后調(diào)用它玄组,返回響應(yīng)就好滔驾。這個(gè)最外層函數(shù)應(yīng)該返回一個(gè)函數(shù),用作真正的中間件處理俄讹。
在外層函數(shù)下寫你的預(yù)處理邏輯哆致,比如配置什么的。當(dāng)然患膛,你也可以在被返回的函數(shù)中寫配置和預(yù)處理摊阀。但是這么做有時(shí)候就有些不直觀,配置踪蹬、預(yù)處理和核心邏輯分開胞此,讓看代碼的人一眼就明白這個(gè)中間件是在做什么。最通常的例子是跃捣,很多的 API 會(huì)對(duì)請(qǐng)求做許多的處理豌鹤,比如記錄下這個(gè)請(qǐng)求的 IP 地址就可以先在這里做這個(gè)步驟;又比如枝缔,為了控制訪問頻率,可以先讀取數(shù)據(jù)庫(kù)中的訪問數(shù)據(jù)蚊惯,根據(jù)訪問數(shù)據(jù)記錄來決定要不要讓這個(gè)請(qǐng)求進(jìn)入到視圖函數(shù)中愿卸。我們對(duì) PUT 請(qǐng)求并沒有什么預(yù)處理或者配置操作要進(jìn)行,所以就什么都沒寫截型。
中間件的處理邏輯雖然簡(jiǎn)單趴荸,但是中間件的寫法和作用大家還是需要掌握的。
接下來宦焦,讓我們創(chuàng)建我們的模型,編輯你的 models.py
:
from django.db import models
# 創(chuàng)建 Cdoe 模型
class CodeModel(models.Model):
name = models.CharField(max_length=50) # 名字最長(zhǎng)為 50 個(gè)字符
code = models.TextField() # 這個(gè)字段沒有文本長(zhǎng)度的限制
def __str__(self):
return 'Code(name={},id={})'.format(self.name,self.id)
在這里要注意一下,如果你是 py2 思恐,__str__
你需要改成 __unicode__
驱还。我們的表結(jié)構(gòu)很簡(jiǎn)單,這里就不多說了精堕。
我們的 API 返回的是 json
數(shù)據(jù)類型孵淘,所以我們需要把最基礎(chǔ)的響應(yīng)方式更改為 JsonResponse
。同時(shí)歹篓,我們還有一個(gè)問題需要考慮瘫证,那就是如何把模型數(shù)據(jù)轉(zhuǎn)換為 json
類型揉阎。 我們知道 REST 中所說的 “表現(xiàn)(表層)狀態(tài)轉(zhuǎn)換” 就是這個(gè)意思,把不同類型的數(shù)據(jù)轉(zhuǎn)換為統(tǒng)一的類型背捌,然后傳送給前端毙籽。如果前端要求是 json
那么我們就傳 json
過去,如果前端請(qǐng)求的是 xml
我們就傳 xml
過去毡庆。這就是“內(nèi)容協(xié)商(協(xié)作)”坑赡。當(dāng)然,我們的應(yīng)用很簡(jiǎn)單扭仁,就只有一種形式垮衷,但是如果是其它的大型應(yīng)用,前端有時(shí)請(qǐng)求的是 json
格式的乖坠,有時(shí)請(qǐng)求的是 xml
格式的搀突。我們的應(yīng)用很簡(jiǎn)單,就不用考慮內(nèi)容協(xié)商了熊泵。
回到我們的問題仰迁,我們?cè)撊绾伟涯P蛿?shù)據(jù)轉(zhuǎn)換為 json
數(shù)據(jù)呢? 把其它數(shù)據(jù)按照一定的格式保存下來顽分,這個(gè)過程我們稱為“序列化”徐许。“序列化”這個(gè)詞其實(shí)很形象卒蘸,它把一系列的數(shù)據(jù)雌隅,按照一定的方式給整整齊齊的排列好,保存下來缸沃,以便他用恰起。在 Django 中,Django 為我們提供了一些簡(jiǎn)單的序列化工具趾牧,我們可以使用這些工具來把模型的內(nèi)容轉(zhuǎn)換為 json
格式检盼。
其中很重要的工具便是 serializers
了,看名字我們就這到它是用來干什么的翘单。其核心函數(shù) serialize(format, queryset[,fields])
就是用于把模型查詢集轉(zhuǎn)換為 json
字符串吨枉。它接收的三個(gè)參數(shù)分別為 format
,format
也就是序列化形式哄芜,如果我們需要 json
形式的貌亭,我們就把 format
賦值為 'json'
。 第二個(gè)參數(shù)為查詢集或者是一個(gè)含有模型實(shí)例的可迭代對(duì)象认臊,也就是說属提,這個(gè)參數(shù)只能接收類似于列表的數(shù)據(jù)結(jié)構(gòu)。fields
是一個(gè)可選參數(shù),他的作用就和 Django 表單中的 fields
一樣冤议,是用來控制哪些字段需要被序列化的斟薇。
編輯你的 views.py
:
from django.views import View # 引入最基本的類視圖
from django.http import JsonResponse # 引入現(xiàn)成的響應(yīng)類
from django.core.serializers import serialize # 引入序列化函數(shù)
from .models import CodeModel # 引入 Code 模型,記得加個(gè) `.` 哦恕酸。
import json # 引入 json 庫(kù)堪滨,我們會(huì)用它來處理 json 字符串。
# 定義最基本的 API 視圖
class APIView(View):
def response(self,
queryset=None,
fields=None,
**kwargs):
"""
序列化傳入的 queryset 或 其他 python 數(shù)據(jù)類型蕊温。返回一個(gè) JsonResponse 袱箱。
:param queryset: 查詢集,可以為 None
:param fields: 查詢集中需要序列化的字段义矛,可以為 None
:param kwargs: 其他需要序列化的關(guān)鍵字參數(shù)
:return: 返回 JsonResponse
"""
# 根據(jù)傳入?yún)?shù)序列化查詢集发笔,得到序列化之后的 json 字符串
if queryset and fields:
serialized_data = serialize(format='json',
queryset=queryset,
fields=fields)
elif queryset:
serialized_data = serialize(format='json',
queryset=queryset)
else:
serialized_data = None
# 這一步很重要,在經(jīng)過上面的查詢步驟之后凉翻, serialized_data 已經(jīng)是一個(gè)字符串
# 我們最終需要把它放入 JsonResponse 中了讨,JsonResponse 只接受 python 數(shù)據(jù)類型
# 所以我們需要先把得到的 json 字符串轉(zhuǎn)化為 python 數(shù)據(jù)結(jié)構(gòu)。
instances = json.loads(serialized_data) if serialized_data else 'No instance'
data = {'instances': instances}
data.update(kwargs) # 添加其他的字段
return JsonResponse(data=data) # 返回響應(yīng)
需要注意的是制轰,我們先序列化了模型前计,然后又用 json
把它轉(zhuǎn)換為了 python 的字典結(jié)構(gòu),因?yàn)槲覀冞€需要把模型的數(shù)據(jù)和我們的其它數(shù)據(jù)(kwargs
)放在一起之后才會(huì)把它變成真正的 json
數(shù)據(jù)類型垃杖。
接下來男杈,重頭戲到了,我們需要編寫我們的 Mixin 了调俘。 在編寫 Mixin 之前伶棒,我們需要遵循以下幾個(gè)原則:
每個(gè) Mixin 只完成一個(gè)功能。這就像是我們?cè)凇吧稀敝信e的例子一樣彩库,一個(gè) Mixin 只會(huì)讓我們的“Man”類多一個(gè)功能出來苞冯。這是為了在使用的時(shí)候能夠更加清晰的明白這個(gè) Mixin 是干什么的,同時(shí)能夠做到靈活的解耦功能侧巨,做到“即插即用”。
-
每個(gè) Mixin 只操作自己知道的屬性和方法鞭达,還是那我們之前的 “Man” 類來做例子司忱。我們知道我們寫的幾個(gè) Mixin 最終都是用于
Man
類的,然而Man
類的屬性有name
畴蹭、age
坦仍,所以在我們的 Mixin 中也可以像這樣來訪問這些屬性:self.name
,self.age
。因?yàn)檫@些屬性都是已知的叨襟。當(dāng)然啦繁扎,Mixin 自己的屬性當(dāng)然也是可以自己調(diào)用的啦。那在 Mixin 中我們需要用到其它的 Mixin 的屬性的時(shí)候該怎么辦呢?很簡(jiǎn)單,直接繼承這個(gè) Mixin 就好了梳玫。 我們的 Mixin 最終是要作用到視圖上的爹梁,所以我們可以把我們的基礎(chǔ)視圖的屬性當(dāng)作是已知屬性。 我們的APIView
是View
類的子類提澎,所以View
的所有屬性和方法我們的Mixin
都可以調(diào)用姚垃。我們通常用到的屬性有:1. `kwargs`: 這是傳入視圖函數(shù)的關(guān)鍵字參數(shù),我們可以在類視圖中使用 `self.kwargs` 來訪問這些傳入的關(guān)鍵字參數(shù)盼忌。 2. `args`: 傳入視圖的位置參數(shù)积糯。 3. `request`: 視圖函數(shù)的第一個(gè)參數(shù),也就是當(dāng)前的請(qǐng)求對(duì)象谦纱,它和我們平時(shí)寫的視圖函數(shù)中的 request 是一模一樣的看成。
編寫 Mixin 是為了代碼的復(fù)用和代碼的解耦,所以在正式開始編寫之前跨嘉,我們必須要想好川慌,哪一些 Mixin 是我們需要編寫的,哪一些邏輯是必須要寫到視圖函數(shù)中偿荷。
首先窘游,凡是對(duì)于有查詢動(dòng)作的請(qǐng)求,我們都有一個(gè)從數(shù)據(jù)庫(kù)中提取查詢集的過程跳纳,所以我們需要編
寫一個(gè)提取查詢集的 Mixin 忍饰。
第二,對(duì)于查詢集來說寺庄,有時(shí)候我們需要的是整個(gè)查詢集艾蓝,有時(shí)候只是需要一個(gè)單一的查詢實(shí)例,比如在更新和刪除的時(shí)候斗塘,我們都是在對(duì)一個(gè)實(shí)例進(jìn)行操作赢织。所以我們還需要編寫一個(gè)能夠提取出單一實(shí)例的 Mixin 。
第三馍盟,對(duì)于 API 的通用操作來說于置,根據(jù) REST 原則,每個(gè)請(qǐng)求都有自己的對(duì)應(yīng)動(dòng)作贞岭,比如 put 對(duì)應(yīng)的是修改動(dòng)作八毯,post 對(duì)應(yīng)的是創(chuàng)建動(dòng)作,delete 對(duì)應(yīng)的是刪除動(dòng)作瞄桨,所以我們需要為這些通用的 API 動(dòng)作一一編寫 Mixin 话速。
第四,正如第三條考慮到的那樣芯侥, API 的不同請(qǐng)求是有自己對(duì)應(yīng)的默認(rèn)動(dòng)作的泊交。如果我們的視圖就是想簡(jiǎn)單的使用他們的默認(rèn)動(dòng)作乳讥,也就是 post 是創(chuàng)建動(dòng)作,put 是修改動(dòng)作廓俭,我們希望視圖函數(shù)能自己將這些請(qǐng)求自己就映射到這些默認(rèn)動(dòng)作上云石,這樣在之后的開發(fā)我們就可以什么都不用做了,連最基本的 get post 視圖方法都不需要我們編寫白指。所以我們需要編寫一個(gè)方法映射 Mixin 留晚。
最后,就我們的應(yīng)用而言告嘲,我們應(yīng)用是為了提供在線解釋器服務(wù)错维,所以會(huì)有一個(gè)執(zhí)行代碼的功能,雖然到目前橄唬,這個(gè)功能的核心函數(shù)執(zhí)行的代碼很簡(jiǎn)單赋焕,但是誰能保證他一直都是這樣簡(jiǎn)單呢?所以為了保持良好的視圖解耦性仰楚,我們也需要把這部分的代碼單獨(dú)獨(dú)立出來成為一個(gè) Mixin 隆判。
現(xiàn)在,讓我們開始編寫我們的 Mixin 僧界。我們編寫 Mixin 的活動(dòng)都會(huì)在 mixins.py
中進(jìn)行侨嘀。
首先,在頂部引入需要用到的包
from django.db import models, IntegrityError # 查詢失敗時(shí)我們需要用到的模塊
import subprocess # 用于運(yùn)行代碼
from django.http import Http404 # 當(dāng)查詢操作失敗時(shí)返回404響應(yīng)
IntegrityError
錯(cuò)誤會(huì)在像數(shù)據(jù)庫(kù)寫入數(shù)據(jù)創(chuàng)建不成功時(shí)被拋出捂襟,這是我們需要捕捉并做出響應(yīng)的錯(cuò)誤咬腕。
獲取查詢集 Mixin 的編寫:
class APIQuerysetMinx(object):
"""
用于獲取查詢集。在使用時(shí)葬荷,model 屬性和 queryset 屬性必有其一涨共。
:model: 模型類
:queryet: 查詢集
"""
model = None
queryset = None
def get_queryset(self):
"""
獲取查詢集。若有 model 參數(shù)宠漩,則默認(rèn)返回所有的模型查詢實(shí)例举反。
:return: 查詢集
"""
# 檢驗(yàn)相應(yīng)參數(shù)是否被傳入,若沒有傳入則拋出錯(cuò)誤
assert self.model or self.queryset, 'No queryset fuound.'
if self.queryset:
return self.queryset
else:
return self.model.objects.all()
可以看到扒吁,我們的 Mixin 的設(shè)計(jì)很簡(jiǎn)單火鼻,只是為子類提供了兩個(gè)參數(shù) queryset
和model
,并且 get_queryset
這個(gè)方法會(huì)使用這兩個(gè)屬性返回相應(yīng)的所有的實(shí)例查詢集雕崩。我們可以這樣使用它:
class GETView(APIQuerysetMinx, View):
model = MyModel
def get(self, *args, **kwargs):
return self.get_queryset()
這樣我們的視圖是不是看起來就方便魁索,清晰了很多,視圖邏輯和具體的操作邏輯相分離晨逝,這樣方便別人閱讀自己的代碼,一看就知道是什么意思懦铺。在之后的 Mixin 使用也是同理的捉貌。
編寫獲取單一實(shí)例的 Mixin :
class APISingleObjectMixin(APIQuerysetMinx):
"""
用于獲取當(dāng)前請(qǐng)求中的實(shí)例。
:lookup_args: list, 用來規(guī)定查詢參數(shù)的參數(shù)列表。默認(rèn)為 ['pk','id]
"""
lookup_args = ['pk', 'id']
def get_object(self):
"""
通過查詢 lookup_args 中的參數(shù)值來返回當(dāng)前請(qǐng)求實(shí)例趁窃。當(dāng)獲取到參數(shù)值時(shí)牧挣,則停止
對(duì)之后的參數(shù)查詢。參數(shù)順序很重要醒陆。
:return: 一個(gè)單一的查詢實(shí)例
"""
queryset = self.get_queryset() # 獲取查詢集
for key in self.lookup_args:
if self.kwargs.get(key):
id = self.kwargs[key] # 獲取查詢參數(shù)值
try:
instance = queryset.get(id=id) # 獲取當(dāng)前實(shí)例
return instance # 實(shí)例存在則返回實(shí)例
except models.ObjectDoesNotExist: # 捕捉實(shí)例不存在異常
raise Http404('No object found.') # 拋出404異常響應(yīng)
raise Http404('No object found.') # 若遍歷所以參數(shù)都未捕捉到值瀑构,則拋出404異常響應(yīng)
我們可以看到,獲取單一實(shí)例的方式是從傳入視圖函數(shù)的關(guān)鍵字參數(shù)kwargs
中獲取對(duì)應(yīng)的 id
或者 pk
然后從查詢集中獲取相應(yīng)的實(shí)例刨摩。并且我們還可以靈活的配置查詢的關(guān)鍵詞是什么寺晌,這個(gè) Mixin 還很方便使用的。
接下來我們需要編寫的是獲取列表的 Mixin
class APIListMixin(APIQuerysetMinx):
"""
API 中的 list 操作澡刹。
"""
def list(self, fields=None):
"""
返回查詢集響應(yīng)
:param fields: 查詢集中希望被實(shí)例化的字段
:return: JsonResopnse
"""
return self.response(
queryset=self.get_queryset(),
fields=fields) # 返回響應(yīng)
我們可以看到呻征,我們只是簡(jiǎn)單的返回了查詢集,并且默認(rèn)的方法還支持傳入需要的序列化的字段罢浇。
執(zhí)行創(chuàng)建操作的 Mixin:
class APICreateMixin(APIQuerysetMinx):
"""
API 中的 create 操作
"""
def create(self, create_fields=None):
"""
使用傳入的參數(shù)列表從 POST 值中獲取對(duì)應(yīng)參數(shù)值陆赋,并用這個(gè)值創(chuàng)建實(shí)例,
成功創(chuàng)建則返回創(chuàng)建成功響應(yīng)嚷闭,否則返回創(chuàng)建失敗響應(yīng)攒岛。
:param create_fields: list, 希望被創(chuàng)建的字段。
若為 None, 則默認(rèn)為 POST 上傳的所有字段胞锰。
:return: JsonResponse
"""
create_values = {}
if create_fields: # 如果傳入了希望被創(chuàng)建的字段灾锯,則從 POST 中獲取每個(gè)值
for field in create_fields:
create_values[field]=self.request.POST.get(field)
else:
for key in self.request.POST: # 若未傳入希望被創(chuàng)建字段,則默認(rèn)為 POST 上傳的
# 字段都為創(chuàng)建字段胜蛉。
create_values[key]=self.request.POST.get(key);
queryset = self.get_queryset() # 獲取查詢集
try:
instance = queryset.create(**create_values) # 利用查詢集來創(chuàng)建實(shí)例
except IntegrityError: # 捕捉創(chuàng)建失敗異常
return self.response(status='Failed to Create.') # 返回創(chuàng)建失敗響應(yīng)
return self.response(status='Successfully Create.') # 創(chuàng)建成功則返回創(chuàng)建成功響應(yīng)
我們可以看到挠进,作為 API 的 Mixin ,創(chuàng)建的默認(rèn)動(dòng)作已經(jīng)是從 POST 中獲取相應(yīng)的數(shù)據(jù)誊册,這就不用我們把提取數(shù)據(jù)的邏輯硬編碼在視圖中了领突,而且考慮到了足夠多的情況。并且我們還手動(dòng)的傳入了 status
案怯,方便前端開發(fā)能夠清楚的知道操作是否成功君旦。
實(shí)例查詢 Mixin:
class APIDetailMixin(APISingleObjectMixin):
"""
API 操作中查詢實(shí)例操作
"""
def detail(self, fields=None):
"""
返回當(dāng)前請(qǐng)求中的實(shí)例
:param fields: 希望被返回實(shí)例中哪些字段被實(shí)例化
:return: JsonResponse
"""
return self.response(
queryset=[self.get_object()],
fields=fields)
同理,我們只是簡(jiǎn)單的調(diào)用了 get_object
方法嘲碱,并沒有做其它的處理金砍。
更新 Mixin:
class APIUpdateMixin(APISingleObjectMixin):
"""
API 中更新實(shí)例操作
"""
def update(self, update_fields=None):
"""
更新當(dāng)前請(qǐng)求中實(shí)例。更新成功則返回成功響應(yīng)麦锯。否則恕稠,返回更新失敗響應(yīng)。
若傳入 updata_fields 更新字段列表扶欣,則只會(huì)從 PUT 上傳值中獲取這個(gè)列表中的字段鹅巍,
否則默認(rèn)為更新 POST 上傳值中所有的字段千扶。
:param update_fields: list, 實(shí)例需要被更新的字段
:return: JsonResponse
"""
instance = self.get_object() # 獲取當(dāng)前請(qǐng)求中的實(shí)例
if not update_fields: # 若無字段更新列表,則默認(rèn)為 PUT 上傳值的所有數(shù)據(jù)
update_fields=self.request.PUT.keys()
try: # 迭代更新實(shí)例字段
for field in update_fields:
update_value = self.request.PUT.get(field) # 從 PUT 中取值
setattr(instance, field, update_value) # 更新字段
instance.save() # 保存實(shí)例更新
except IntegrityError: # 捕捉更新錯(cuò)誤
return self.response(status='Failed to Update.') # 返回更新失敗響應(yīng)
return self.response(
status='Successfully Update')# 更新成功則返回更新成功響應(yīng)
setattr
的作用就是給一個(gè)對(duì)象設(shè)置屬性骆捧,當(dāng)查詢的實(shí)例被找到之后澎羞,我們采用這種方法來給實(shí)例更新值。因?yàn)槲覀冊(cè)谶@種情況下不能使用 .
路徑符來訪問字段敛苇,因?yàn)槲覀儾恢烙心男┳侄螘?huì)被更新妆绞。同時(shí),作為 API 的 Mixin 枫攀,更新時(shí)獲取數(shù)據(jù)的地方已經(jīng)默認(rèn)為從 PUT 請(qǐng)求中獲取數(shù)據(jù)括饶。
刪除操作 Mixin
class APIDeleteMixin(APISingleObjectMixin):
"""
API 刪除實(shí)例操作
"""
def remove(self):
"""
刪除當(dāng)前請(qǐng)求中的實(shí)例。刪除成功則返回刪除成功響應(yīng)脓豪。
:return: JsonResponse
"""
instance = self.get_object() # 獲取當(dāng)前實(shí)例
instance.delete() # 刪除實(shí)例
return self.response(status='Successfully Delete') # 返回刪除成功響應(yīng)
需要注意的是巷帝,我們的方法名不叫 delete
,而是 remove
扫夜,這是因?yàn)?delete
是請(qǐng)求方法名楞泼,我們不能占用它。
運(yùn)行代碼的 Mixin:
class APIRunCodeMixin(object):
"""
運(yùn)行代碼操作
"""
def run_code(self, code):
"""
運(yùn)行所給的代碼笤闯,并返回執(zhí)行結(jié)果
:param code: str, 需要被運(yùn)行的代碼
:return: str, 運(yùn)行結(jié)果
"""
try:
output = subprocess.check_output(['python', '-c', code], # 運(yùn)行代碼
stderr=subprocess.STDOUT, # 重定向錯(cuò)誤輸出流到子進(jìn)程
universal_newlines=True, # 將返回執(zhí)行結(jié)果轉(zhuǎn)換為字符串
timeout=30) # 設(shè)定執(zhí)行超時(shí)時(shí)間
except subprocess.CalledProcessError as e: # 捕捉執(zhí)行失敗異常
output = e.output # 獲取子進(jìn)程報(bào)錯(cuò)信息
except subprocess.TimeoutExpired as e: # 捕捉超時(shí)異常
output = '\r\n'.join(['Time Out!', e.output]) # 獲取子進(jìn)程報(bào)錯(cuò)堕阔,并添加運(yùn)行超時(shí)提示
return output # 返回執(zhí)行結(jié)果
這個(gè)也不多說,就只是簡(jiǎn)單的把之前的函數(shù)式更改為了類颗味。不過要注意的是超陆,如果你用的是 py2,subprocess
有的屬性的引用方式會(huì)和 3 有寫不同浦马,大家可以自行去查閱如何正確引入相關(guān)的屬性时呀。
前幾個(gè) Mixin 都沒有很詳細(xì)的說,下面這個(gè) Mixin 我們需要詳細(xì)的說明晶默。
class APIMethodMapMixin(object):
"""
將請(qǐng)求方法映射到子類屬性上
:method_map: dict, 方法映射字典谨娜。
如將 get 方法映射到 list 方法,其值則為 {'get':'list'}
"""
method_map = {}
def __init__(self,*args,**kwargs):
"""
映射請(qǐng)求方法磺陡。會(huì)從傳入子類的關(guān)鍵字參數(shù)中尋找 method_map 參數(shù)趴梢,期望值為 dict類型。尋找對(duì)應(yīng)參數(shù)值币他。
若在類屬性和傳入?yún)?shù)中同時(shí)定義了 method_map 坞靶,則以傳入?yún)?shù)為準(zhǔn)。
:param args: 傳入的位置參數(shù)
:param kwargs: 傳入的關(guān)鍵字參數(shù)
"""
method_map=kwargs['method_map'] if kwargs.get('method_map',None) \
else self.method_map # 獲取 method_map 參數(shù)
for request_method, mapped_method in method_map.items(): # 迭代映射方法
mapped_method = getattr(self, mapped_method) # 獲取被映射方法
method_proxy = self.view_proxy(mapped_method) # 設(shè)置對(duì)應(yīng)視圖代理
setattr(self, request_method, method_proxy) # 將視圖代碼映射到視圖代理方法上
super(APIMethodMapMixin,self).__init__(*args,**kwargs) # 執(zhí)行子類的其他初始化
def view_proxy(self, mapped_method):
"""
代理被映射方法蝴悉,并代理接收傳入視圖函數(shù)的其他參數(shù)彰阴。
:param mapped_method: 被代理的映射方法
:return: function, 代理視圖函數(shù)。
"""
def view(*args, **kwargs):
"""
視圖的代理方法
:param args: 傳入視圖函數(shù)的位置參數(shù)
:param kwargs: 傳入視圖函數(shù)的關(guān)鍵字參數(shù)
:return: 返回執(zhí)行被映射方法
"""
return mapped_method() # 返回執(zhí)行代理方法
return view # 返回代理視圖
首先拍冠,大家不要被嚇到尿这。我們慢慢來分析廉丽。
我們先給子類提供了一個(gè) method_map
的屬性,這是一個(gè)字典妻味,子類可以通過給這個(gè)字典配置相應(yīng)的值來使用我們的 APIMethodMapMixin
,字典的鍵為請(qǐng)求的方法名欣福,值為要執(zhí)行的操作责球。。接下來看看 __init__
方法拓劝,首先雏逾,會(huì)在子類視圖實(shí)例化的時(shí)候?qū)ふ?method_map
參數(shù),如果找到了就會(huì)以這個(gè)參數(shù)作為方法映射的字典郑临,在子類中編寫的配置就不會(huì)生效了栖博。也就是說:
# views.py
class ExampleView(APIMethodMapMixin, APIView):
method_map = {'get':'list','put':'update'}
# urls.py
urlpatterns = [url(r'^$',ExampleView.as_view(method_map={'get':'list'}))]
如果在初始化視圖類的時(shí)候也傳入了 method_map
參數(shù),那我們定義在 ExampleView
中的屬性就沒用了厢洞,視圖會(huì)以初始化時(shí)的參數(shù)作為最終標(biāo)準(zhǔn)仇让。由于我們的字典只是一個(gè)字符串,我們要做的是把子類的對(duì)應(yīng)操作方法和請(qǐng)求方法對(duì)應(yīng)起來躺翻,所以我們首先使用 getattr
來獲取子類的響應(yīng)操作的方法丧叽,然后利用了 view_proxy
代理了視圖方法。為什么我們需要這個(gè)代理方法公你?原因很簡(jiǎn)單踊淳,因?yàn)樵谀J(rèn)的視圖中,View
會(huì)向視圖傳遞參數(shù)陕靠,然而迂尝,我們的操作方法,他們的參數(shù)和被傳入視圖的參數(shù)是截然不同的剪芥,所以我們需要使用一個(gè)函數(shù)來代理接收這些參數(shù)垄开,這個(gè)函數(shù)就是我們視圖代理函數(shù)返回的 view
函數(shù),這個(gè)函數(shù)會(huì)接收所有傳向視圖的參數(shù)粗俱,然后不對(duì)這些參數(shù)做出處理说榆,只是簡(jiǎn)單的調(diào)用被映射的方法。
python 基礎(chǔ)很不錯(cuò)的同學(xué)應(yīng)該已經(jīng)發(fā)現(xiàn)了寸认,我們的 view_proxy
的寫法不就是一個(gè)裝飾器的寫法嗎签财?是的,裝飾器也是這樣寫的偏塞,只是我們?cè)?__init__
中手動(dòng)調(diào)用了它而已唱蒸,平時(shí)我們用 @
來使用裝飾器和我們手動(dòng)調(diào)用的過程是完全相同的。在最后灸叼,我們把操作方法設(shè)置為了請(qǐng)求對(duì)應(yīng)方法的值神汹,這樣我們就可以成功的調(diào)用相應(yīng)的操作了庆捺。別忘了在最后調(diào)用 super
哦。
以上便是我們所有的 Mixin 的編寫∑ㄎ海現(xiàn)在滔以,我們來完成編寫 views.py
。
首先氓拼,在頂上引入這些包:
from django.views import View # 引入最基本的類視圖
from django.http import JsonResponse, HttpResponse # 引入現(xiàn)成的響應(yīng)類
from django.core.serializers import serialize # 引入序列化函數(shù)
from .models import CodeModel # 引入 Code 模型你画,記得加個(gè) `.` 哦。
import json # 引入 json 庫(kù)桃漾,我們會(huì)用它來處理 json 字符串坏匪。
from .mixins import APIDetailMixin, APIUpdateMixin, \
APIDeleteMixin, APIListMixin, APIRunCodeMixin, \
APICreateMixin, APIMethodMapMixin, APISingleObjectMixin # 引入我們編寫的所有 Mixin
我們的核心 API:
class APICodeView(APIListMixin, # 獲取列表
APIDetailMixin, # 獲取當(dāng)前請(qǐng)求實(shí)例詳細(xì)信息
APIUpdateMixin, # 更新當(dāng)前請(qǐng)求實(shí)例
APIDeleteMixin, # 刪除當(dāng)前實(shí)例
APICreateMixin, # 創(chuàng)建新的的實(shí)例
APIMethodMapMixin, # 請(qǐng)求方法與資源操作方法映射
APIView): # 記得在最后繼承 APIView
model = CodeModel # 傳入模型
def list(self): # 這里僅僅是簡(jiǎn)單的給父類的 list 函數(shù)傳參。
return super(APICodeView, self).list(fields=['name'])
有了 Mixin 是不是很方便撬统,這種感覺不要太爽适滓。
接下來完成運(yùn)行代碼的 API :
class APIRunCodeView(APIRunCodeMixin,
APISingleObjectMixin,
APIView):
model = CodeModel # 傳入模型
def get(self, request, *args, **kwargs):
"""
GET 請(qǐng)求僅對(duì)能獲取到 pk 值的 url 響應(yīng)
:param request: 請(qǐng)求對(duì)象
:param args: 位置參數(shù)
:param kwargs: 關(guān)鍵字參數(shù)
:return: JsonResponse
"""
instance = self.get_object() # 獲取對(duì)象
code = instance.code # 獲取代碼
output = self.run_code(code) # 運(yùn)行代碼
return self.response(output=output, status='Successfully Run') # 返回響應(yīng)
def post(self, request, *args, **kwargs):
"""
POST 請(qǐng)求可以被任意訪問,并會(huì)檢查 url 參數(shù)中的 save 值恋追,如果 save 為 true 則會(huì)
保存上傳代碼凭迹。
:param request: 請(qǐng)求對(duì)象
:param args: 位置參數(shù)
:param kwargs: 關(guān)鍵字參數(shù)
:return: JsonResponse
"""
code = self.request.POST.get('code') # 獲取代碼
save = self.request.GET.get('save') == 'true' # 獲取 save 參數(shù)值
name = self.request.POST.get('name') # 獲取代碼片段名稱
output = self.run_code(code) # 運(yùn)行代碼
if save: # 判斷是否保存代碼
instance = self.model.objects.create(name=name, code=code)
return self.response(status='Successfully Run and Save',
output=output) # 返回響應(yīng)
def put(self, request, *args, **kwrags):
"""
PUT 請(qǐng)求僅對(duì)更改操作作出響應(yīng)
:param request: 請(qǐng)求對(duì)象
:param args: 位置參數(shù)
:param kwrags: 關(guān)鍵字參數(shù)
:return: JsonResponse
"""
code = self.request.PUT.get('code') # 獲取代碼
name = self.request.PUT.get('name') # 獲取代碼片段名稱
save = self.request.GET.get('save') == 'true' # 獲取 save 參數(shù)值
output = self.run_code(code) # 運(yùn)行代碼
if save: # 判斷是否需要更改代碼
instance = self.get_object() # 獲取當(dāng)前實(shí)例
setattr(instance, 'name', name) # 更改名字
setattr(instance, 'code', code) # 更改代碼
instance.save()
return self.response(status='Successfully Run and Change',
output=output) # 返回響應(yīng)
值得注意的是,我們使用了一個(gè) save
參數(shù)來判斷上傳的代碼是否需要保存苦囱,因?yàn)樯蟼鞣绞蕉际?POST 我們?cè)谶@種情況下就需要增加新的參數(shù)來決定是否需要保存蕊苗。而且由于我們沒有怎么使用 Mixin ,所有的字段我們都是手動(dòng)提取的沿彭,所有的操作過程都是我們自己寫的朽砰,就顯得有點(diǎn)笨搓搓的。由此可見 Mixin 是多么的好用喉刘。
別忘了瞧柔,我們還要提供靜態(tài)文件服務(wù):
# 主頁視圖
def home(request):
"""
讀取 'index.html' 并返回響應(yīng)
:param request: 請(qǐng)求對(duì)象
:return: HttpResponse
"""
with open('frontend/index.html', 'rb') as f:
content = f.read()
return HttpResponse(content)
# 讀取 js 視圖
def js(request, filename):
"""
讀取 js 文件并返回 js 文件響應(yīng)
:param request: 請(qǐng)求對(duì)象
:param filename: str-> 文件名
:return: HttpResponse
"""
with open('frontend/js/{}'.format(filename), 'rb') as f:
js_content = f.read()
return HttpResponse(content=js_content,
content_type='application/javascript') # 返回 js 響應(yīng)
# 讀取 css 視圖
def css(request, filename):
"""
讀取 css 文件,并返回 css 文件響應(yīng)
:param request: 請(qǐng)求對(duì)象
:param filename: str-> 文件名
:return: HttpResponse
"""
with open('frontend/css/{}'.format(filename), 'rb') as f:
css_content = f.read()
return HttpResponse(content=css_content,
content_type='text/css') # 返回 css 響應(yīng)
在靜態(tài)文件的響應(yīng)中需要把響應(yīng)頭更改為正確的響應(yīng)頭睦裳,不然瀏覽器就不認(rèn)識(shí)你傳回去的是什么靜態(tài)件了造锅。
最后,按照我們之前的設(shè)計(jì)廉邑,完成我們的 API URL 配置哥蔚。
編輯你的 urls.py
,這個(gè)文件是和你的 settings.py
在同一個(gè)目錄哦
# 這是我們的 URL 入口配置蛛蒙,我們直接將入口配置到具體的 URL 上糙箍。
from django.conf.urls import url, include # 引入需要用到的配置函數(shù)
# include 用來引入其他的 URL 配置。參數(shù)可以是個(gè)路徑字符串牵祟,也可以是個(gè) url 對(duì)象列表
from online_intepreter_app.views import APICodeView, APIRunCodeView, home, js, css # 引入我們的視圖函數(shù)
from django.views.decorators.csrf import csrf_exempt # 同樣的深夯,我們不需要使用 csrf 功能。
# 注意我們這里的 csrf_exempt 的用法,這和將它作為裝飾器使用的效果是一樣的
# 普通的集合操作 API
generic_code_view = csrf_exempt(APICodeView.as_view(method_map={'get': 'list',
'post': 'create'})) # 傳入自定義的 method_map 參數(shù)
# 針對(duì)某個(gè)對(duì)象的操作 API
detail_code_view = csrf_exempt(APICodeView.as_view(method_map={'get': 'detail',
'put': 'update',
'delete': 'remove'}))
# 運(yùn)行代碼操作 API
run_code_view = csrf_exempt(APIRunCodeView.as_view())
# Code 應(yīng)用 API 配置
code_api = [
url(r'^$', generic_code_view, name='generic_code'), # 集合操作
url(r'^(?P<pk>\d*)/$', detail_code_view, name='detail_code'), # 訪問某個(gè)特定對(duì)象
url(r'^run/$', run_code_view, name='run_code'), # 運(yùn)行代碼
url(r'^run/(?P<pk>\d*)/$', run_code_view, name='run_specific_code') # 運(yùn)行特定代碼
]
api_v1 = [url('^codes/', include(code_api))] # API 的 v1 版本
api_versions = [url(r'^v1/', include(api_v1))] # API 的版本控制入口 URL
urlpatterns = [
url(r'^api/', include(api_versions)), # API 訪問 URL
url(r'^$', home, name='index'), # 主頁視圖
url(r'^js/(?P<filename>.*\.js)$', js, name='js'), # 訪問 js 文件咕晋,記得雹拄,最后沒有 /
url(r'^css/(?P<filename>.*\.css)$', css, name='css') # 訪問 css 文件,記得掌呜,最后沒有 /
]
記得滓玖,在靜態(tài)文件服務(wù)的 url 后面沒有 /
,因?yàn)樵谇岸艘玫臅r(shí)候是不會(huì)加 /
的质蕉,這是對(duì)一個(gè)文件的直接訪問呢撞。
最后,回到項(xiàng)目路徑下饰剥,運(yùn)行:
python manage.py makemigrations
python manage.py migrate
創(chuàng)建好數(shù)據(jù)庫(kù)之后我們就可以進(jìn)入前端的開發(fā)了。
我們的后端就算完成了摧阅。休息一下汰蓉,準(zhǔn)備向前端進(jìn)發(fā)。
前端開發(fā)
我們把工作路徑切換到 frontend
下棒卷。這一次我們的重點(diǎn)放在 js 的編寫上顾孽。這一次的編寫沒有什么難點(diǎn),重點(diǎn)是在于對(duì)前端原理的理解和應(yīng)用上比规,代碼不難若厚,但是希望大家著重的理解其中的設(shè)計(jì)模式。最好的方式就是自己敲一遍代碼蜒什。只有跟著敲一次才知道自己哪里有問題测秸。
首先把我們需要的 js 和 css 都放在對(duì)應(yīng)的文件下。大家可以去我的 github 把 bootstrap.js 和 bootstrap.css 以及 jquery.js 下載下來灾常,把 js 文件放在 js
路徑下霎冯,css 放在 css
路徑下。準(zhǔn)備工作做完了钞瀑。
首先編寫我們的主頁 html 沈撞,這次的 html 做了一些改動(dòng),并且添加了新的元素雕什。所以大家不要直接使用上一次的 index.html
缠俺,應(yīng)該自己敲一次,才能注意到一些小細(xì)節(jié)贷岸。有了第一章的經(jīng)驗(yàn)壹士,我就不多說了。
編輯你的 index.html
:
<!DOCTYPE html>
<html lang="ch">
<head>
<meta charset="UTF-8">
<title>在線 Python 解釋器</title>
<link href="css/bootstrap.css" rel="stylesheet">
<link rel="stylesheet" href="css/main.css" rel="stylesheet"> <!--引入我們寫的 css-->
</head>
<body>
<div class="continer-fluid"><!--使用 fluid 類屬性偿警,讓主頁填滿整個(gè)瀏覽器-->
<div class="row text-center h1">
在線 Python 解釋器
</div>
<hr>
<div class="row">
<div class="col-md-3">
<table class="table table-striped"><!--文件列表-->
<thead> <!--標(biāo)題-->
<tr>
<th class="text-center">文件名</th> <!--標(biāo)題居中-->
<th class="text-center">選項(xiàng)</th> <!--標(biāo)題居中-->
</tr>
</thead>
<tbody></tbody> <!-- 列表實(shí)體墓卦,由 js 渲染列表實(shí)體-->
</table>
</div>
<div class="col-md-9">
<div class="container-fluid">
<div class="col-md-6">
<p class="text-center h3">請(qǐng)?jiān)谙路捷斎氪a:</p>
<textarea class="form-control" id="code-input"></textarea> <!--代碼輸入-->
<label for="code-name-input">代碼片段名</label>
<p class="text-info">如需保存代碼,建議輸入代碼片段名</p>
<input type="text" class="form-control" id="code-name-input">
<hr>
<div id="code-options"
style="display: flex;
justify-content: space-around;
flex-wrap: wrap" > <!--代碼選項(xiàng)户敬,采用 flex 布局落剪,使每個(gè)選項(xiàng)都均勻分布-->
</div>
</div>
<p class="text-center h3">輸出</p>
<div class="col-md-6">
<textarea id="code-output" disabled
class="form-control text-center"></textarea><!--結(jié)果輸出-->
</div>
</div>
</div>
</div>
</div>
<script src="js/jquery.js"></script>
<script src="js/bootstrap.js"></script>
<script src="js/main.js"></script> <!--引入我們的 js 文件-->
</body>
</html>
main.css
:
#code-input, #code-output {
resize: none;
font-size: 25px;
} /*設(shè)置輸入輸出框的字體大小睁本,禁用他們的 resize 功能*/
接下來到了前端開發(fā)中的重點(diǎn)了,接下來的開發(fā)都會(huì)在 main.js
中進(jìn)行忠怖。
在第一章的開發(fā)中呢堰,我們的 API 很簡(jiǎn)單,就一個(gè) POST 凡泣,但是這一次枉疼,我們的 API 多,而且比較復(fù)雜鞋拟,甚至還有 GET 參數(shù)骂维,所以我們需要管理我們的 API ,所以硬編碼 API 一定是行不通了贺纲,硬編碼 API 不僅會(huì)導(dǎo)致靈活性不夠航闺,還會(huì)增加手動(dòng)輸入錯(cuò)誤的幾率。所以我們這樣來管理我們的 API:
const api = {
v1: { // api 版本號(hào)
codes: { // api v1 版本下的 code api猴誊。
list: function () { //獲取實(shí)例查詢集
return '/api/v1/codes/'
},
detail: function (pk) { // 獲取單一實(shí)例
return `/api/v1/codes/${pk}/`
},
create: function () { // 創(chuàng)建實(shí)例
return `/api/v1/codes/`
},
update: function (pk) { // 更新實(shí)例
return `/api/v1/codes/${pk}/`
},
remove: function (pk) { //刪除實(shí)例
return `/api/v1/codes/${pk}/`
},
run: function () { //運(yùn)行代碼
return '/api/v1/codes/run/'
},
runSave: function () {// 保存并運(yùn)行代碼
return '/api/v1/codes/run/?save=true'
},
runSpecific: function (pk) { // 運(yùn)行特定代碼實(shí)例
return `/api/v1/codes/run/${pk}/`
},
runSaveSpecific: function (pk) { // 運(yùn)行并保存特定代碼實(shí)例
return `/api/v1/codes/run/${pk}/?save=true`
}
}
}
};
不要被嚇到潦刃,或許有的同學(xué)會(huì)覺得使用函數(shù)來返回 API 是多此一舉的。但是我們想想懈叹,如果你的代碼特別多乖杠,特別長(zhǎng),你會(huì)不會(huì)寫著寫著就忘了自己調(diào)用的 API 是干什么的澄成?所以為了保證良好的語義性胧洒,我們需要有良好的層級(jí)結(jié)構(gòu)和良好的命名規(guī)則。使用函數(shù)不僅可以正確的生成含有參數(shù)的 URL 而且也方便我們將來做進(jìn)一步的改進(jìn)墨状。如果哪一天 API 發(fā)生變化了略荡,我們直接在函數(shù)中做出對(duì)應(yīng)的修改就好了,不需要像硬編碼那樣挨著挨著更改歉胶。
接下來我們的核心概念來了 —— state
汛兜。在“上”我們已經(jīng)知道了狀態(tài)的概念和store
怕膛,就是用來儲(chǔ)存狀態(tài)的東西它浅。所以我們像這樣來定義我們的狀態(tài)策治。
let store = {
list: { //列表狀態(tài)
state: undefined,
changed: false
},
detail: { //特定實(shí)例狀態(tài)
state: undefined,
changed: false
},
output: { //輸出狀態(tài)
state: undefined,
changed: false
}
};
我們把不同的狀態(tài)放在 store 每個(gè)狀態(tài)有 state 和 changed 屬性缀壤,state 用來儲(chǔ)存 UI 相關(guān)聯(lián)的變量信息龄句,changed 作為狀態(tài)是否改變的信號(hào)逆皮。UI 只需要監(jiān)聽 chagned 變量胁镐,當(dāng) changed 為 true 時(shí)才讀取并改變狀態(tài)只估。要是你忘了什么是“狀態(tài)”臼氨,趕緊回去看看上一個(gè)部分吧掺喻。
我們已經(jīng)定義完了 API 和 狀態(tài),但是真正向后端發(fā)起請(qǐng)求動(dòng)作的函數(shù)還都沒有完成。接著在下面寫我們的動(dòng)作函數(shù)感耙。
這些動(dòng)作負(fù)責(zé)調(diào)用 API 褂乍,并接受 API 返回的數(shù)據(jù),然后將這些數(shù)據(jù)保存進(jìn) store 中即硼。注意逃片,在修改完?duì)顟B(tài)之后,記得將狀態(tài)的 changed 屬性改為 true ,不然狀態(tài)不會(huì)刷新到監(jiān)聽的 UI 上只酥。
得到單一的實(shí)例褥实,因?yàn)槲覀?Django 模型序列化的之后的格式不是很符合我們的要求,所以我們需要做一些處理裂允。模型字段序列化之后是這樣的损离。
{'model':'app.modelName','pk':'pk',fields:[modelFields]}
比如我們的 Code 模型,一個(gè)實(shí)例序列化之后值這樣的:
{'model':'online_intepreter_app.Code',pk:'1', fields[{'name':'name','code':'code'}]}
如果是查詢集绝编,則返回的就是想上面一樣的對(duì)象列表僻澎。
我們需要把實(shí)例的 pk 和字段給放到一起。
//從后端返回的數(shù)據(jù)中瓮增,把實(shí)例相關(guān)的數(shù)據(jù)處理成我們期望的形式,好方便我們調(diào)用
function getInstance(data) {
let instance = data.fields;
instance.pk = data.pk;
return instance
}
獲取 code 列表:
//獲取 code 列表哩俭,更改 list 狀態(tài)
function getList() {
$.getJSON({
url: api.v1.codes.list(),
success: function (data) {
store.list.state = data.instances;
store.list.changed = true;
}
})
}
大家已經(jīng)注意到了绷跑,請(qǐng)求完成之后,改變的狀態(tài)值凡资,并且也發(fā)出了響應(yīng)的狀態(tài)更改信號(hào)砸捏,就是把changed
更改為true
。
創(chuàng)建實(shí)例動(dòng)作
function create(code, name) {
$.post({
url: api.v1.codes.create(),
data: {'code': code, 'name': name},
dataType: 'json',
success: function (data) {
getList();
alert('保存成功隙赁!');
}
})
}
在創(chuàng)建完成后垦藏,我們又更新了 list
狀態(tài),這樣就可以實(shí)時(shí)刷新我們的 list
了伞访。
更新實(shí)例動(dòng)作
function update(pk, code, name) {
$.ajax({
url: api.v1.codes.update(pk),
type: 'PUT',
data: {'code': code, 'name': name},
dataType: 'json',
success: function (data) {
getList();
alert('更新成功掂骏!');
}
})
}
同理,我們?cè)诟峦瓿珊笠菜⑿铝?list
厚掷。
獲取實(shí)例動(dòng)作
function getDetail(pk) {
$.getJSON({
url: api.v1.codes.detail(pk),
success: function (data) {
let detail = getInstance(data.instances[0]);
store.detail.state = detail;
store.detail.changed = true;
}
})
}
我們?cè)讷@取實(shí)例的時(shí)候使用了 getInstance
弟灼,保證獲取的實(shí)例是符合我們要求的。
刪除實(shí)例
function remove(pk) {
$.ajax({
url: api.v1.codes.remove(pk),
type: 'DELETE',
dataType: 'json',
success: function (data) {
getList();
alert('刪除成功冒黑!');
}
})
}
我們刪除實(shí)例的動(dòng)作還是叫做 remove
田绑,不叫 delete
是因?yàn)?delete
是默認(rèn)關(guān)鍵字。
運(yùn)行代碼的幾個(gè)動(dòng)作也是和上面同理:
function run(code) {
$.post({
url: api.v1.codes.run(),
dataType: 'json',
data: {'code': code},
success: function (data) {
let output = data.output;
store.output.state = output;
store.output.changed = true;
}
})
}
//運(yùn)行保存代碼抡爹,并刷新 output 和 list 狀態(tài)掩驱。
function runSave(code, name) {
$.post({
url: api.v1.codes.runSave(),
dataType: 'json',
data: {'code': code, 'name': name},
success: function (data) {
let output = data.output;
store.output.state = output;
store.output.changed = true;
getList();
alert('保存成功!');
}
})
}
//運(yùn)行特定的代碼實(shí)例,并刷新 output 狀態(tài)
function runSpecific(pk) {
$.get({
url: api.v1.codes.runSpecific(pk),
dataType: 'json',
success: function (data) {
let output = data.output;
store.output.state = output;
store.output.changed = true;
}
})
}
//運(yùn)行并保存特定代碼實(shí)例欧穴,并刷新 output 和 list 狀態(tài)
function runSaveSpecific(pk, code, name) {
$.ajax({
url: api.v1.codes.runSaveSpecific(pk),
type:'PUT',
dataType: 'json',
data: {'code': code, 'name': name},
success: function (data) {
let output = data.output;
store.output.state = output;
store.output.changed = true;
getList();
alert('保存成功民逼!');
}
})
}
以上就是我們所有的 API 動(dòng)作了,我們的 UI 需要跟隨這些動(dòng)作而引起的狀態(tài)改變而做出對(duì)應(yīng)刷新動(dòng)作苔可,所以接下來讓我們來編寫每個(gè) UI 的響應(yīng)刷新動(dòng)作缴挖。
動(dòng)態(tài)大小改變:
function flexSize(selector) {
let ele = $(selector);
ele.css({
'height': 'auto',
'overflow-y': 'hidden'
}).height(ele.prop('scrollHeight'))
}
//將動(dòng)態(tài)變動(dòng)大小的動(dòng)作綁定到輸入框上
$('#code-input').on('input', function () {
flexSize(this)
});
把我們的列表渲染到 table
元素中:
function renderToTable(instance, tbody) {
let name = instance.name;
let pk = instance.pk;
let options = `\
<button class='btn btn-primary' onclick="getDetail(${pk})">查看</button>\
<button class="btn btn-primary" onclick="runSpecific(${pk})">運(yùn)行</button>\
<button class="btn btn-danger" onclick="remove(${pk})">刪除</button>`;
let child = `<tr><td class="text-center">${name}</td><td>${options}</td></tr>`;
tbody.append(child);
}
在這里要注意的是,我們使用模板字符串來作為渲染列表的方法焚辅,映屋。并且往其中也添加了對(duì)應(yīng)的參數(shù)。
接下來要編寫渲染代碼選項(xiàng)
function renderSpecificCodeOptions(pk) {
let options = `\
<button class="btn btn-primary" onclick="run($('#code-input').val())">運(yùn)行</button>\
<button class="btn btn-primary" onclick=\
"update(${pk},$('#code-input').val(),$('#code-name-input').val())">保存修改</button>\
<button class="btn" onclick=\
"runSaveSpecific(${pk}, $('#code-input').val(), $('#code-name-input').val())">保存并運(yùn)行</button>\
<button class="btn btn-primary" onclick="renderGeneralCodeOptions()">New</button>`;
$('#code-options').empty().append(options);// 先清除之前的選項(xiàng)同蜻,再添加當(dāng)前的選項(xiàng)
}
在渲染的時(shí)候要先把已有的內(nèi)容先清除棚点,不然之前的按鈕就會(huì)保留在頁面上。
我們有一個(gè)新建代碼的選項(xiàng)湾蔓,新建代碼的選項(xiàng)是不同的瘫析,所以我們需要單獨(dú)編寫:
function renderGeneralCodeOptions() {
let options = `\
<button class="btn btn-primary" onclick="run($('#code-input').val())">運(yùn)行</button>\
<button class="btn btn-primary" onclick=\
"create($('#code-input').val(),$('#code-name-input').val())">保存</button>\
<button class="btn btn-primary" onclick=\
"runSave($('#code-input').val(),$('#code-name-input').val())">保存并運(yùn)行</button>\
<button class="btn btn-primary" onclick="renderGeneralCodeOptions()">New</button>`;
$('#code-input').val('');// 清除輸入框
$('#code-output').val('');// 清除輸出框
$('#code-name-input').val('');// 清除代碼片段名輸入框
flexSize('#code-output');// 由于我們?cè)诟淖冚斎搿⑤敵隹虻膬?nèi)容時(shí)并沒有出發(fā) ‘input’ 事件默责,所以需要手動(dòng)運(yùn)行這個(gè)函數(shù)
$('#code-options').empty().append(options);// 清除的之前的選項(xiàng)贬循,再添加當(dāng)前的選項(xiàng)
}
同樣的,我們需要清除之前的數(shù)據(jù)才可以把我們的選項(xiàng)給渲染上去桃序。
終于杖虾,我們來到了最重要的部分。我們已經(jīng)編寫完了所有的動(dòng)作媒熊。要怎么把這些動(dòng)作給連接起來呢奇适?我們需要在狀態(tài)改變的時(shí)候就出發(fā)動(dòng)作,所以我們需要寫一個(gè) watcher
來監(jiān)聽我們的狀態(tài):
function watcher() {
for (let op in store) {
switch (op) {
case 'list':// 當(dāng) list 狀態(tài)改變時(shí)就刷新頁面中展示 list 的 UI芦鳍,在這里嚷往,我們的 UI 一個(gè) <table> 。
if (store[op].changed) {
let instances = store[op].state;
let tbody = $('tbody');
tbody.empty();
for (let i = 0; i < instances.length; i++) {
let instance = getInstance(instances[i]);
renderToTable(instance, tbody);
}
store[op].changed = false; // 記得將 changed 信號(hào)改回去哦柠衅。
}
break;
case 'detail':
if (store[op].changed) {// 當(dāng) detail 狀態(tài)改變時(shí)皮仁,就更新 代碼輸入框,代碼片段名輸入框菲宴,結(jié)果輸出框的狀態(tài)
let instance = store[op].state;
$('#code-input').val(instance.code);
$('#code-name-input').val(instance.name);
$('#code-output').val('');// 記得請(qǐng)空上次運(yùn)行代碼的結(jié)果哦魂贬。
flexSize('#code-input');// 同樣的,沒有出發(fā) 'input' 動(dòng)作裙顽,就要手動(dòng)改變值
renderSpecificCodeOptions(instance.pk);// 渲染代碼選項(xiàng)
store[op].changed = false;// 把 changed 信號(hào)改回去
}
break;
case 'output':
if (store[op].changed) { //當(dāng) output 狀態(tài)改變時(shí)付燥,就改變輸出框的的狀態(tài)。
let output = store[op].state;
$('#code-output').val(output);
flexSize('#code-output');// 記得手動(dòng)調(diào)用這個(gè)函數(shù)愈犹。
store[op].changed = false // changed 改回去
}
break;
}
}
}
我們的 watcher
會(huì)不斷的遍歷監(jiān)聽每個(gè)狀態(tài)键科,一旦狀態(tài)改變闻丑,就會(huì)執(zhí)行相應(yīng)的動(dòng)作。不過要注意的是勋颖,在動(dòng)作執(zhí)行完的時(shí)候要把 changed
信號(hào)給修改回去嗦嗡,不然你的 UI 會(huì)一直刷新。
最后我們做好收尾工作饭玲。
getList();// 初始化的時(shí)候我們應(yīng)該手動(dòng)的調(diào)用一次侥祭,好讓列表能在頁面上展示出來。
renderGeneralCodeOptions();// 手動(dòng)調(diào)用一次茄厘,好讓代碼選項(xiàng)渲染出來
setInterval("watcher()", 500);// 將 watcher 設(shè)置為 500 毫秒矮冬,也就是 0.5 秒就執(zhí)行一次,
// 這樣就實(shí)現(xiàn)了 UI 在不斷的監(jiān)聽狀態(tài)的變化次哈。
保存你的代碼胎署,將你的項(xiàng)目運(yùn)行起來,不出意外的話窑滞,效果就是這樣的:本章琼牧,我們學(xué)習(xí)并應(yīng)用了 REST 和 UI 的一些概念,希望大家能掌握這些概念哀卫,因?yàn)檫@對(duì)我們以后的開發(fā)來說是非常重要的巨坊。 這個(gè)小項(xiàng)目加上注釋,還是比較難的此改,希望大家能理解其中的每一個(gè)知識(shí)點(diǎn)趾撵。或許有的同學(xué)已經(jīng)發(fā)現(xiàn)我們根本沒有必要自己來手寫實(shí)現(xiàn)一些“通用”的東西带斑,比如 REST 規(guī)范下的一些 API 操作鼓寺,完全可以用現(xiàn)成的輪子來代替勋拟。而且勋磕,我們的應(yīng)用并沒有對(duì)上傳的數(shù)據(jù)進(jìn)行檢查,這樣我們的應(yīng)用豈不是處于被攻擊的風(fēng)險(xiǎn)下敢靡?并且我們并沒有對(duì) API 的請(qǐng)求頻率做出限制挂滓,要是有人寫個(gè)爬蟲無限制的訪問 API ,我們的應(yīng)用還可能會(huì)奔潰掉啸胧。我們還有太多的問題沒有考慮到赶站。
為了解決以上的問題,在下一章纺念,我們將會(huì)真正進(jìn)入 REST 開發(fā)贝椿,使用 Django REST framework 來改進(jìn)我們的應(yīng)用。