Django RESTful 系列教程(二)(下)

項(xiàng)目開發(fā)

在上一章我們了解了 REST 和 Mixin 以及 UI 狀態(tài)的概念、API 設(shè)計(jì)相關(guān)的一些知識,現(xiàn)在我們將會使用這些概念來真正編寫一個 REST 項(xiàng)目。在本章,我們將會涵蓋以下知識點(diǎn):

  1. Mixin 的編寫,掌握 Mixin 的最基本編寫原則
  2. Store 與 state 的編寫。理解并能應(yīng)用 UI 的狀態(tài)概念。
  3. 了解 API 的基本編寫規(guī)范和原則

本章的一些代碼會涉及到元編程的一點(diǎn)點(diǎn)知識,還有裝飾器的知識,這些都會在我們的教程中有所提及。但是由于我們的主要目標(biāo)是開發(fā)應(yīng)用,而不是進(jìn)行編程教學(xué),所以如果有碰到不懂的地方,大家可以先自行查找資料,如果還是不懂,可以留言提 issue ,我將會在教程中酌情補(bǔ)充講解。同樣的,本章的完整代碼在這里,別忘了 star 喲~

設(shè)計(jì)項(xiàng)目

在第一章,不管是在前端還是在后端開發(fā),我們在寫代碼之前都有設(shè)計(jì)的過程,同樣的,在這里我們也需要設(shè)計(jì)好我們的項(xiàng)目才可以開始寫代碼。

需求分析

后端開發(fā)的主職責(zé)是提供 API 服務(wù),同時(shí),我們不能再把 javascript 寫在 html 里了,因?yàn)檫@次的 javascript 代碼會有點(diǎn)多,所以我們要提供靜態(tài)文件服務(wù)。一般來說,靜態(tài)文件服務(wù)都是由專門的靜態(tài)文件服務(wù)器來完成的,比如說 CDN ,也可以用 Nginx 。在這一章,我們的項(xiàng)目非常小,所以就使用 Django 來提供靜態(tài)文件服務(wù)。我們計(jì)劃自己編寫一個簡易的靜態(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ù)處理就可以了。我們將會在之后的章節(jié)中使用這種模式,使用 Nginx 作為靜態(tài)文件服務(wù)器。不熟悉 Nginx ? 沒關(guān)系,我們會有專門的一章講解 Nginx ,以及有相應(yīng)的練習(xí)項(xiàng)目。
做個深呼吸,開始動手了。

后端開發(fā)

在終端中新建一個項(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í),將不需要的文件刪除,并且再新建幾個空文件。按照如下來修改我們的項(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 會遍歷這個列表中的應(yīng)用,并在使用 makemigrations 這個命令時(shí)才會自動的搜尋并創(chuàng)建我們應(yīng)用的模型。

MIDDLEWARE: 我們需要使用的中間件。由于 Django 不支持對 PUT 方法的數(shù)據(jù)處理,所以我們需要寫一個中間件來給它加上這個功能。之后我們會更加詳細(xì)的了解中間件的寫法。

DATABASES: 配置我們的數(shù)據(jù)庫。在這里,我們只是簡單的使用了 sqlite3 數(shù)據(jù)庫。

以上便是所有的配置了。

現(xiàn)在我們先來編寫 PUT 中間件,來讓 Django 支持 PUT 請求。我們可以使用 POST 方法向 Django 應(yīng)用上傳數(shù)據(jù),并且可以使用 request.POST 來訪問 POST 數(shù)據(jù)。我們也想像使用 POST 一樣來使用 PUT ,利用 request.PUT 就可以訪問到 PUT 請求的數(shù)據(jù)。

中間件是 django 很重要的一部分,它在請求和響應(yīng)之間充當(dāng)預(yù)處理器的角色。
很多通用的邏輯可以放到這里,django 會自動的調(diào)用他們。
在這里,我們寫了一個簡單的中間件來處理 PUT 請求。只要是 PUT 請求,我們就對它作這樣的處理。所以,當(dā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 請求
            setattr(request, 'PUT', QueryDict(request.body))  # 給請求設(shè)置 PUT 屬性,這樣我們就可以在視圖函數(shù)中訪問這個屬性了
            # request.body 是請求的主體。我們知道請求有請求頭,那請求的主體就是
            # request.body 了。當(dāng)然,你一定還會問,為什么這樣就可以訪問 PUT 請求的相關(guān)
            # 數(shù)據(jù)了呢?這涉及到了 http 協(xié)議的知識,這里就不展開了,有興趣的同學(xué)可以自行查閱資料
        response = get_response(request)  # 使用 get_response 返回響應(yīng)
        return response  # 返回響應(yīng)

    return middleware  # 返回核心的中間件處理函數(shù)

QueryDict 是 django 專門為請求的查詢字符串做的數(shù)據(jù)結(jié)構(gòu),它類似字典,但是又不是字典。
request 對象的 POST GET 屬性都是這樣的字典。類似字典,是因?yàn)?QueryDict 和 python 的 dict 有相似的 API 接口,所以你可以把它當(dāng)字典來調(diào)用。

不是字典,是因?yàn)?QueryDict 允許同一個鍵有多個直。比如 {'a':[‘1’,‘2’]},a 同時(shí)有值 1 和 2,所以,一般不要用 QueryDict[key] 的形式來訪問相應(yīng) key 的值,因?yàn)槟愕玫降臅且粋€列表,而不是一個單一的值,應(yīng)該用 QueryDict.get(key) 來獲取你想要的值,除非你知道你在干什么,你才能這樣來取值。為什么會允許多個值呢,因?yàn)?GET
請求中,常常有這種參數(shù)http://www.example.com/?action=search&achtion=filter
action 在這里有兩個值,有時(shí)候我們需要對這兩個值都作出響應(yīng)。但是當(dāng)你用 .get(key)方法取值的時(shí)候,只會取到最新的一個值。如果確實(shí)需要訪問這個鍵的多個值,應(yīng)該用 .getList(key) 方法來訪問,比如剛才的例子應(yīng)該用 request.GET.getList('action') 來訪問 action 的多個值。

