項目開發
在上一章我們了解了 REST 和 Mixin 以及 UI 狀態的概念、API 設計相關的一些知識,現在我們將會使用這些概念來真正編寫一個 REST 項目。在本章,我們將會涵蓋以下知識點:
- Mixin 的編寫,掌握 Mixin 的最基本編寫原則
- Store 與 state 的編寫。理解并能應用 UI 的狀態概念。
- 了解 API 的基本編寫規范和原則
本章的一些代碼會涉及到元編程的一點點知識,還有裝飾器的知識,這些都會在我們的教程中有所提及。但是由于我們的主要目標是開發應用,而不是進行編程教學,所以如果有碰到不懂的地方,大家可以先自行查找資料,如果還是不懂,可以留言提 issue ,我將會在教程中酌情補充講解。同樣的,本章的完整代碼在這里,別忘了 star 喲~
設計項目
在第一章,不管是在前端還是在后端開發,我們在寫代碼之前都有設計的過程,同樣的,在這里我們也需要設計好我們的項目才可以開始寫代碼。
需求分析
后端開發的主職責是提供 API 服務,同時,我們不能再把 javascript 寫在 html 里了,因為這次的 javascript 代碼會有點多,所以我們要提供靜態文件服務。一般來說,靜態文件服務都是由專門的靜態文件服務器來完成的,比如說 CDN ,也可以用 Nginx 。在這一章,我們的項目非常小,所以就使用 Django 來提供靜態文件服務。我們計劃自己編寫一個簡易的靜態文件服務。
項目結構
我們的項目結構如下:
online_intepreter_project/
frontend/ # 前端目錄
index.html
css/
...
js/
...
online_intepreter_project/ # 項目配置文件
settings.py
urls.py
...
online_intepreter_app/ # 我們真正的應用在這里
...
manage.py
大家可以看到,其實這一次,我們還是以后端為主,前端并沒有獨立出后端的項目結構,就像剛才所說,靜態文件,或者說是前端文件,應該盡量由專門的服務器來提供服務,后端專門負責數據處理就可以了。我們將會在之后的章節中使用這種模式,使用 Nginx 作為靜態文件服務器。不熟悉 Nginx ? 沒關系,我們會有專門的一章講解 Nginx ,以及有相應的練習項目。
做個深呼吸,開始動手了。
后端開發
在終端中新建一個項目:
python django-admin.py startproject online_intepreter_project
在這之前,我們使用的都是單文件的 Django ,這一次我們需要使用 Django 的 ORM ,所以需要按照標準的 Django 項目結構來構建我們的項目。然后切換到項目路徑內,建立我們的 app:
python manage.py startapp online_intepreter_app
同時,將不需要的文件刪除,并且再新建幾個空文件。按照如下來修改我們的項目結構:
online_intepreter_project/
frontend/ # 前端目錄
index.html
css/
bootstrap.css
main.css
js/
main.js
bootstrap.js
jquery.js
online_intepreter_project/ # 項目配置文件
__init__.py
settings.py # 項目配置
urls.py # URL 配置
online_intepreter_app/ # 我們真正的應用在這里
__init__.py
views.py # 視圖
models.py # 模型
middlewares.py # 中間件
mixins.py # mixin
manage.py
編輯項目的 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
: 安裝我們的應用。Django 會遍歷這個列表中的應用,并在使用 makemigrations
這個命令時才會自動的搜尋并創建我們應用的模型。
MIDDLEWARE
: 我們需要使用的中間件。由于 Django 不支持對 PUT 方法的數據處理,所以我們需要寫一個中間件來給它加上這個功能。之后我們會更加詳細的了解中間件的寫法。
DATABASES
: 配置我們的數據庫。在這里,我們只是簡單的使用了 sqlite3
數據庫。
以上便是所有的配置了。
現在我們先來編寫 PUT 中間件,來讓 Django 支持 PUT 請求。我們可以使用 POST 方法向 Django 應用上傳數據,并且可以使用 request.POST 來訪問 POST 數據。我們也想像使用 POST 一樣來使用 PUT ,利用 request.PUT 就可以訪問到 PUT 請求的數據。
中間件是 django 很重要的一部分,它在請求和響應之間充當預處理器的角色。
很多通用的邏輯可以放到這里,django 會自動的調用他們。
在這里,我們寫了一個簡單的中間件來處理 PUT 請求。只要是 PUT 請求,我們就對它作這樣的處理。所以,當你對某個請求都有相同的處理操作時,可以把它寫在中間件里。所以,中間件是什么呢?
中間件只是視圖函數的公共部分。你把中間件的核心處理邏輯復制粘貼到視圖函數中也是能夠正常運行的。
打開你的 middlewares.py
:
from django.http import QuerDict
def put_middleware(get_response):
def middleware(request):
if request.method == 'PUT': # 如果是 PUT 請求
setattr(request, 'PUT', QueryDict(request.body)) # 給請求設置 PUT 屬性,這樣我們就可以在視圖函數中訪問這個屬性了
# request.body 是請求的主體。我們知道請求有請求頭,那請求的主體就是
# request.body 了。當然,你一定還會問,為什么這樣就可以訪問 PUT 請求的相關
# 數據了呢?這涉及到了 http 協議的知識,這里就不展開了,有興趣的同學可以自行查閱資料
response = get_response(request) # 使用 get_response 返回響應
return response # 返回響應
return middleware # 返回核心的中間件處理函數
QueryDict
是 django 專門為請求的查詢字符串做的數據結構,它類似字典,但是又不是字典。
request
對象的 POST
GET
屬性都是這樣的字典。類似字典,是因為 QueryDict
和 python 的 dict
有相似的 API 接口,所以你可以把它當字典來調用。
不是字典,是因為 QueryDict
允許同一個鍵有多個直。比如 {'a':[‘1’,‘2’]},a 同時有值 1 和 2,所以,一般不要用 QueryDict[key]
的形式來訪問相應 key 的值,因為你得到的會是一個列表,而不是一個單一的值,應該用 QueryDict.get(key)
來獲取你想要的值,除非你知道你在干什么,你才能這樣來取值。為什么會允許多個值呢,因為 GET
請求中,常常有這種參數http://www.example.com/?action=search&achtion=filter
,
action
在這里有兩個值,有時候我們需要對這兩個值都作出響應。但是當你用 .get(key)
方法取值的時候,只會取到最新的一個值。如果確實需要訪問這個鍵的多個值,應該用 .getList(key)
方法來訪問,比如剛才的例子應該用 request.GET.getList('action')
來訪問 action
的多個值。
同理,對于 POST
請求也應該這么做。
接下來要說說 request.body
。做過爬蟲的同學一定都知道,請求有請求頭,那這個 body
就是我們的請求體了。嚴格的講,這個“請求體”應該叫做“載荷”,用英文來講,這就叫做“payload”。載荷里又有許多的學問了,感興趣的同學可以自己去了解相關的資料。只需要知道一件很簡單的事情,就是把 request.body
放進 QueryDict
就可以把上傳的字段轉換為我們需要的字典了。
由于原生的 request
對象并沒有 PUT 屬性,所以我們需要在中間件中加上這個屬性,這樣我們就可以在視圖函數中用 request.PUT
來訪問 PUT 請求中的參數值了。
中間件在 1.11 版本里是一個可調用對象,和之前的類中間件不同。既然是可調用對象,那就有兩種寫法,一種是函數,因為函數就是一個可調用對象;一種是自己用類來寫一個可調用對象,也就是包含 __call__()
方法的類。
在 1.11 版本中,中間件對象應該接收一個 get_response
的參數,這個參數用來獲取上一個中間件處理之后的響應,每個中間件處理完請求之后都應該用這個函數來返回一個響應,我們不需要關心這個 get _response
函數是怎么寫的,是什么東西,只需要記得在最后調用它,返回響應就好。這個最外層函數應該返回一個函數,用作真正的中間件處理。
在外層函數下寫你的預處理邏輯,比如配置什么的。當然,你也可以在被返回的函數中寫配置和預處理。但是這么做有時候就有些不直觀,配置、預處理和核心邏輯分開,讓看代碼的人一眼就明白這個中間件是在做什么。最通常的例子是,很多的 API 會對請求做許多的處理,比如記錄下這個請求的 IP 地址就可以先在這里做這個步驟;又比如,為了控制訪問頻率,可以先讀取數據庫中的訪問數據,根據訪問數據記錄來決定要不要讓這個請求進入到視圖函數中。我們對 PUT 請求并沒有什么預處理或者配置操作要進行,所以就什么都沒寫。
中間件的處理邏輯雖然簡單,但是中間件的寫法和作用大家還是需要掌握的。
接下來,讓我們創建我們的模型,編輯你的 models.py
:
from django.db import models
# 創建 Cdoe 模型
class CodeModel(models.Model):
name = models.CharField(max_length=50) # 名字最長為 50 個字符
code = models.TextField() # 這個字段沒有文本長度的限制
def __str__(self):
return 'Code(name={},id={})'.format(self.name,self.id)
在這里要注意一下,如果你是 py2 ,__str__
你需要改成 __unicode__
。我們的表結構很簡單,這里就不多說了。
我們的 API 返回的是 json
數據類型,所以我們需要把最基礎的響應方式更改為 JsonResponse
。同時,我們還有一個問題需要考慮,那就是如何把模型數據轉換為 json
類型。 我們知道 REST 中所說的 “表現(表層)狀態轉換” 就是這個意思,把不同類型的數據轉換為統一的類型,然后傳送給前端。如果前端要求是 json
那么我們就傳 json
過去,如果前端請求的是 xml
我們就傳 xml
過去。這就是“內容協商(協作)”。當然,我們的應用很簡單,就只有一種形式,但是如果是其它的大型應用,前端有時請求的是 json
格式的,有時請求的是 xml
格式的。我們的應用很簡單,就不用考慮內容協商了。
回到我們的問題,我們該如何把模型數據轉換為 json
數據呢? 把其它數據按照一定的格式保存下來,這個過程我們稱為“序列化”。“序列化”這個詞其實很形象,它把一系列的數據,按照一定的方式給整整齊齊的排列好,保存下來,以便他用。在 Django 中,Django 為我們提供了一些簡單的序列化工具,我們可以使用這些工具來把模型的內容轉換為 json
格式。
其中很重要的工具便是 serializers
了,看名字我們就這到它是用來干什么的。其核心函數 serialize(format, queryset[,fields])
就是用于把模型查詢集轉換為 json
字符串。它接收的三個參數分別為 format
,format
也就是序列化形式,如果我們需要 json
形式的,我們就把 format
賦值為 'json'
。 第二個參數為查詢集或者是一個含有模型實例的可迭代對象,也就是說,這個參數只能接收類似于列表的數據結構。fields
是一個可選參數,他的作用就和 Django 表單中的 fields
一樣,是用來控制哪些字段需要被序列化的。
編輯你的 views.py
:
from django.views import View # 引入最基本的類視圖
from django.http import JsonResponse # 引入現成的響應類
from django.core.serializers import serialize # 引入序列化函數
from .models import CodeModel # 引入 Code 模型,記得加個 `.` 哦。
import json # 引入 json 庫,我們會用它來處理 json 字符串。
# 定義最基本的 API 視圖
class APIView(View):
def response(self,
queryset=None,
fields=None,
**kwargs):
"""
序列化傳入的 queryset 或 其他 python 數據類型。返回一個 JsonResponse 。
:param queryset: 查詢集,可以為 None
:param fields: 查詢集中需要序列化的字段,可以為 None
:param kwargs: 其他需要序列化的關鍵字參數
:return: 返回 JsonResponse
"""
# 根據傳入參數序列化查詢集,得到序列化之后的 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
# 這一步很重要,在經過上面的查詢步驟之后, serialized_data 已經是一個字符串
# 我們最終需要把它放入 JsonResponse 中,JsonResponse 只接受 python 數據類型
# 所以我們需要先把得到的 json 字符串轉化為 python 數據結構。
instances = json.loads(serialized_data) if serialized_data else 'No instance'
data = {'instances': instances}
data.update(kwargs) # 添加其他的字段
return JsonResponse(data=data) # 返回響應
需要注意的是,我們先序列化了模型,然后又用 json
把它轉換為了 python 的字典結構,因為我們還需要把模型的數據和我們的其它數據(kwargs
)放在一起之后才會把它變成真正的 json
數據類型。
接下來,重頭戲到了,我們需要編寫我們的 Mixin 了。 在編寫 Mixin 之前,我們需要遵循以下幾個原則:
每個 Mixin 只完成一個功能。這就像是我們在“上”中舉的例子一樣,一個 Mixin 只會讓我們的“Man”類多一個功能出來。這是為了在使用的時候能夠更加清晰的明白這個 Mixin 是干什么的,同時能夠做到靈活的解耦功能,做到“即插即用”。
-
每個 Mixin 只操作自己知道的屬性和方法,還是那我們之前的 “Man” 類來做例子。我們知道我們寫的幾個 Mixin 最終都是用于
Man
類的,然而Man
類的屬性有name
、age
,所以在我們的 Mixin 中也可以像這樣來訪問這些屬性:self.name
,self.age
。因為這些屬性都是已知的。當然啦,Mixin 自己的屬性當然也是可以自己調用的啦。那在 Mixin 中我們需要用到其它的 Mixin 的屬性的時候該怎么辦呢?很簡單,直接繼承這個 Mixin 就好了。 我們的 Mixin 最終是要作用到視圖上的,所以我們可以把我們的基礎視圖的屬性當作是已知屬性。 我們的APIView
是View
類的子類,所以View
的所有屬性和方法我們的Mixin
都可以調用。我們通常用到的屬性有:1. `kwargs`: 這是傳入視圖函數的關鍵字參數,我們可以在類視圖中使用 `self.kwargs` 來訪問這些傳入的關鍵字參數。 2. `args`: 傳入視圖的位置參數。 3. `request`: 視圖函數的第一個參數,也就是當前的請求對象,它和我們平時寫的視圖函數中的 request 是一模一樣的。
編寫 Mixin 是為了代碼的復用和代碼的解耦,所以在正式開始編寫之前,我們必須要想好,哪一些 Mixin 是我們需要編寫的,哪一些邏輯是必須要寫到視圖函數中。
首先,凡是對于有查詢動作的請求,我們都有一個從數據庫中提取查詢集的過程,所以我們需要編
寫一個提取查詢集的 Mixin 。
第二,對于查詢集來說,有時候我們需要的是整個查詢集,有時候只是需要一個單一的查詢實例,比如在更新和刪除的時候,我們都是在對一個實例進行操作。所以我們還需要編寫一個能夠提取出單一實例的 Mixin 。
第三,對于 API 的通用操作來說,根據 REST 原則,每個請求都有自己的對應動作,比如 put 對應的是修改動作,post 對應的是創建動作,delete 對應的是刪除動作,所以我們需要為這些通用的 API 動作一一編寫 Mixin 。
第四,正如第三條考慮到的那樣, API 的不同請求是有自己對應的默認動作的。如果我們的視圖就是想簡單的使用他們的默認動作,也就是 post 是創建動作,put 是修改動作,我們希望視圖函數能自己將這些請求自己就映射到這些默認動作上,這樣在之后的開發我們就可以什么都不用做了,連最基本的 get post 視圖方法都不需要我們編寫。所以我們需要編寫一個方法映射 Mixin 。
最后,就我們的應用而言,我們應用是為了提供在線解釋器服務,所以會有一個執行代碼的功能,雖然到目前,這個功能的核心函數執行的代碼很簡單,但是誰能保證他一直都是這樣簡單呢?所以為了保持良好的視圖解耦性,我們也需要把這部分的代碼單獨獨立出來成為一個 Mixin 。
現在,讓我們開始編寫我們的 Mixin 。我們編寫 Mixin 的活動都會在 mixins.py
中進行。
首先,在頂部引入需要用到的包
from django.db import models, IntegrityError # 查詢失敗時我們需要用到的模塊
import subprocess # 用于運行代碼
from django.http import Http404 # 當查詢操作失敗時返回404響應
IntegrityError
錯誤會在像數據庫寫入數據創建不成功時被拋出,這是我們需要捕捉并做出響應的錯誤。
獲取查詢集 Mixin 的編寫:
class APIQuerysetMinx(object):
"""
用于獲取查詢集。在使用時,model 屬性和 queryset 屬性必有其一。
:model: 模型類
:queryet: 查詢集
"""
model = None
queryset = None
def get_queryset(self):
"""
獲取查詢集。若有 model 參數,則默認返回所有的模型查詢實例。
:return: 查詢集
"""
# 檢驗相應參數是否被傳入,若沒有傳入則拋出錯誤
assert self.model or self.queryset, 'No queryset fuound.'
if self.queryset:
return self.queryset
else:
return self.model.objects.all()
可以看到,我們的 Mixin 的設計很簡單,只是為子類提供了兩個參數 queryset
和model
,并且 get_queryset
這個方法會使用這兩個屬性返回相應的所有的實例查詢集。我們可以這樣使用它:
class GETView(APIQuerysetMinx, View):
model = MyModel
def get(self, *args, **kwargs):
return self.get_queryset()
這樣我們的視圖是不是看起來就方便,清晰了很多,視圖邏輯和具體的操作邏輯相分離,這樣方便別人閱讀自己的代碼,一看就知道是什么意思。在之后的 Mixin 使用也是同理的。
編寫獲取單一實例的 Mixin :
class APISingleObjectMixin(APIQuerysetMinx):
"""
用于獲取當前請求中的實例。
:lookup_args: list, 用來規定查詢參數的參數列表。默認為 ['pk','id]
"""
lookup_args = ['pk', 'id']
def get_object(self):
"""
通過查詢 lookup_args 中的參數值來返回當前請求實例。當獲取到參數值時,則停止
對之后的參數查詢。參數順序很重要。
:return: 一個單一的查詢實例
"""
queryset = self.get_queryset() # 獲取查詢集
for key in self.lookup_args:
if self.kwargs.get(key):
id = self.kwargs[key] # 獲取查詢參數值
try:
instance = queryset.get(id=id) # 獲取當前實例
return instance # 實例存在則返回實例
except models.ObjectDoesNotExist: # 捕捉實例不存在異常
raise Http404('No object found.') # 拋出404異常響應
raise Http404('No object found.') # 若遍歷所以參數都未捕捉到值,則拋出404異常響應
我們可以看到,獲取單一實例的方式是從傳入視圖函數的關鍵字參數kwargs
中獲取對應的 id
或者 pk
然后從查詢集中獲取相應的實例。并且我們還可以靈活的配置查詢的關鍵詞是什么,這個 Mixin 還很方便使用的。
接下來我們需要編寫的是獲取列表的 Mixin
class APIListMixin(APIQuerysetMinx):
"""
API 中的 list 操作。
"""
def list(self, fields=None):
"""
返回查詢集響應
:param fields: 查詢集中希望被實例化的字段
:return: JsonResopnse
"""
return self.response(
queryset=self.get_queryset(),
fields=fields) # 返回響應
我們可以看到,我們只是簡單的返回了查詢集,并且默認的方法還支持傳入需要的序列化的字段。
執行創建操作的 Mixin:
class APICreateMixin(APIQuerysetMinx):
"""
API 中的 create 操作
"""
def create(self, create_fields=None):
"""
使用傳入的參數列表從 POST 值中獲取對應參數值,并用這個值創建實例,
成功創建則返回創建成功響應,否則返回創建失敗響應。
:param create_fields: list, 希望被創建的字段。
若為 None, 則默認為 POST 上傳的所有字段。
:return: JsonResponse
"""
create_values = {}
if create_fields: # 如果傳入了希望被創建的字段,則從 POST 中獲取每個值
for field in create_fields:
create_values[field]=self.request.POST.get(field)
else:
for key in self.request.POST: # 若未傳入希望被創建字段,則默認為 POST 上傳的
# 字段都為創建字段。
create_values[key]=self.request.POST.get(key);
queryset = self.get_queryset() # 獲取查詢集
try:
instance = queryset.create(**create_values) # 利用查詢集來創建實例
except IntegrityError: # 捕捉創建失敗異常
return self.response(status='Failed to Create.') # 返回創建失敗響應
return self.response(status='Successfully Create.') # 創建成功則返回創建成功響應
我們可以看到,作為 API 的 Mixin ,創建的默認動作已經是從 POST 中獲取相應的數據,這就不用我們把提取數據的邏輯硬編碼在視圖中了,而且考慮到了足夠多的情況。并且我們還手動的傳入了 status
,方便前端開發能夠清楚的知道操作是否成功。
實例查詢 Mixin:
class APIDetailMixin(APISingleObjectMixin):
"""
API 操作中查詢實例操作
"""
def detail(self, fields=None):
"""
返回當前請求中的實例
:param fields: 希望被返回實例中哪些字段被實例化
:return: JsonResponse
"""
return self.response(
queryset=[self.get_object()],
fields=fields)
同理,我們只是簡單的調用了 get_object
方法,并沒有做其它的處理。
更新 Mixin:
class APIUpdateMixin(APISingleObjectMixin):
"""
API 中更新實例操作
"""
def update(self, update_fields=None):
"""
更新當前請求中實例。更新成功則返回成功響應。否則,返回更新失敗響應。
若傳入 updata_fields 更新字段列表,則只會從 PUT 上傳值中獲取這個列表中的字段,
否則默認為更新 POST 上傳值中所有的字段。
:param update_fields: list, 實例需要被更新的字段
:return: JsonResponse
"""
instance = self.get_object() # 獲取當前請求中的實例
if not update_fields: # 若無字段更新列表,則默認為 PUT 上傳值的所有數據
update_fields=self.request.PUT.keys()
try: # 迭代更新實例字段
for field in update_fields:
update_value = self.request.PUT.get(field) # 從 PUT 中取值
setattr(instance, field, update_value) # 更新字段
instance.save() # 保存實例更新
except IntegrityError: # 捕捉更新錯誤
return self.response(status='Failed to Update.') # 返回更新失敗響應
return self.response(
status='Successfully Update')# 更新成功則返回更新成功響應
setattr
的作用就是給一個對象設置屬性,當查詢的實例被找到之后,我們采用這種方法來給實例更新值。因為我們在這種情況下不能使用 .
路徑符來訪問字段,因為我們不知道有哪些字段會被更新。同時,作為 API 的 Mixin ,更新時獲取數據的地方已經默認為從 PUT 請求中獲取數據。
刪除操作 Mixin
class APIDeleteMixin(APISingleObjectMixin):
"""
API 刪除實例操作
"""
def remove(self):
"""
刪除當前請求中的實例。刪除成功則返回刪除成功響應。
:return: JsonResponse
"""
instance = self.get_object() # 獲取當前實例
instance.delete() # 刪除實例
return self.response(status='Successfully Delete') # 返回刪除成功響應
需要注意的是,我們的方法名不叫 delete
,而是 remove
,這是因為 delete
是請求方法名,我們不能占用它。
運行代碼的 Mixin:
class APIRunCodeMixin(object):
"""
運行代碼操作
"""
def run_code(self, code):
"""
運行所給的代碼,并返回執行結果
:param code: str, 需要被運行的代碼
:return: str, 運行結果
"""
try:
output = subprocess.check_output(['python', '-c', code], # 運行代碼
stderr=subprocess.STDOUT, # 重定向錯誤輸出流到子進程
universal_newlines=True, # 將返回執行結果轉換為字符串
timeout=30) # 設定執行超時時間
except subprocess.CalledProcessError as e: # 捕捉執行失敗異常
output = e.output # 獲取子進程報錯信息
except subprocess.TimeoutExpired as e: # 捕捉超時異常
output = '\r\n'.join(['Time Out!', e.output]) # 獲取子進程報錯,并添加運行超時提示
return output # 返回執行結果
這個也不多說,就只是簡單的把之前的函數式更改為了類。不過要注意的是,如果你用的是 py2,subprocess
有的屬性的引用方式會和 3 有寫不同,大家可以自行去查閱如何正確引入相關的屬性。
前幾個 Mixin 都沒有很詳細的說,下面這個 Mixin 我們需要詳細的說明。
class APIMethodMapMixin(object):
"""
將請求方法映射到子類屬性上
:method_map: dict, 方法映射字典。
如將 get 方法映射到 list 方法,其值則為 {'get':'list'}
"""
method_map = {}
def __init__(self,*args,**kwargs):
"""
映射請求方法。會從傳入子類的關鍵字參數中尋找 method_map 參數,期望值為 dict類型。尋找對應參數值。
若在類屬性和傳入參數中同時定義了 method_map ,則以傳入參數為準。
:param args: 傳入的位置參數
:param kwargs: 傳入的關鍵字參數
"""
method_map=kwargs['method_map'] if kwargs.get('method_map',None) \
else self.method_map # 獲取 method_map 參數
for request_method, mapped_method in method_map.items(): # 迭代映射方法
mapped_method = getattr(self, mapped_method) # 獲取被映射方法
method_proxy = self.view_proxy(mapped_method) # 設置對應視圖代理
setattr(self, request_method, method_proxy) # 將視圖代碼映射到視圖代理方法上
super(APIMethodMapMixin,self).__init__(*args,**kwargs) # 執行子類的其他初始化
def view_proxy(self, mapped_method):
"""
代理被映射方法,并代理接收傳入視圖函數的其他參數。
:param mapped_method: 被代理的映射方法
:return: function, 代理視圖函數。
"""
def view(*args, **kwargs):
"""
視圖的代理方法
:param args: 傳入視圖函數的位置參數
:param kwargs: 傳入視圖函數的關鍵字參數
:return: 返回執行被映射方法
"""
return mapped_method() # 返回執行代理方法
return view # 返回代理視圖
首先,大家不要被嚇到。我們慢慢來分析。
我們先給子類提供了一個 method_map
的屬性,這是一個字典,子類可以通過給這個字典配置相應的值來使用我們的 APIMethodMapMixin
,字典的鍵為請求的方法名,值為要執行的操作。。接下來看看 __init__
方法,首先,會在子類視圖實例化的時候尋找 method_map
參數,如果找到了就會以這個參數作為方法映射的字典,在子類中編寫的配置就不會生效了。也就是說:
# views.py
class ExampleView(APIMethodMapMixin, APIView):
method_map = {'get':'list','put':'update'}
# urls.py
urlpatterns = [url(r'^$',ExampleView.as_view(method_map={'get':'list'}))]
如果在初始化視圖類的時候也傳入了 method_map
參數,那我們定義在 ExampleView
中的屬性就沒用了,視圖會以初始化時的參數作為最終標準。由于我們的字典只是一個字符串,我們要做的是把子類的對應操作方法和請求方法對應起來,所以我們首先使用 getattr
來獲取子類的響應操作的方法,然后利用了 view_proxy
代理了視圖方法。為什么我們需要這個代理方法?原因很簡單,因為在默認的視圖中,View
會向視圖傳遞參數,然而,我們的操作方法,他們的參數和被傳入視圖的參數是截然不同的,所以我們需要使用一個函數來代理接收這些參數,這個函數就是我們視圖代理函數返回的 view
函數,這個函數會接收所有傳向視圖的參數,然后不對這些參數做出處理,只是簡單的調用被映射的方法。
python 基礎很不錯的同學應該已經發現了,我們的 view_proxy
的寫法不就是一個裝飾器的寫法嗎?是的,裝飾器也是這樣寫的,只是我們在 __init__
中手動調用了它而已,平時我們用 @
來使用裝飾器和我們手動調用的過程是完全相同的。在最后,我們把操作方法設置為了請求對應方法的值,這樣我們就可以成功的調用相應的操作了。別忘了在最后調用 super
哦。
以上便是我們所有的 Mixin 的編寫。現在,我們來完成編寫 views.py
。
首先,在頂上引入這些包:
from django.views import View # 引入最基本的類視圖
from django.http import JsonResponse, HttpResponse # 引入現成的響應類
from django.core.serializers import serialize # 引入序列化函數
from .models import CodeModel # 引入 Code 模型,記得加個 `.` 哦。
import json # 引入 json 庫,我們會用它來處理 json 字符串。
from .mixins import APIDetailMixin, APIUpdateMixin, \
APIDeleteMixin, APIListMixin, APIRunCodeMixin, \
APICreateMixin, APIMethodMapMixin, APISingleObjectMixin # 引入我們編寫的所有 Mixin
我們的核心 API:
class APICodeView(APIListMixin, # 獲取列表
APIDetailMixin, # 獲取當前請求實例詳細信息
APIUpdateMixin, # 更新當前請求實例
APIDeleteMixin, # 刪除當前實例
APICreateMixin, # 創建新的的實例
APIMethodMapMixin, # 請求方法與資源操作方法映射
APIView): # 記得在最后繼承 APIView
model = CodeModel # 傳入模型
def list(self): # 這里僅僅是簡單的給父類的 list 函數傳參。
return super(APICodeView, self).list(fields=['name'])
有了 Mixin 是不是很方便,這種感覺不要太爽。
接下來完成運行代碼的 API :
class APIRunCodeView(APIRunCodeMixin,
APISingleObjectMixin,
APIView):
model = CodeModel # 傳入模型
def get(self, request, *args, **kwargs):
"""
GET 請求僅對能獲取到 pk 值的 url 響應
:param request: 請求對象
:param args: 位置參數
:param kwargs: 關鍵字參數
:return: JsonResponse
"""
instance = self.get_object() # 獲取對象
code = instance.code # 獲取代碼
output = self.run_code(code) # 運行代碼
return self.response(output=output, status='Successfully Run') # 返回響應
def post(self, request, *args, **kwargs):
"""
POST 請求可以被任意訪問,并會檢查 url 參數中的 save 值,如果 save 為 true 則會
保存上傳代碼。
:param request: 請求對象
:param args: 位置參數
:param kwargs: 關鍵字參數
:return: JsonResponse
"""
code = self.request.POST.get('code') # 獲取代碼
save = self.request.GET.get('save') == 'true' # 獲取 save 參數值
name = self.request.POST.get('name') # 獲取代碼片段名稱
output = self.run_code(code) # 運行代碼
if save: # 判斷是否保存代碼
instance = self.model.objects.create(name=name, code=code)
return self.response(status='Successfully Run and Save',
output=output) # 返回響應
def put(self, request, *args, **kwrags):
"""
PUT 請求僅對更改操作作出響應
:param request: 請求對象
:param args: 位置參數
:param kwrags: 關鍵字參數
:return: JsonResponse
"""
code = self.request.PUT.get('code') # 獲取代碼
name = self.request.PUT.get('name') # 獲取代碼片段名稱
save = self.request.GET.get('save') == 'true' # 獲取 save 參數值
output = self.run_code(code) # 運行代碼
if save: # 判斷是否需要更改代碼
instance = self.get_object() # 獲取當前實例
setattr(instance, 'name', name) # 更改名字
setattr(instance, 'code', code) # 更改代碼
instance.save()
return self.response(status='Successfully Run and Change',
output=output) # 返回響應
值得注意的是,我們使用了一個 save
參數來判斷上傳的代碼是否需要保存,因為上傳方式都是 POST 我們在這種情況下就需要增加新的參數來決定是否需要保存。而且由于我們沒有怎么使用 Mixin ,所有的字段我們都是手動提取的,所有的操作過程都是我們自己寫的,就顯得有點笨搓搓的。由此可見 Mixin 是多么的好用。
別忘了,我們還要提供靜態文件服務:
# 主頁視圖
def home(request):
"""
讀取 'index.html' 并返回響應
:param request: 請求對象
:return: HttpResponse
"""
with open('frontend/index.html', 'rb') as f:
content = f.read()
return HttpResponse(content)
# 讀取 js 視圖
def js(request, filename):
"""
讀取 js 文件并返回 js 文件響應
:param request: 請求對象
: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 響應
# 讀取 css 視圖
def css(request, filename):
"""
讀取 css 文件,并返回 css 文件響應
:param request: 請求對象
: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 響應
在靜態文件的響應中需要把響應頭更改為正確的響應頭,不然瀏覽器就不認識你傳回去的是什么靜態件了。
最后,按照我們之前的設計,完成我們的 API URL 配置。
編輯你的 urls.py
,這個文件是和你的 settings.py
在同一個目錄哦
# 這是我們的 URL 入口配置,我們直接將入口配置到具體的 URL 上。
from django.conf.urls import url, include # 引入需要用到的配置函數
# include 用來引入其他的 URL 配置。參數可以是個路徑字符串,也可以是個 url 對象列表
from online_intepreter_app.views import APICodeView, APIRunCodeView, home, js, css # 引入我們的視圖函數
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 參數
# 針對某個對象的操作 API
detail_code_view = csrf_exempt(APICodeView.as_view(method_map={'get': 'detail',
'put': 'update',
'delete': 'remove'}))
# 運行代碼操作 API
run_code_view = csrf_exempt(APIRunCodeView.as_view())
# Code 應用 API 配置
code_api = [
url(r'^$', generic_code_view, name='generic_code'), # 集合操作
url(r'^(?P<pk>\d*)/$', detail_code_view, name='detail_code'), # 訪問某個特定對象
url(r'^run/$', run_code_view, name='run_code'), # 運行代碼
url(r'^run/(?P<pk>\d*)/$', run_code_view, name='run_specific_code') # 運行特定代碼
]
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 文件,記得,最后沒有 /
]
記得,在靜態文件服務的 url 后面沒有 /
,因為在前端引用的時候是不會加 /
的,這是對一個文件的直接訪問。
最后,回到項目路徑下,運行:
python manage.py makemigrations
python manage.py migrate
創建好數據庫之后我們就可以進入前端的開發了。
我們的后端就算完成了。休息一下,準備向前端進發。
前端開發
我們把工作路徑切換到 frontend
下。這一次我們的重點放在 js 的編寫上。這一次的編寫沒有什么難點,重點是在于對前端原理的理解和應用上,代碼不難,但是希望大家著重的理解其中的設計模式。最好的方式就是自己敲一遍代碼。只有跟著敲一次才知道自己哪里有問題。
首先把我們需要的 js 和 css 都放在對應的文件下。大家可以去我的 github 把 bootstrap.js 和 bootstrap.css 以及 jquery.js 下載下來,把 js 文件放在 js
路徑下,css 放在 css
路徑下。準備工作做完了。
首先編寫我們的主頁 html ,這次的 html 做了一些改動,并且添加了新的元素。所以大家不要直接使用上一次的 index.html
,應該自己敲一次,才能注意到一些小細節。有了第一章的經驗,我就不多說了。
編輯你的 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 類屬性,讓主頁填滿整個瀏覽器-->
<div class="row text-center h1">
在線 Python 解釋器
</div>
<hr>
<div class="row">
<div class="col-md-3">
<table class="table table-striped"><!--文件列表-->
<thead> <!--標題-->
<tr>
<th class="text-center">文件名</th> <!--標題居中-->
<th class="text-center">選項</th> <!--標題居中-->
</tr>
</thead>
<tbody></tbody> <!-- 列表實體,由 js 渲染列表實體-->
</table>
</div>
<div class="col-md-9">
<div class="container-fluid">
<div class="col-md-6">
<p class="text-center h3">請在下方輸入代碼:</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" > <!--代碼選項,采用 flex 布局,使每個選項都均勻分布-->
</div>
</div>
<p class="text-center h3">輸出</p>
<div class="col-md-6">
<textarea id="code-output" disabled
class="form-control text-center"></textarea><!--結果輸出-->
</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;
} /*設置輸入輸出框的字體大小,禁用他們的 resize 功能*/
接下來到了前端開發中的重點了,接下來的開發都會在 main.js
中進行。
在第一章的開發中,我們的 API 很簡單,就一個 POST ,但是這一次,我們的 API 多,而且比較復雜,甚至還有 GET 參數,所以我們需要管理我們的 API ,所以硬編碼 API 一定是行不通了,硬編碼 API 不僅會導致靈活性不夠,還會增加手動輸入錯誤的幾率。所以我們這樣來管理我們的 API:
const api = {
v1: { // api 版本號
codes: { // api v1 版本下的 code api。
list: function () { //獲取實例查詢集
return '/api/v1/codes/'
},
detail: function (pk) { // 獲取單一實例
return `/api/v1/codes/${pk}/`
},
create: function () { // 創建實例
return `/api/v1/codes/`
},
update: function (pk) { // 更新實例
return `/api/v1/codes/${pk}/`
},
remove: function (pk) { //刪除實例
return `/api/v1/codes/${pk}/`
},
run: function () { //運行代碼
return '/api/v1/codes/run/'
},
runSave: function () {// 保存并運行代碼
return '/api/v1/codes/run/?save=true'
},
runSpecific: function (pk) { // 運行特定代碼實例
return `/api/v1/codes/run/${pk}/`
},
runSaveSpecific: function (pk) { // 運行并保存特定代碼實例
return `/api/v1/codes/run/${pk}/?save=true`
}
}
}
};
不要被嚇到,或許有的同學會覺得使用函數來返回 API 是多此一舉的。但是我們想想,如果你的代碼特別多,特別長,你會不會寫著寫著就忘了自己調用的 API 是干什么的?所以為了保證良好的語義性,我們需要有良好的層級結構和良好的命名規則。使用函數不僅可以正確的生成含有參數的 URL 而且也方便我們將來做進一步的改進。如果哪一天 API 發生變化了,我們直接在函數中做出對應的修改就好了,不需要像硬編碼那樣挨著挨著更改。
接下來我們的核心概念來了 —— state
。在“上”我們已經知道了狀態的概念和store
,就是用來儲存狀態的東西。所以我們像這樣來定義我們的狀態。
let store = {
list: { //列表狀態
state: undefined,
changed: false
},
detail: { //特定實例狀態
state: undefined,
changed: false
},
output: { //輸出狀態
state: undefined,
changed: false
}
};
我們把不同的狀態放在 store 每個狀態有 state 和 changed 屬性,state 用來儲存 UI 相關聯的變量信息,changed 作為狀態是否改變的信號。UI 只需要監聽 chagned 變量,當 changed 為 true 時才讀取并改變狀態。要是你忘了什么是“狀態”,趕緊回去看看上一個部分吧。
我們已經定義完了 API 和 狀態,但是真正向后端發起請求動作的函數還都沒有完成。接著在下面寫我們的動作函數。
這些動作負責調用 API ,并接受 API 返回的數據,然后將這些數據保存進 store 中。注意,在修改完狀態之后,記得將狀態的 changed 屬性改為 true ,不然狀態不會刷新到監聽的 UI 上。
得到單一的實例,因為我們 Django 模型序列化的之后的格式不是很符合我們的要求,所以我們需要做一些處理。模型字段序列化之后是這樣的。
{'model':'app.modelName','pk':'pk',fields:[modelFields]}
比如我們的 Code 模型,一個實例序列化之后值這樣的:
{'model':'online_intepreter_app.Code',pk:'1', fields[{'name':'name','code':'code'}]}
如果是查詢集,則返回的就是想上面一樣的對象列表。
我們需要把實例的 pk 和字段給放到一起。
//從后端返回的數據中,把實例相關的數據處理成我們期望的形式,好方便我們調用
function getInstance(data) {
let instance = data.fields;
instance.pk = data.pk;
return instance
}
獲取 code 列表:
//獲取 code 列表,更改 list 狀態
function getList() {
$.getJSON({
url: api.v1.codes.list(),
success: function (data) {
store.list.state = data.instances;
store.list.changed = true;
}
})
}
大家已經注意到了,請求完成之后,改變的狀態值,并且也發出了響應的狀態更改信號,就是把changed
更改為true
。
創建實例動作
function create(code, name) {
$.post({
url: api.v1.codes.create(),
data: {'code': code, 'name': name},
dataType: 'json',
success: function (data) {
getList();
alert('保存成功!');
}
})
}
在創建完成后,我們又更新了 list
狀態,這樣就可以實時刷新我們的 list
了。
更新實例動作
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('更新成功!');
}
})
}
同理,我們在更新完成后也刷新了 list
。
獲取實例動作
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;
}
})
}
我們在獲取實例的時候使用了 getInstance
,保證獲取的實例是符合我們要求的。
刪除實例
function remove(pk) {
$.ajax({
url: api.v1.codes.remove(pk),
type: 'DELETE',
dataType: 'json',
success: function (data) {
getList();
alert('刪除成功!');
}
})
}
我們刪除實例的動作還是叫做 remove
,不叫 delete
是因為 delete
是默認關鍵字。
運行代碼的幾個動作也是和上面同理:
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;
}
})
}
//運行保存代碼,并刷新 output 和 list 狀態。
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('保存成功!');
}
})
}
//運行特定的代碼實例,并刷新 output 狀態
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;
}
})
}
//運行并保存特定代碼實例,并刷新 output 和 list 狀態
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 動作了,我們的 UI 需要跟隨這些動作而引起的狀態改變而做出對應刷新動作,所以接下來讓我們來編寫每個 UI 的響應刷新動作。
動態大小改變:
function flexSize(selector) {
let ele = $(selector);
ele.css({
'height': 'auto',
'overflow-y': 'hidden'
}).height(ele.prop('scrollHeight'))
}
//將動態變動大小的動作綁定到輸入框上
$('#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})">運行</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);
}
在這里要注意的是,我們使用模板字符串來作為渲染列表的方法,。并且往其中也添加了對應的參數。
接下來要編寫渲染代碼選項
function renderSpecificCodeOptions(pk) {
let options = `\
<button class="btn btn-primary" onclick="run($('#code-input').val())">運行</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())">保存并運行</button>\
<button class="btn btn-primary" onclick="renderGeneralCodeOptions()">New</button>`;
$('#code-options').empty().append(options);// 先清除之前的選項,再添加當前的選項
}
在渲染的時候要先把已有的內容先清除,不然之前的按鈕就會保留在頁面上。
我們有一個新建代碼的選項,新建代碼的選項是不同的,所以我們需要單獨編寫:
function renderGeneralCodeOptions() {
let options = `\
<button class="btn btn-primary" onclick="run($('#code-input').val())">運行</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())">保存并運行</button>\
<button class="btn btn-primary" onclick="renderGeneralCodeOptions()">New</button>`;
$('#code-input').val('');// 清除輸入框
$('#code-output').val('');// 清除輸出框
$('#code-name-input').val('');// 清除代碼片段名輸入框
flexSize('#code-output');// 由于我們在改變輸入、輸出框的內容時并沒有出發 ‘input’ 事件,所以需要手動運行這個函數
$('#code-options').empty().append(options);// 清除的之前的選項,再添加當前的選項
}
同樣的,我們需要清除之前的數據才可以把我們的選項給渲染上去。
終于,我們來到了最重要的部分。我們已經編寫完了所有的動作。要怎么把這些動作給連接起來呢?我們需要在狀態改變的時候就出發動作,所以我們需要寫一個 watcher
來監聽我們的狀態:
function watcher() {
for (let op in store) {
switch (op) {
case 'list':// 當 list 狀態改變時就刷新頁面中展示 list 的 UI,在這里,我們的 UI 一個 <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 信號改回去哦。
}
break;
case 'detail':
if (store[op].changed) {// 當 detail 狀態改變時,就更新 代碼輸入框,代碼片段名輸入框,結果輸出框的狀態
let instance = store[op].state;
$('#code-input').val(instance.code);
$('#code-name-input').val(instance.name);
$('#code-output').val('');// 記得請空上次運行代碼的結果哦。
flexSize('#code-input');// 同樣的,沒有出發 'input' 動作,就要手動改變值
renderSpecificCodeOptions(instance.pk);// 渲染代碼選項
store[op].changed = false;// 把 changed 信號改回去
}
break;
case 'output':
if (store[op].changed) { //當 output 狀態改變時,就改變輸出框的的狀態。
let output = store[op].state;
$('#code-output').val(output);
flexSize('#code-output');// 記得手動調用這個函數。
store[op].changed = false // changed 改回去
}
break;
}
}
}
我們的 watcher
會不斷的遍歷監聽每個狀態,一旦狀態改變,就會執行相應的動作。不過要注意的是,在動作執行完的時候要把 changed
信號給修改回去,不然你的 UI 會一直刷新。
最后我們做好收尾工作。
getList();// 初始化的時候我們應該手動的調用一次,好讓列表能在頁面上展示出來。
renderGeneralCodeOptions();// 手動調用一次,好讓代碼選項渲染出來
setInterval("watcher()", 500);// 將 watcher 設置為 500 毫秒,也就是 0.5 秒就執行一次,
// 這樣就實現了 UI 在不斷的監聽狀態的變化。
保存你的代碼,將你的項目運行起來,不出意外的話,效果就是這樣的:本章,我們學習并應用了 REST 和 UI 的一些概念,希望大家能掌握這些概念,因為這對我們以后的開發來說是非常重要的。 這個小項目加上注釋,還是比較難的,希望大家能理解其中的每一個知識點。或許有的同學已經發現我們根本沒有必要自己來手寫實現一些“通用”的東西,比如 REST 規范下的一些 API 操作,完全可以用現成的輪子來代替。而且,我們的應用并沒有對上傳的數據進行檢查,這樣我們的應用豈不是處于被攻擊的風險下?并且我們并沒有對 API 的請求頻率做出限制,要是有人寫個爬蟲無限制的訪問 API ,我們的應用還可能會奔潰掉。我們還有太多的問題沒有考慮到。
為了解決以上的問題,在下一章,我們將會真正進入 REST 開發,使用 Django REST framework 來改進我們的應用。