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

項(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):

  1. Mixin 的編寫涉波,掌握 Mixin 的最基本編寫原則
  2. Store 與 state 的編寫。理解并能應(yīng)用 UI 的狀態(tài)概念炭序。
  3. 了解 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ù)分別為 formatformat 也就是序列化形式哄芜,如果我們需要 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è)原則:

  1. 每個(gè) Mixin 只完成一個(gè)功能。這就像是我們?cè)凇吧稀敝信e的例子一樣彩库,一個(gè) Mixin 只會(huì)讓我們的“Man”類多一個(gè)功能出來苞冯。這是為了在使用的時(shí)候能夠更加清晰的明白這個(gè) Mixin 是干什么的,同時(shí)能夠做到靈活的解耦功能侧巨,做到“即插即用”。

  2. 每個(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)作是已知屬性。 我們的 APIViewView 類的子類提澎,所以 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ù) querysetmodel,并且 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)用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末陷谱,一起剝皮案震驚了整個(gè)濱河市烙博,隨后出現(xiàn)的幾起案子瑟蜈,更是在濱河造成了極大的恐慌,老刑警劉巖渣窜,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铺根,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡乔宿,警方通過查閱死者的電腦和手機(jī)位迂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來详瑞,“玉大人掂林,你說我怎么就攤上這事「蚺埃” “怎么了党饮?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)驳庭。 經(jīng)常有香客問我刑顺,道長(zhǎng),這世上最難降的妖魔是什么饲常? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任蹲堂,我火速辦了婚禮,結(jié)果婚禮上贝淤,老公的妹妹穿的比我還像新娘柒竞。我一直安慰自己,他們只是感情好播聪,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布朽基。 她就那樣靜靜地躺著,像睡著了一般离陶。 火紅的嫁衣襯著肌膚如雪稼虎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天招刨,我揣著相機(jī)與錄音霎俩,去河邊找鬼。 笑死沉眶,一個(gè)胖子當(dāng)著我的面吹牛打却,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播谎倔,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼柳击,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了片习?” 一聲冷哼從身側(cè)響起捌肴,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤彤守,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后哭靖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體具垫,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年试幽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了筝蚕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡铺坞,死狀恐怖起宽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情济榨,我是刑警寧澤坯沪,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站擒滑,受9級(jí)特大地震影響腐晾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜丐一,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一藻糖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧库车,春花似錦巨柒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至珍坊,卻和暖如春牺勾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背垫蛆。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工禽最, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留腺怯,地道東北人袱饭。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像呛占,于是被迫代替她去往敵國(guó)和親虑乖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

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