同理,對于 POST 請求也應(yīng)該這么做。

接下來要說說 request.body 。做過爬蟲的同學(xué)一定都知道,請求有請求頭,那這個 body 就是我們的請求體了。嚴(yán)格的講,這個“請求體”應(yīng)該叫做“載荷”,用英文來講,這就叫做“payload”。載荷里又有許多的學(xué)問了,感興趣的同學(xué)可以自己去了解相關(guān)的資料。只需要知道一件很簡單的事情,就是把 request.body 放進(jìn) QueryDict 就可以把上傳的字段轉(zhuǎn)換為我們需要的字典了。

由于原生的 request 對象并沒有 PUT 屬性,所以我們需要在中間件中加上這個屬性,這樣我們就可以在視圖函數(shù)中用 request.PUT 來訪問 PUT 請求中的參數(shù)值了。

中間件在 1.11 版本里是一個可調(diào)用對象,和之前的類中間件不同。既然是可調(diào)用對象,那就有兩種寫法,一種是函數(shù),因?yàn)楹瘮?shù)就是一個可調(diào)用對象;一種是自己用類來寫一個可調(diào)用對象,也就是包含 __call__() 方法的類。

在 1.11 版本中,中間件對象應(yīng)該接收一個 get_response的參數(shù),這個參數(shù)用來獲取上一個中間件處理之后的響應(yīng),每個中間件處理完請求之后都應(yīng)該用這個函數(shù)來返回一個響應(yīng),我們不需要關(guān)心這個 get _response 函數(shù)是怎么寫的,是什么東西,只需要記得在最后調(diào)用它,返回響應(yīng)就好。這個最外層函數(shù)應(yīng)該返回一個函數(shù),用作真正的中間件處理。

在外層函數(shù)下寫你的預(yù)處理邏輯,比如配置什么的。當(dāng)然,你也可以在被返回的函數(shù)中寫配置和預(yù)處理。但是這么做有時(shí)候就有些不直觀,配置、預(yù)處理和核心邏輯分開,讓看代碼的人一眼就明白這個中間件是在做什么。最通常的例子是,很多的 API 會對請求做許多的處理,比如記錄下這個請求的 IP 地址就可以先在這里做這個步驟;又比如,為了控制訪問頻率,可以先讀取數(shù)據(jù)庫中的訪問數(shù)據(jù),根據(jù)訪問數(shù)據(jù)記錄來決定要不要讓這個請求進(jìn)入到視圖函數(shù)中。我們對 PUT 請求并沒有什么預(yù)處理或者配置操作要進(jìn)行,所以就什么都沒寫。

中間件的處理邏輯雖然簡單,但是中間件的寫法和作用大家還是需要掌握的。

接下來,讓我們創(chuàng)建我們的模型,編輯你的 models.py

from django.db import models


# 創(chuàng)建 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__ 。我們的表結(jié)構(gòu)很簡單,這里就不多說了。

我們的 API 返回的是 json 數(shù)據(jù)類型,所以我們需要把最基礎(chǔ)的響應(yīng)方式更改為 JsonResponse 。同時(shí),我們還有一個問題需要考慮,那就是如何把模型數(shù)據(jù)轉(zhuǎn)換為 json 類型。 我們知道 REST 中所說的 “表現(xiàn)(表層)狀態(tài)轉(zhuǎn)換” 就是這個意思,把不同類型的數(shù)據(jù)轉(zhuǎn)換為統(tǒng)一的類型,然后傳送給前端。如果前端要求是 json 那么我們就傳 json 過去,如果前端請求的是 xml 我們就傳 xml 過去。這就是“內(nèi)容協(xié)商(協(xié)作)”。當(dāng)然,我們的應(yīng)用很簡單,就只有一種形式,但是如果是其它的大型應(yīng)用,前端有時(shí)請求的是 json 格式的,有時(shí)請求的是 xml 格式的。我們的應(yīng)用很簡單,就不用考慮內(nèi)容協(xié)商了。

回到我們的問題,我們該如何把模型數(shù)據(jù)轉(zhuǎn)換為 json 數(shù)據(jù)呢? 把其它數(shù)據(jù)按照一定的格式保存下來,這個過程我們稱為“序列化”。“序列化”這個詞其實(shí)很形象,它把一系列的數(shù)據(jù),按照一定的方式給整整齊齊的排列好,保存下來,以便他用。在 Django 中,Django 為我們提供了一些簡單的序列化工具,我們可以使用這些工具來把模型的內(nèi)容轉(zhuǎn)換為 json 格式。

其中很重要的工具便是 serializers 了,看名字我們就這到它是用來干什么的。其核心函數(shù) serialize(format, queryset[,fields]) 就是用于把模型查詢集轉(zhuǎn)換為 json 字符串。它接收的三個參數(shù)分別為 formatformat 也就是序列化形式,如果我們需要 json 形式的,我們就把 format 賦值為 'json' 。 第二個參數(shù)為查詢集或者是一個含有模型實(shí)例的可迭代對象,也就是說,這個參數(shù)只能接收類似于列表的數(shù)據(jù)結(jié)構(gòu)。fields 是一個可選參數(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 模型,記得加個 `.`  哦。
import json  # 引入 json 庫,我們會用它來處理 json 字符串。

# 定義最基本的 API 視圖
class APIView(View):
    def response(self,
                 queryset=None,
                 fields=None,
                 **kwargs):
        """
        序列化傳入的 queryset 或 其他 python 數(shù)據(jù)類型。返回一個 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)是一個字符串
        # 我們最終需要把它放入 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)放在一起之后才會把它變成真正的 json 數(shù)據(jù)類型。

接下來,重頭戲到了,我們需要編寫我們的 Mixin 了。 在編寫 Mixin 之前,我們需要遵循以下幾個原則:

  1. 每個 Mixin 只完成一個功能。這就像是我們在“上”中舉的例子一樣,一個 Mixin 只會讓我們的“Man”類多一個功能出來。這是為了在使用的時(shí)候能夠更加清晰的明白這個 Mixin 是干什么的,同時(shí)能夠做到靈活的解耦功能,做到“即插即用”。

  2. 每個 Mixin 只操作自己知道的屬性和方法,還是那我們之前的 “Man” 類來做例子。我們知道我們寫的幾個 Mixin 最終都是用于 Man 類的,然而 Man 類的屬性有 nameage ,所以在我們的 Mixin 中也可以像這樣來訪問這些屬性: self.name , self.age 。因?yàn)檫@些屬性都是已知的。當(dāng)然啦,Mixin 自己的屬性當(dāng)然也是可以自己調(diào)用的啦。那在 Mixin 中我們需要用到其它的 Mixin 的屬性的時(shí)候該怎么辦呢?很簡單,直接繼承這個 Mixin 就好了。 我們的 Mixin 最終是要作用到視圖上的,所以我們可以把我們的基礎(chǔ)視圖的屬性當(dāng)作是已知屬性。 我們的 APIViewView 類的子類,所以 View 的所有屬性和方法我們的 Mixin 都可以調(diào)用。我們通常用到的屬性有:

     1. `kwargs`: 這是傳入視圖函數(shù)的關(guān)鍵字參數(shù),我們可以在類視圖中使用 `self.kwargs` 來訪問這些傳入的關(guān)鍵字參數(shù)。
     2. `args`: 傳入視圖的位置參數(shù)。
     3. `request`: 視圖函數(shù)的第一個參數(shù),也就是當(dāng)前的請求對象,它和我們平時(shí)寫的視圖函數(shù)中的 request 是一模一樣的。
    

編寫 Mixin 是為了代碼的復(fù)用和代碼的解耦,所以在正式開始編寫之前,我們必須要想好,哪一些 Mixin 是我們需要編寫的,哪一些邏輯是必須要寫到視圖函數(shù)中。
首先,凡是對于有查詢動作的請求,我們都有一個從數(shù)據(jù)庫中提取查詢集的過程,所以我們需要編
寫一個提取查詢集的 Mixin 。

第二,對于查詢集來說,有時(shí)候我們需要的是整個查詢集,有時(shí)候只是需要一個單一的查詢實(shí)例,比如在更新和刪除的時(shí)候,我們都是在對一個實(shí)例進(jìn)行操作。所以我們還需要編寫一個能夠提取出單一實(shí)例的 Mixin 。

第三,對于 API 的通用操作來說,根據(jù) REST 原則,每個請求都有自己的對應(yīng)動作,比如 put 對應(yīng)的是修改動作,post 對應(yīng)的是創(chuàng)建動作,delete 對應(yīng)的是刪除動作,所以我們需要為這些通用的 API 動作一一編寫 Mixin 。

第四,正如第三條考慮到的那樣, API 的不同請求是有自己對應(yīng)的默認(rèn)動作的。如果我們的視圖就是想簡單的使用他們的默認(rèn)動作,也就是 post 是創(chuàng)建動作,put 是修改動作,我們希望視圖函數(shù)能自己將這些請求自己就映射到這些默認(rèn)動作上,這樣在之后的開發(fā)我們就可以什么都不用做了,連最基本的 get post 視圖方法都不需要我們編寫。所以我們需要編寫一個方法映射 Mixin 。

最后,就我們的應(yīng)用而言,我們應(yīng)用是為了提供在線解釋器服務(wù),所以會有一個執(zhí)行代碼的功能,雖然到目前,這個功能的核心函數(shù)執(zhí)行的代碼很簡單,但是誰能保證他一直都是這樣簡單呢?所以為了保持良好的視圖解耦性,我們也需要把這部分的代碼單獨(dú)獨(dú)立出來成為一個 Mixin 。

現(xiàn)在,讓我們開始編寫我們的 Mixin 。我們編寫 Mixin 的活動都會在 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 錯誤會在像數(shù)據(jù)庫寫入數(shù)據(jù)創(chuàng)建不成功時(shí)被拋出,這是我們需要捕捉并做出響應(yīng)的錯誤。

獲取查詢集 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ù)是否被傳入,若沒有傳入則拋出錯誤
        assert self.model or self.queryset, 'No queryset fuound.'
        if self.queryset:
            return self.queryset
        else:
            return self.model.objects.all()

可以看到,我們的 Mixin 的設(shè)計(jì)很簡單,只是為子類提供了兩個參數(shù) querysetmodel,并且 get_queryset 這個方法會使用這兩個屬性返回相應(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)前請求中的實(shí)例。

    :lookup_args: list, 用來規(guī)定查詢參數(shù)的參數(shù)列表。默認(rèn)為 ['pk','id]
    """
    lookup_args = ['pk', 'id']

    def get_object(self):
        """
        通過查詢 lookup_args 中的參數(shù)值來返回當(dāng)前請求實(shí)例。當(dāng)獲取到參數(shù)值時(shí),則停止
        對之后的參數(shù)查詢。參數(shù)順序很重要。
        :return: 一個單一的查詢實(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中獲取對應(yīng)的 id 或者 pk 然后從查詢集中獲取相應(yīng)的實(shí)例。并且我們還可以靈活的配置查詢的關(guān)鍵詞是什么,這個 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)

我們可以看到,我們只是簡單的返回了查詢集,并且默認(rèn)的方法還支持傳入需要的序列化的字段。

執(zhí)行創(chuàng)建操作的 Mixin:

class APICreateMixin(APIQuerysetMinx):
    """
    API 中的 create 操作
    """
    def create(self, create_fields=None):
        """
        使用傳入的參數(shù)列表從 POST 值中獲取對應(yīng)參數(shù)值,并用這個值創(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 中獲取每個值
            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)動作已經(jīng)是從 POST 中獲取相應(yīng)的數(shù)據(jù),這就不用我們把提取數(shù)據(jù)的邏輯硬編碼在視圖中了,而且考慮到了足夠多的情況。并且我們還手動的傳入了 status ,方便前端開發(fā)能夠清楚的知道操作是否成功。

實(shí)例查詢 Mixin:

class APIDetailMixin(APISingleObjectMixin):
    """
    API 操作中查詢實(shí)例操作
    """
    def detail(self, fields=None):
        """
        返回當(dāng)前請求中的實(shí)例
        :param fields: 希望被返回實(shí)例中哪些字段被實(shí)例化
        :return: JsonResponse
        """
        return self.response(
            queryset=[self.get_object()],
            fields=fields)

同理,我們只是簡單的調(diào)用了 get_object 方法,并沒有做其它的處理。

更新 Mixin:

class APIUpdateMixin(APISingleObjectMixin):
    """
    API 中更新實(shí)例操作
    """
    def update(self, update_fields=None):
        """
        更新當(dāng)前請求中實(shí)例。更新成功則返回成功響應(yīng)。否則,返回更新失敗響應(yīng)。
        若傳入 updata_fields 更新字段列表,則只會從 PUT 上傳值中獲取這個列表中的字段,
        否則默認(rèn)為更新 POST 上傳值中所有的字段。
        :param update_fields: list, 實(shí)例需要被更新的字段
        :return: JsonResponse
        """
        instance = self.get_object() # 獲取當(dā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: # 捕捉更新錯誤
            return self.response(status='Failed to Update.') # 返回更新失敗響應(yīng)
        return self.response(
            status='Successfully Update')# 更新成功則返回更新成功響應(yīng)

setattr 的作用就是給一個對象設(shè)置屬性,當(dāng)查詢的實(shí)例被找到之后,我們采用這種方法來給實(shí)例更新值。因?yàn)槲覀冊谶@種情況下不能使用 . 路徑符來訪問字段,因?yàn)槲覀儾恢烙心男┳侄螘桓隆M瑫r(shí),作為 API 的 Mixin ,更新時(shí)獲取數(shù)據(jù)的地方已經(jīng)默認(rèn)為從 PUT 請求中獲取數(shù)據(jù)。

刪除操作 Mixin

class APIDeleteMixin(APISingleObjectMixin):
    """
    API 刪除實(shí)例操作
    """
    def remove(self):
        """
        刪除當(dā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 是請求方法名,我們不能占用它。

運(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, # 重定向錯誤輸出流到子進(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)錯信息
        except subprocess.TimeoutExpired as e: # 捕捉超時(shí)異常
            output = '\r\n'.join(['Time Out!', e.output]) # 獲取子進(jìn)程報(bào)錯,并添加運(yùn)行超時(shí)提示
        return output # 返回執(zhí)行結(jié)果

這個也不多說,就只是簡單的把之前的函數(shù)式更改為了類。不過要注意的是,如果你用的是 py2,subprocess 有的屬性的引用方式會和 3 有寫不同,大家可以自行去查閱如何正確引入相關(guān)的屬性。

前幾個 Mixin 都沒有很詳細(xì)的說,下面這個 Mixin 我們需要詳細(xì)的說明。

class APIMethodMapMixin(object):
    """
    將請求方法映射到子類屬性上

    :method_map: dict, 方法映射字典。
    如將 get 方法映射到 list 方法,其值則為 {'get':'list'}
    """
    method_map = {}
    def __init__(self,*args,**kwargs):
        """
        映射請求方法。會從傳入子類的關(guān)鍵字參數(shù)中尋找 method_map 參數(shù),期望值為 dict類型。尋找對應(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è)置對應(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 # 返回代理視圖

首先,大家不要被嚇到。我們慢慢來分析。
我們先給子類提供了一個 method_map 的屬性,這是一個字典,子類可以通過給這個字典配置相應(yīng)的值來使用我們的 APIMethodMapMixin ,字典的鍵為請求的方法名,值為要執(zhí)行的操作。。接下來看看 __init__ 方法,首先,會在子類視圖實(shí)例化的時(shí)候?qū)ふ?method_map 參數(shù),如果找到了就會以這個參數(shù)作為方法映射的字典,在子類中編寫的配置就不會生效了。也就是說:

# 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 中的屬性就沒用了,視圖會以初始化時(shí)的參數(shù)作為最終標(biāo)準(zhǔn)。由于我們的字典只是一個字符串,我們要做的是把子類的對應(yīng)操作方法和請求方法對應(yīng)起來,所以我們首先使用 getattr 來獲取子類的響應(yīng)操作的方法,然后利用了 view_proxy 代理了視圖方法。為什么我們需要這個代理方法?原因很簡單,因?yàn)樵谀J(rèn)的視圖中,View 會向視圖傳遞參數(shù),然而,我們的操作方法,他們的參數(shù)和被傳入視圖的參數(shù)是截然不同的,所以我們需要使用一個函數(shù)來代理接收這些參數(shù),這個函數(shù)就是我們視圖代理函數(shù)返回的 view 函數(shù),這個函數(shù)會接收所有傳向視圖的參數(shù),然后不對這些參數(shù)做出處理,只是簡單的調(diào)用被映射的方法。

python 基礎(chǔ)很不錯的同學(xué)應(yīng)該已經(jīng)發(fā)現(xiàn)了,我們的 view_proxy 的寫法不就是一個裝飾器的寫法嗎?是的,裝飾器也是這樣寫的,只是我們在 __init__ 中手動調(diào)用了它而已,平時(shí)我們用 @ 來使用裝飾器和我們手動調(diào)用的過程是完全相同的。在最后,我們把操作方法設(shè)置為了請求對應(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 模型,記得加個 `.`  哦。
import json  # 引入 json 庫,我們會用它來處理 json 字符串。
from .mixins import APIDetailMixin, APIUpdateMixin, \
    APIDeleteMixin, APIListMixin, APIRunCodeMixin, \
    APICreateMixin, APIMethodMapMixin, APISingleObjectMixin  # 引入我們編寫的所有 Mixin

我們的核心 API:

class APICodeView(APIListMixin,  # 獲取列表
                  APIDetailMixin,  # 獲取當(dāng)前請求實(shí)例詳細(xì)信息
                  APIUpdateMixin,  # 更新當(dāng)前請求實(shí)例
                  APIDeleteMixin,  # 刪除當(dāng)前實(shí)例
                  APICreateMixin,  # 創(chuàng)建新的的實(shí)例
                  APIMethodMapMixin,  # 請求方法與資源操作方法映射
                  APIView):  # 記得在最后繼承 APIView
    model = CodeModel  # 傳入模型

    def list(self):  # 這里僅僅是簡單的給父類的 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 請求僅對能獲取到 pk 值的 url 響應(yīng)
        :param request: 請求對象
        :param args: 位置參數(shù)
        :param kwargs: 關(guān)鍵字參數(shù)
        :return: JsonResponse
        """
        instance = self.get_object()  # 獲取對象
        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 請求可以被任意訪問,并會檢查 url 參數(shù)中的 save 值,如果 save 為 true 則會
        保存上傳代碼。
        :param request: 請求對象
        :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 請求僅對更改操作作出響應(yīng)
        :param request: 請求對象
        :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)

值得注意的是,我們使用了一個 save 參數(shù)來判斷上傳的代碼是否需要保存,因?yàn)樯蟼鞣绞蕉际?POST 我們在這種情況下就需要增加新的參數(shù)來決定是否需要保存。而且由于我們沒有怎么使用 Mixin ,所有的字段我們都是手動提取的,所有的操作過程都是我們自己寫的,就顯得有點(diǎn)笨搓搓的。由此可見 Mixin 是多么的好用。

別忘了,我們還要提供靜態(tài)文件服務(wù):

# 主頁視圖
def home(request):
    """
    讀取 'index.html' 并返回響應(yīng)
    :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 文件響應(yīng)
    :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 響應(yīng)


# 讀取 css 視圖
def css(request, filename):
    """
    讀取 css 文件,并返回 css 文件響應(yīng)
    :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 響應(yīng)

在靜態(tài)文件的響應(yīng)中需要把響應(yīng)頭更改為正確的響應(yīng)頭,不然瀏覽器就不認(rèn)識你傳回去的是什么靜態(tài)件了。

最后,按照我們之前的設(shè)計(jì),完成我們的 API URL 配置。

編輯你的 urls.py,這個文件是和你的 settings.py 在同一個目錄哦

# 這是我們的 URL 入口配置,我們直接將入口配置到具體的 URL 上。

from django.conf.urls import url, include  # 引入需要用到的配置函數(shù)
# include 用來引入其他的 URL 配置。參數(shù)可以是個路徑字符串,也可以是個 url 對象列表

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ù)
# 針對某個對象的操作 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'),  # 訪問某個特定對象
    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í)候是不會加 / 的,這是對一個文件的直接訪問。

最后,回到項(xiàng)目路徑下,運(yùn)行:

python manage.py makemigrations
python manage.py migrate

創(chuàng)建好數(shù)據(jù)庫之后我們就可以進(jìn)入前端的開發(fā)了。

我們的后端就算完成了。休息一下,準(zhǔn)備向前端進(jìn)發(fā)。

前端開發(fā)

我們把工作路徑切換到 frontend 下。這一次我們的重點(diǎn)放在 js 的編寫上。這一次的編寫沒有什么難點(diǎn),重點(diǎn)是在于對前端原理的理解和應(yīng)用上,代碼不難,但是希望大家著重的理解其中的設(shè)計(jì)模式。最好的方式就是自己敲一遍代碼。只有跟著敲一次才知道自己哪里有問題。

首先把我們需要的 js 和 css 都放在對應(yīng)的文件下。大家可以去我的 github 把 bootstrap.js 和 bootstrap.css 以及 jquery.js 下載下來,把 js 文件放在 js 路徑下,css 放在 css 路徑下。準(zhǔn)備工作做完了。

首先編寫我們的主頁 html ,這次的 html 做了一些改動,并且添加了新的元素。所以大家不要直接使用上一次的 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 類屬性,讓主頁填滿整個瀏覽器-->
    <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">請?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 布局,使每個選項(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ā)都會在 main.js 中進(jìn)行。

在第一章的開發(fā)中,我們的 API 很簡單,就一個 POST ,但是這一次,我們的 API 多,而且比較復(fù)雜,甚至還有 GET 參數(shù),所以我們需要管理我們的 API ,所以硬編碼 API 一定是行不通了,硬編碼 API 不僅會導(dǎo)致靈活性不夠,還會增加手動輸入錯誤的幾率。所以我們這樣來管理我們的 API:

const api = {
    v1: {  // api 版本號
        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é)會覺得使用函數(shù)來返回 API 是多此一舉的。但是我們想想,如果你的代碼特別多,特別長,你會不會寫著寫著就忘了自己調(diào)用的 API 是干什么的?所以為了保證良好的語義性,我們需要有良好的層級結(jié)構(gòu)和良好的命名規(guī)則。使用函數(shù)不僅可以正確的生成含有參數(shù)的 URL 而且也方便我們將來做進(jìn)一步的改進(jìn)。如果哪一天 API 發(fā)生變化了,我們直接在函數(shù)中做出對應(yīng)的修改就好了,不需要像硬編碼那樣挨著挨著更改。

接下來我們的核心概念來了 —— state。在“上”我們已經(jīng)知道了狀態(tài)的概念和store,就是用來儲存狀態(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 每個狀態(tài)有 state 和 changed 屬性,state 用來儲存 UI 相關(guān)聯(lián)的變量信息,changed 作為狀態(tài)是否改變的信號。UI 只需要監(jiān)聽 chagned 變量,當(dāng) changed 為 true 時(shí)才讀取并改變狀態(tài)。要是你忘了什么是“狀態(tài)”,趕緊回去看看上一個部分吧。

我們已經(jīng)定義完了 API 和 狀態(tài),但是真正向后端發(fā)起請求動作的函數(shù)還都沒有完成。接著在下面寫我們的動作函數(shù)。
這些動作負(fù)責(zé)調(diào)用 API ,并接受 API 返回的數(shù)據(jù),然后將這些數(shù)據(jù)保存進(jìn) store 中。注意,在修改完?duì)顟B(tài)之后,記得將狀態(tài)的 changed 屬性改為 true ,不然狀態(tài)不會刷新到監(jiān)聽的 UI 上。

得到單一的實(shí)例,因?yàn)槲覀?Django 模型序列化的之后的格式不是很符合我們的要求,所以我們需要做一些處理。模型字段序列化之后是這樣的。

{'model':'app.modelName','pk':'pk',fields:[modelFields]}

比如我們的 Code 模型,一個實(shí)例序列化之后值這樣的:

{'model':'online_intepreter_app.Code',pk:'1', fields[{'name':'name','code':'code'}]}

如果是查詢集,則返回的就是想上面一樣的對象列表。
我們需要把實(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)注意到了,請求完成之后,改變的狀態(tài)值,并且也發(fā)出了響應(yīng)的狀態(tài)更改信號,就是把changed更改為true

創(chuàng)建實(shí)例動作

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í)例動作

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

獲取實(shí)例動作

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;
        }
    })
}

我們在獲取實(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í)例的動作還是叫做 remove ,不叫 delete 是因?yàn)?delete 是默認(rèn)關(guān)鍵字。

運(yùn)行代碼的幾個動作也是和上面同理:

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 動作了,我們的 UI 需要跟隨這些動作而引起的狀態(tài)改變而做出對應(yīng)刷新動作,所以接下來讓我們來編寫每個 UI 的響應(yīng)刷新動作。

動態(tài)大小改變:

function flexSize(selector) {
    let ele = $(selector);
    ele.css({
        'height': 'auto',
        'overflow-y': 'hidden'
    }).height(ele.prop('scrollHeight'))
}
//將動態(tài)變動大小的動作綁定到輸入框上
$('#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);
}

在這里要注意的是,我們使用模板字符串來作為渲染列表的方法,。并且往其中也添加了對應(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)容先清除,不然之前的按鈕就會保留在頁面上。

我們有一個新建代碼的選項(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');// 由于我們在改變輸入、輸出框的內(nèi)容時(shí)并沒有出發(fā) ‘input’ 事件,所以需要手動運(yùn)行這個函數(shù)
    $('#code-options').empty().append(options);// 清除的之前的選項(xiàng),再添加當(dāng)前的選項(xiàng)
}

同樣的,我們需要清除之前的數(shù)據(jù)才可以把我們的選項(xiàng)給渲染上去。

終于,我們來到了最重要的部分。我們已經(jīng)編寫完了所有的動作。要怎么把這些動作給連接起來呢?我們需要在狀態(tài)改變的時(shí)候就出發(fā)動作,所以我們需要寫一個 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 一個 <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) {// 當(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('');// 記得請空上次運(yùn)行代碼的結(jié)果哦。
                    flexSize('#code-input');// 同樣的,沒有出發(fā) 'input' 動作,就要手動改變值
                    renderSpecificCodeOptions(instance.pk);// 渲染代碼選項(xiàng)
                    store[op].changed = false;// 把 changed 信號改回去
                }
                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');// 記得手動調(diào)用這個函數(shù)。
                    store[op].changed = false // changed 改回去
                }
                break;
        }
    }
}

我們的 watcher 會不斷的遍歷監(jiān)聽每個狀態(tài),一旦狀態(tài)改變,就會執(zhí)行相應(yīng)的動作。不過要注意的是,在動作執(zhí)行完的時(shí)候要把 changed 信號給修改回去,不然你的 UI 會一直刷新。

最后我們做好收尾工作。

getList();// 初始化的時(shí)候我們應(yīng)該手動的調(diào)用一次,好讓列表能在頁面上展示出來。
renderGeneralCodeOptions();// 手動調(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)檫@對我們以后的開發(fā)來說是非常重要的。 這個小項(xiàng)目加上注釋,還是比較難的,希望大家能理解其中的每一個知識點(diǎn)。或許有的同學(xué)已經(jīng)發(fā)現(xiàn)我們根本沒有必要自己來手寫實(shí)現(xiàn)一些“通用”的東西,比如 REST 規(guī)范下的一些 API 操作,完全可以用現(xiàn)成的輪子來代替。而且,我們的應(yīng)用并沒有對上傳的數(shù)據(jù)進(jìn)行檢查,這樣我們的應(yīng)用豈不是處于被攻擊的風(fēng)險(xiǎn)下?并且我們并沒有對 API 的請求頻率做出限制,要是有人寫個爬蟲無限制的訪問 API ,我們的應(yīng)用還可能會奔潰掉。我們還有太多的問題沒有考慮到。

為了解決以上的問題,在下一章,我們將會真正進(jìn)入 REST 開發(fā),使用 Django REST framework 來改進(jìn)我們的應(yīng)用。

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

推薦閱讀更多精彩內(nèi)容