Django RESTful 系列教程(一)


這是一個關于 Django RESTful 開發的教程。教程將會持續更新,更新進度為每個星期一篇。我們將會學習 Django RESTful 開發。在你閱讀這個系列的教程之前,你需要注意這些:

  • 筆者用的是 python3.5 ,Django 1.11
  • 熟練 python 的使用。當文中提到裝飾器或者等概念時,請有最基本的映像。了解 JavaScript 的基本使用。
  • 在跟隨教程的任何過程中,有任何問題,大家可以評論留言,或者給我發郵件 1130195942@qq.com ,或者是在 github 上提 issue。
  • 所有的代碼和教程的 MakrDown 文本都可以在 github上找到。歡迎大家 clone 或者 star 。
  • 以前做過 Django 的相關項目,對 Django 有一定的了解,至少完成過官方的入門教程。在講到 模型視圖 等概念時,有一定的了解。從一定程度上來說,這也是一個進階的教程。
  • 在本教程的最后,我們將會使用 DjangoDjango REST frameworkVueVue-Router 來做一個前后端分離的項目,也就是說,這篇教程會包含前端的內容,如果你對前端不了解也沒關系,在擁有最基本的基礎之上,大膽跟教程走可以了。
  • 轉載請聯系!

本章概要:

很多的 web 框架都以方便使用而著稱,特別是 flask ,一個文件就可以做一個 Hellow world 了,那 django 可以嗎?答案時肯定的。同時,我們將會簡單的了解下 REST 的概念 。最后,我們將會利用我們才學的知識來編寫我們的第一個 REST 項目

  • 單文件 django
  • REST 是什么
  • 第一個 Django REST 項目。

單文件 Django

發生了什么?

相信大家對 Django 有一定的了解,對構建項目的每個過程也已經非常清楚了。總是重復的那么幾個步驟:

  1. 先運行 django-admin startproject <your-project> 創建項目
  2. 再切換到項目路徑下運行 python manage.py startapp <your-app>,創建項目的 app 。
  3. 在每個 App 里寫代碼,寫完了最后想要運行項目時運行 python manage.py runserver 來啟動本地的開發服務器。

有的時候,我們僅僅是想做個實驗,僅僅時想看看剛才手動寫入的數據到底有沒有正確寫入或者是看看我的視圖反響解析出來到底是什么樣子。更重要的是,我們不想每次需要查看一些相關數據時,都需要從 app 目錄里切出來,然后 runserver 。或許你會辯駁說,我們有 django 提供的 shell 可用,這樣也可以很方便的和我們的應用交互。那么能不能再簡單一點?換句話說,我們能不能直接執行我們當前編寫的腳本呢?

新建一個文件夾,叫做 test-project 。并在里面創建一個新的文件 test.py 。你的目錄結構大概是這樣的:

test/
    test.py

在開頭引入這些包:

test.py

from django.conf import settings 
from django.http import HttpResponse
from django.conf.urls import url

我們依次來看看他們都是什么意思。

from django.conf import settings

settings 是 django 的配置文件鉤子,你可以在項目的任何地方引入它,可以通過 . 路徑符來訪問項目的配置。比如 settings.ROOT_URLCONF 就會返回根 url 配置。關于鉤子,我需要多說兩句。講道理,如果需要引用項目配置,標準的寫法難道不應該是 import project.settings as settings 嗎,這樣才能連接到項目的配置啊,為什么我只是引入 django 自己的配置就可以了呢。這就是 django 的神奇之處了,在一切都還沒有運行之前,django 首先做的就是加載配置文件,并且把 settings 對象的屬性連接到各個配置上。注意,settings 是個對象,所以像 from django.conf.settings import DEBUG 之類的語法是錯誤的。因為它不是個模塊。所以在訪問配置時,只能以 settings.<key> 的形式來調用配置。

首先加載配置文件是一件天經地義的事情,只有知道了各個部分的配置如何,相應的功能才能按照需求運轉。請大家記住這一點,這非常重要。在 django 中,加載配置文件有兩種方式:

第一種是使用 settings.configure(**settings)
手動的寫每一項配置,這樣做的好處是,如果你需要配置的東西不多,那就不單獨再建個文件作為配置文件了。

第二種是使用 django.setup()
這是通過環境變量來配置的方法。
django.setup() 方法會自己查詢環境變量 'DJANGO_SETTINGS_MODULE` 的值,會把它的值作為配置文件的路徑,并讀取這個文件的配置。

以上兩種方法都可以用來配置 django 。我們這里采用第一種。注意,兩種方式必須用一種,也就是說,想要使用 django ,必須對 django 進行配置。

from django.http import HttpResponse

用于返回一個響應。

from django.conf.urls import url
用于配置 urlpatterns 。

首先,讓我們來編寫配置,在 test.py下一行接著寫:

test.py

setting = {
    'DEBUG':True,
    'ROOT_URLCONF':__name__
}

settings.configure(**setting)

我們只是進行了簡單的配置,設置 DEGUBTrue 是因為我們想要在出錯時能看到錯誤報告。設置 ROOT_URLCONF__name__ 也就是這個文件本身,也就是說,我們打算把 urlpatterns 這個變量寫進這個文件中。

這個配置很簡單吧。

接下來讓我們編寫視圖,在 test.py 加入以下代碼:

test.py

def home(request):
    return HttpResponse('Hello world!')

這個視圖非常簡單,僅僅是返回一個字符串。

最后,把 urlpatterns 寫在下面:

test.py

urlpatterns = [url('^$',home,name='home')]

到目前為止,你的代碼應該是這樣的:

test.py

from django.conf import settings
from django.http import HttpResponse
from django.conf.urls import url
setting = {
    'DEBUG':True,
    'ROOT_URLCONF':__name__
}

settings.configure(**setting)

def home(request):
    return HttpResponse('Hello world!')

urlpatterns = [url('^$',home,name='home')]

該如何運行呢?一般情況下,我們是用 manage.py 來運行的。那 manage.py 又是怎么運行的?在 manage.py 內部,它調用了 django 的 exute_from_command_line(**command_line_args) 方法來運行我們的應用,所以,把這部分代碼添加到最后(實際上,這是從 manage.py 復制粘貼過來的,去掉了不必要的部分,大家也可以這么做,嘿嘿嘿):

test.py

if __name__ == '__main__':
    import sys
    from django.core.management import execute_from_command_line
    execute_from_command_line(sys.argv)

此時,你的代碼應該長這樣:

test.py

from django.conf import settings
from django.http import HttpResponse
from django.conf.urls import url
setting = {
    'DEBUG':True,
    'ROOT_URLCONF':__name__
}

settings.configure(**setting)

def home(request):
    return HttpResponse('Hello world!')

urlpatterns = [url('^$',home,name='home')]

if __name__ == '__main__':
    import sys
    from django.core.management import execute_from_command_line
    execute_from_command_line(sys.argv)

回到 test 目錄下,在終端運行 python test.py runserver ,然后在瀏覽器訪問 127.0.0.1:8000 ,不出意外的話,你會看到瀏覽器上有個 hello world

我們僅僅用了 19 行代碼就完成了一個單文件的 django 應用。其實它的原理很簡單,就是把以前分開的代碼給放在了一起,urls.pytes.pysettings.pytest.pyviews.pytest.py,甚至連 manage.py 也是 test.py
這個小 demo 意義在于讓大家了解 django 在運行的時候都發生了些什么,了解 django 的運行流程,為以后的開發打下基礎。

REST 是什么

先有個印象

REST的種種好處我不再贅述。簡單的說說為什么我們需要用 REST 。相信寫過模板的同學都知道,只要哪怕頁面中的數據有一絲絲變動,那整個頁面都需要重新渲染,這對性能無疑是巨大的浪費,并且頁面中只有一些元素會和數據相聯系,比如列表中的 <li> 元素,如果數據有變化,能直接只更新 <li> 元素就好了,REST 就是為此而生。
提到 REST ,很多人可能知道一些概念,比如我們將要做的前后端分離的項目會用到它,大概明白它可以用用 json 來交換數據。REST 不是什么具體的軟件或者代碼,而是一種思想。這么說就太抽象了,REST 剛出來的時候是以論文的形式提出的,是一種設計的形式。對它的概念我們就先了解到這里。在本章,我們就把 REST 簡單的當作是不再讓 django 來渲染我們的前端,而是用 JS 在前端請求數據,用 JS 來渲染我們的頁面。讓 django 專注于后端的數據處理。

我們的 REST

為了明確我們的 REST 開發,我們的前后端的分工大概如下:

客戶端(瀏覽器)----> 前端頁面-----> 后端處理數據,并把數據以 json 形式發送到前端
(這里本來是 flow 流程圖,結果簡書貌似不支持)

我們的 REST 設計目前就是這樣,實際上,REST 的抽象架構也就是這樣的,

第一個REST項目

這個項目的意義在于讓大家了解 REST 的大致開發流程,踩踩需要踩的坑。這次我們會做一個簡單的在線代碼執行系統,由于不會用到數據庫和模版,所以我們就使用剛才學習的單文件 django 來開發這個應用。

注意,在開發這個應用時,需要你對 JavaScriptJQuery 有最基本的了解,要是你對他們還不了解,那就在敲代碼時多多查閱文檔,在練習當中學會他們。同時我們還會使用 Bootstrap 。在跟隨教程敲代碼時,注意多翻翻文檔,一邊敲一邊查看文檔,搞明白每一行代碼是是什么意思。同時,代碼注釋也是很好的文檔搜索關鍵詞。

設計應用

我們希望在用戶訪問我們的主頁,并能在頁面中編寫python代碼,在點擊執行按鈕時,主頁上能返回程序執行的結果。

創建我們的應用

新建一個文件夾,叫做 online_python ,并創建的目錄結構:

onlie_python/
    index.html
    online_app.py

準備工作

先把剛才在 test.py 里的代碼復制過來,

online_app.py

from django.conf import settings
from django.http import HttpResponse
from django.conf.urls import url
setting = {
    'DEBUG':True,
    'ROOT_URLCONF':__name__
}

settings.configure(**setting)

def home(request):
    return HttpResponse('Hello world!')

urlpatterns = [url('^$',home,name='home')]

if __name__ == '__main__':
    import sys
    from django.core.management import execute_from_command_line
    execute_from_command_line(sys.argv)

我們需要用戶在訪問訪問 http://127.0.0.1:8000/ 時,視圖應該返回主頁的 html,也就是我們的 index.html 。由于我們并沒有使用 django 的模板引擎,所以 render 函數也不能用了。所以我們需要自己手動的把 index.html 寫入到響應中。所以把我們的 home 函數改成這個樣子:

online_app.py

def home(request):
    with open('index.html','rb') as f:
        html = f.read()
    return HttpResponse(html)

注意,這里是以二進制讀取的方式('rb')打開的 index.html ,也就是說最終的 html 的值為字節串,也就是 b'....'的形式,為什么要用二進制形式打開呢?
原因有兩個:

  1. 最主要的也是最重要的,在一個 html 文件中,你不知道會有什么樣的語言夾雜進去,一旦 python 無法識別其中的編碼,就會報編碼錯誤。然而實際上,讀取并解析 html 是瀏覽器來完成的工作,django 只是簡單的充當一個傳遞者的角色,它只需要把 html 文件傳給瀏覽器即可。
  1. 這也涉及到了一些瀏覽器和服務器數據傳輸的知識,瀏覽器與服務器的內容交互都是以二進制流的方式進行的,所以正規的響應就應返回字節串。django 的 HttpResponse 為我們做了轉換的工作,所以你也可以把字符串傳給 HttpResponse

由于我們的 index.html 還沒有任何內容,在 index.html 寫入以下內容:

index.html

<!DOCTYPE html>
<html>
<head>
    <title>在線 Python 解釋器</title>
</head>
<body>
<h1>在線 Python 解釋器</h1>
</body>
</html>

在根路徑下運行 python online_app.py runserver ,在瀏覽器中訪問 http://127.0.0.1:8000 ,你應該可以在瀏覽器中看到 在線 Python 解釋器 的字樣。

一切已經就緒,你準備好了嗎?

前端開發

接下來,讓我們專注于前端的開發,如果你對 js 和 jqery 不是很了解,那也沒關系,教程中會進行講解,如果有不懂的地方,利用教程中的關鍵詞去查文檔就行了。

我們需要使用 Bootstrap ,所以要引入 jqery。在 Bootstrap 的官網的基本模板給復制進來并替換掉原來的代碼,刪除其中的注釋,此時你的 index.html 是這樣的:

index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bootstrap 101 Template</title>
    <link href="css/bootstrap.min.css" rel="stylesheet">
  </head>
  <body>
    <h1>你好,世界!</h1>
    <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
    <script src="js/bootstrap.min.js"></script>
  </body>
</html>

我們需要從頁面中來引用 Bootstrap 的 js 文件和 css 文件,所以把第 8 行 替換為:

<link rel="stylesheet"  integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

把第 13 行替換為:

<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

現在,讓我們正式開始前端頁面的開發。最好把 Bootstrap 的官方文檔打開,好方便隨時查閱。

首先,我們需要對頁面進行布局,先把我們頁面的大概的樣子設計好,我們的頁面大概是這樣的:

+-------------------------+
+       ---標題----       +
+ 代碼輸入框     結果顯示框 +
+            |            +
+  +------+  |  +------+  +
+  +      +  |  +      +  +
+  +      +  |  +      +  +
+  +      +  |  +      +  +
+  +------+  |  +------+  +
+            |            +
+                         +
+-------------------------+

由于前端代碼的特殊性,代碼所在的行對最終的結果有影響,所以我給下面的代碼手動添加了行號。

注意:在下面的代碼中 + 與 - 分別代表代碼的刪除、增加,他們之前的數字是行號。

把第 7 行的改成:
index.html

7- <title>Bootstrap 101 Template</title>
7+ <title>在線 PYthony 解釋器</title>

刪除第 11 行內容, 并替換為Bootstrap 布局容器 <div class=container></div>,我們將會在這個布局容器中完成我們的頁面。

index.html

11- <h1>你好,世界!</h1>
11+  <div class="container"><!-- 頁面的整體布局 -->
12+      
13+  </div>

我們可以大致把頁面看成兩個 Bootstrap container 的兩個 row。
也就是:

+-------------------------+
+       ---標題----       +---------> 標題單獨為一行
+ 代碼輸入框   結果顯示框   +------>+
+            |            +      +
+  +------+  |  +------+  +      +
+  +      +  |  +      +  +      +
+  +      +  |  +      +  +      +-----> 主體內容可以看作一行分成了兩列
+  +      +  |  +      +  +      +
+  +------+  |  +------+  +      +
+            |            +      +
+                         +----->+
+-------------------------+

按照上面的布局,我們這樣來寫代碼:

inxex.html

12+      <div class="row"> <!-- 這一行單獨用來放標題 -->
13+        <div class="col-md-12"> <!-- 根據 bs規定,所有內容應放在 col 中。這一列占滿一行 -->
14+           <p class="text-center h1"> <!-- text-center 類是 bs 中央排版,h1 是 bs 一號標題類 -->
15+           在線 Python 解釋器
16+         </p>
17+        </div>
18+      </div>
19       <hr><!-- 標題和真正內容的分割線 -->
20+      <div class="row"></div><!-- 這一行用來放置主要內容 -->

保存你的代碼,在瀏覽器中打開 index.html 你可以看到瀏覽器中央已經有個標題了。

已經可以看見標題了

接下來我們把代碼輸入框和結果顯示框也完成。
因為我們的主體布局是左右布局,所以我們要先把左右布局先寫好:

index.html

20 <div class="row"><!-- 這一行用來放置主要內容 -->
21+  <div class="col-md-6"></div><!-- 代碼輸入部分 -->
22+  <div class="col-md-6"></div><!-- 結果顯示部分 -->
23</div>

現在我們把需要在屏幕上顯示的具體元素先寫好:

代碼輸入部分:

index.html

21<div class="col-lg-6"><!-- 代碼輸入部分 -->
22+  <p class="text-center h3">
23+    在下面輸入代碼
24+  </p>
25+  <textarea id="code" class="form-control" placeholder="Your code here."></textarea> 
26+  <button type="button" class="btn btn-primary">運行</button>
27</div>

結果顯示部分:

index.html

28<div class="col-lg-6"><!-- 結果顯示部分 -->
29+   <p class="text-center">運行結果</p>
30+   <div class="col-lg-12"><textarea id="output" disabled placeholder="Please input your code and click <run> button to excute your python script" class="text-center form-control"></textarea></div>
31+   </div>
32</div>

我們為了不去處理前端復雜的轉義符號,我們就用 <textarea> 來展示我們的文本,只是這個文本是不可編輯的。

我們大概的框架就已經寫好了,目前,你的 index.html 應該是這個樣子的:

index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>在線 Python 解釋器</title>
    <link rel="stylesheet"  integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  </head>
  <body><!--在下面的注釋中 bs 代表 bootstrap -->
  <div class="container"><!-- 頁面的整體布局 -->
      <div class="row"> <!-- 這一行單獨用來放標題 -->
        <div class="col-lg-12"> <!-- 根據 bs規定,所有內容應放在 col 中。這一列占滿一行 -->
          <p class="text-center h1"> <!-- text-center 是 bs 中央排版類,h1 是 bs 一號標題類 -->
            在線 Python 解釋器
          </p>
        </div>
      </div>
      <hr><!-- 標題和真正內容的分割線 -->
      <div class="row"><!-- 這一行用來放置主要內容 -->
        <div class="col-lg-6"><!-- 代碼輸入部分 -->
          <p class="text-center h3">
            在下面輸入代碼
          </p>
          <textarea id="code" placeholder="Your code here." class="form-control"></textarea>
          <button id="run" type="button" class="btn btn-primary ">運行</button>
        </div>
        <div class="col-lg-6"><!-- 結果顯示部分 -->
        <p class="text-center h3">運行結果</p>
        <div class="col-lg-12"><textarea id="output" disabled placeholder="Please input your code and click <run> button to excute your python script" class="text-center form-control"></textarea></div>
        </div>
      </div>
  </div>
    <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
  </body>
</html>

現在,在瀏覽器里打開你的 index.html ,你會看到它是這個樣子的:

十分丑陋的界面

現在我們已經看到了一個很粗糙的界面了,雖然很丑,但是一切都在按照我們的計劃進行。
接下來,讓我們來編寫一些簡單的 css 來讓界面變的美觀一點,你要是不知道這些 css 都是什么意思,MDN 是個查詢文檔的好地方。

在第8 行下面插入下面的代碼:

index.html

9+<style type="text/css">
10+    
11+</style>

按鈕的位置好像太偏了,讓我們用 css 來把它調整到一個合適的地方:

為了能夠改變 button 的位置,我們需要在外面套上一個 div 元素,我們希望把按鈕放在右邊,所以需要用到 text-right
index.html

29- <button id="run" type="button" class="btn btn-primary">運行</button>
29+ <div class="text-right"><button id="run" type="button" class="btn btn-primary ">運行</button></div>

然后為我們的 button 添加上 css 樣式:

index.html

10+#run {
21+    width: 20%; /*規定按鈕的寬度*/
12+    margin-top: 10px; /*留出和輸入框的間距*/
13+}

保存你的 index.html 文件,在瀏覽器中打開它,你會看到它是這個這樣子的:

美化之后的主頁

大家在打開的頁面中,試著輸入幾行代碼。你會發現這樣的情況:

不合適的輸入框

輸入框的大小是固定的,只有手動的點擊右邊的翻頁按鈕才可以看到下面的代碼,這樣的輸入框用起來簡直不方便,我們需要改善一下用戶體驗。我們想讓輸入框的大小隨著輸入代碼的行數而改變,也就是說,輸入框的大小是動態變化的,同時,我們希望我們的輸出框也是動態變化的。這就需要用到 js 了。
在 41 行下面插入一個 <script> 標簽
index.html

42+<script>
43+    
44+</script>

我們先來梳理一下動態輸入輸出框的邏輯。輸入一次大小就變化一次,也就是說,它們的 css 是動態變化的,也就是它們的高度是動態變化的。在 <textarea> 中,當用戶的輸入超出了 <textarea> 的大小時,它的右邊就會自動出現一個滾動條,如果我們讓 textarea 的高度等度滾動條的高度,那么此時 <textarea> 的高度就等于用戶輸入的文本高度了。所以我們需要在用戶輸入一次之后就調整一下大小。
先來編寫改變大小的函數:

index.html

43+ // 改變大小函數
44+function changeSize(ele){
45+   $(ele).css({'height':'auto','overflow-y':'hidden'}).height(ele.scrollHeight)
46+}

我們用 js 動態的改變了 <textarea> 的高度 ,在這里我們需要注意一點,我們并沒有一來就把高度設置為 <textarea> 滾動條的高度,而是先讓它自動適應,然后再改變它的大小。這是為了讓輸入框能夠自動“縮回去”,想想看,如果我輸入了幾行文本,出現了滾動條,此時我們的輸入框自動調整大小,滾動條消失,然后又刪除這幾行文本,你會發現,我們的輸入框“回不去”了,為什么呢?因為此時,滾動條的高度還是原來的高度,所以輸入框還是原來的大小,需要改變這個大小,所以我們就需要 height:auto 來幫我們“縮回去。

沒看懂剛才的解釋?沒關系,等我們完成這一部分,會有一個小實驗,大家跟著試一次就明白了。

現在把這個動態的變化應用到輸入框。

index.html

47+// 應用到輸入框
48+$('#code').each(function(){
49+    this.oninput = function(){
50+      changeSize(this)
51+    }
52+  })

現在,保存你的 index.html,在瀏覽器里打開他,不出意外的話,你看到的會是這個效果:

動態輸入框效果

現在我們來做之前說的實驗。
大家把 45 行改成這樣,去掉了:

index.html

45- $(ele).css({'height':'auto','overflow-y':'hidden'}).height(ele.scrollHeight)}
45+$(ele).css({'height':ele.scrollHeight,'overflow-y':'hidden'})
      }

保存之后在瀏覽器里打開,你會發現效果是這樣的:


不能“縮回去”的輸入框

所以我們先使 height:auto ,再讓高度等于滾動條的高度,才能讓輸入框“縮回去”。現在實驗做完了,把 45 行改回去。

index.html

45-$(ele).css({'height':ele.scrollHeight,'overflow-y':'hidden'})}
45+ $(ele).css({'height':'auto','overflow-y':'hidden'}).height(ele.scrollHeight)}

仔細的讀者也許已經發現,輸入和輸出框右下角有個小三角,那是瀏覽器為了方便用戶自己調整大小而產生的,然而我們并不希望用戶這樣做,所以我們需要禁用這個功能,只需要在 css 里加入 resize: none; 就可以了。是不是覺得字體很小?我們也把字體改的大一點。

index.html

14+#code {
15+  font-size: 25px;
16+  resize: none;
17+}
18+#output {
19+  font-size: 25px;
20+  resize: none;
21+}

此時你的 index.html 應該長得像這樣:

index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>在線 Python 解釋器</title>
    <link rel="stylesheet"  integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <style>
      #run {
          width: 20%; /*規定按鈕的寬度*/
          margin-top: 10px; /*留出和輸入框的間距*/
      }
      #code {
        font-size: 25px;
        resize: none;
      }
      #output {
        font-size: 25px;
        resize: none;
      }
    </style>
  </head>
  <body><!--在下面的注釋中 bs 代表 bootstrap -->
  <div class="container"><!-- 頁面的整體布局 -->
      <div class="row"> <!-- 這一行單獨用來放標題 -->
        <div class="col-lg-12"> <!-- 根據 bs規定,所有內容應放在 col 中。這一列占滿一行 -->
          <p class="text-center h1"> <!-- text-center 是 bs 中央排版類,h1 是 bs 一號標題類 -->
            在線 Python 解釋器
          </p>
        </div>
      </div>
      <hr><!-- 標題和真正內容的分割線 -->
      <div class="row"><!-- 這一行用來放置主要內容 -->
        <div class="col-lg-6"><!-- 代碼輸入部分 -->
          <p class="text-center h3">
            在下面輸入代碼
          </p>
          <textarea id="code" placeholder="Your code here." class="form-control" ></textarea>
          <div class='text-right'><button id="run" type="button" class="btn btn-primary ">運行</button></div>
        </div>
        <div class="col-lg-6"><!-- 結果顯示部分 -->
        <p class="text-center h3">運行結果</p>
        <div class="col-lg-12"><textarea id="output" disabled placeholder="Please input your code and click <run> button to excute your python script" class="text-center form-control"></textarea></div>
        </div>
      </div>
  </div>
    <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
    <script>
      // 動態大小函數
      function changeSize(ele){
        $(ele).css({'height':'auto','overflow-y':'hidden'}).height(ele.scrollHeight)
      }
      // 應用到輸入框
      $('#code').each(function(){
          this.oninput = function(){
            changeSize(this)
          }
        })
    </script>
  </body>
</html>

保存你的 index.html ,在瀏覽器里打開它,你看到的應該是這樣:

最終的樣子

到目前,我們有關界面 UI 顯示的的部分已經全部完成了。當然,有興趣的同學可以自己加一個背景。我自己隨便選了個背景。。


我加的背景

現在重頭來了,我們將實現前端頁面和后端的交互部分。
同樣的,我們先來完成對交互的設計。我們希望這樣來交互:
用戶點擊 運行 按鈕時,js 自動發送輸入框的代碼,待后端處理完之后,接收來自后端的結果,然后再把結果顯示在輸出框內。我們希望我們用 POST 方法向后端的 /api/ 路徑發送用戶代碼。

在真正開始開發之前,在這里我們會用到一個東西,叫做 ajax,它相當于前端的 requests ,為我們提供了 js 向 URL 發送請求的功能,只是功能沒 requests 那么強大,jquery 提供了 ajax 支持,所以我們直接使用就好了。不過我建議,對 ajax 不了解的的同學,現在最好打開 jquery 的 ajax 部分的文檔,在跟隨代碼時對照著看。

先獲取用戶輸入框代碼:

index.html

61+//獲取輸入框代碼
62+function getCode(){
63+  return $('#code').val()
64+}

將獲取的結果打印到輸出框,同時,輸出框需要根據內容的大小而改變。
index.hthml

65+//打印結果到輸出框并改變輸出框大小
66+function print(data){
67+  var ele = document.getElementById('output')
68+  output.value = data['output']
69+  changeSize(output)
70+}

需要注意的是,我們的打印函數最終是作為 ajax 請求成功之后的回調函數來使用的,ajax 會自動往里面傳入一個 data 參數,這個 data 是響應數據。我們并沒有直接就打印 data ,因為萬一后端需要對數據做進一步的分類,比如多一個 status 字段來表示代碼執行狀態(成功或者失敗),那么直接打印 data 就是不合適的做法了。所以我們選擇的是提取 data 的 output 字段,這樣不管 data 怎么變,只要有 output 參數,我們展示結果的代碼就能正常執行。

最后,把發送代碼的動作綁定到點擊按鈕:

index.html

71+// 點擊按鈕發送代碼
72+$('#run').click(function(){
73+  $.ajax({
74+    url:'/api/', //代碼發送的地址
75+    type:'POST', // 請求類型
76+    data: {'code':getCode()},//調用代碼獲取函數,獲得代碼文本
77+    dataType: 'json', //期望獲取的響應類型為 json
78+    success: print // 在請求成功之后調用 pprint 函數,將結果打印到輸出框
79+  })
80+})

到這里,我們前端的所有內容就算完成了。完整的前端代碼大家可以在 github 中找到,就不貼在這里了。接下來,讓我們進入后端開發。

后端開發

經過了漫長的前端開發,我們終于來到了后端。我們的代碼在這里將不會再標行數。所以大家可以靈活安排自己的代碼,自由的做相應的調整。

打開 online_app.py ,現在頂部引入:

online_app.py

from django.views.decorators.http import require_POST # 目前的 API 視圖只能用于接收 POST 請求
from django.http import JsonResponse # 用于返回 JSON 數據

先來編寫我們的 api 視圖函數:

online_app.py

@require_POST
def api(request):
    code = request.POST.get('code')
    output = run_code(code)
    return JsonResponse(data={'output':output})

具體運行代碼的函數我們將會在下面實現。在下面把我們的 URL 配置改成這樣,加上我們的 api 視圖。

online_app.py

urlpatterns = [url('^api/$',api,name='api'),
                url('^$',home,name='home')]

現在我們來實現 run_code 函數。在接著往下看之前,先做個深呼吸,因為這個函數會用到你可能不熟悉的模塊 subprocess ,當很多人看到這個模塊的名字或者聽到“多進程”這個詞的時候,或許他能對 python 實現多進程的種種缺點批判一番,但是當叫他真的寫個多進程時卻會感到十分為難。別擔心,我們只是在這里簡單的使用 subprocess 封裝好了的功能。為了更好的編寫這個函數,確保它的功能正常,我們需要為這個函數編寫測試。所以我們需要在編寫好了這個函數在把它應用到我們的 app 中,所以在你的 app 的路徑,也就是 online_python 下建一個新文件 test.py 。為了一切從簡,這里我們就不使用 unittest 了,我們使用人肉測試。

先引入 subprocess
test.py

import subprocess

接下來我們需要仔細考慮 run_code 會遇到的情況:

  1. 能夠正確執行來自客戶端的代碼。也就是說,如果客戶端的代碼是正確的,那么 run_code 的輸出結果也應該是預期的那樣。
  2. 當用戶代碼發生錯誤時,能夠返回錯誤信息。來自客戶端的代碼難免會有錯誤,我們需要像 python 解釋器一樣返回詳盡的錯誤跟蹤信息。
  3. 當用戶的代碼執行時間過長時,自動中斷代碼的執行,并在前端給出執行超時提示。有的時候,客戶端的代碼可能陷入死循環,為了提早讓用戶知道代碼異常,我們應該主動中斷代碼執行。有的時候用戶代碼可能是正確的,但是執行時間真的太長,我們也需要中斷執行,不能讓這個進程一直占用系統資源。一旦用戶過多,系統資源很快就會支撐不住

在編寫 run_code 的過程中,也是對 subprocess 模塊的學習,所以大家可以把 subprocess 文檔打開對照著看

首先,run_code 能正確的執行客戶端代碼。由于我們是直接運行的字符串,所以首先得解決如何用 python 腳本來執行 python 字符串。那就是使用 python -c <your_script_code> 命令。所以我們應該開一個進程來執行這個命令。在 subprocess 中,執行一個進程最常用的方法是 subprocess.run(*args,**kwargs), 但是它不返回輸出結果,所以我們需要使用 subprocess.check_output(*args,**kwargs)。現在我們來編寫 run_cdoe 函數:

test.py

import subprocess

def run_cdoe(code):
    output = subprocess.check_output(['python','-c',code])
    return output

code = """print('Test success')"""
print(run_cdoe(code))

現在我們來看看輸出,看看是不是我們想要的輸出:

輸出

輸出的是字節串。但是我們期望的是字符串。我們有兩種辦法,第一種是直接手動轉換結果,將 output 轉換為 string,但是這會有個問題。你要是直接解碼,會出現一個問題,如果你得到的結果是來自你的 shell ,那輸出結果的編碼就是 shell 的編碼,每個系統的 shell 編碼是不同的,難道需要我們為每個 shell 編寫解碼代碼嗎?所以這個看起來可行的方法是沒有普適性的。所以我們就只能采用第二種方法了,第二個方法很簡單,那就是加上 universal_newlines=True 參數,加上這個參數之后,subprocess 會自動為我們將輸出解碼為字符串。它具體是怎么實現的,大家可以去文檔看介紹。現在正確執行代碼給出正確輸出結果的功能解決了。
期望輸出

現在解決第 2 個需求。輸出錯誤。
在 subprocess 中,有個參數是 stderr ,大家看意思就已經明白它是干什么的了,是用來控制錯誤輸出流的。默認的錯誤輸出是輸出到主進程的,也就是調用這個進程的進程。讓我們來故意引發一個錯誤,看看具體是怎么回事:

錯誤輸出

大家可以看到,子進程的錯誤輸出也在主進程的錯誤輸出里。
我們希望錯誤輸出也能輸出到 output 上,output 本來是子進程的標準輸出,所以現在我們需要捕捉子進程的錯誤輸出流導。怎么做呢,那就是讓 stderr=subprocess.STDOUT,大家就會看到這個效果:
錯誤輸出流重定向

子進程的報錯已經看不到了,因為錯誤輸出流已經被重定向到了子進程。但是我們看,主進程依然報錯了。這是 sbuprocess 的機制,在子進程沒有執行成功時,就會引發 subprocess.CalledProcessError ,這個錯誤的 output 屬性包含了子進程的錯誤輸出。所以我們這樣來編寫 run_code

test.py

import subprocess

def run_cdoe(code):
    try:
        output = subprocess.check_output(['python','-c',code],universal_newlines=True,stderr=subprocess.STDOUT)
    except subprocess.CalledProcessError as e:
        output = e.output
    return output

我們來看看效果:

最終效果

現在還剩下第 3 個需求,控制客戶端代碼執行時間。同樣的我們還是依靠給 subprocess 傳遞參數來實現控制,這個參數就是 timeout ,它的單位是秒,所以我們希望在 30 秒之后還沒執行結束就中斷執行。在 subprocess 中,超時引發的錯誤是 subprocess.TimeoutExpired,它的 output 參數也包含了子進程的錯誤輸出。所以把 run_code 改成這樣:

test.py

import subprocess

def run_cdoe(code):
    try:
        output = subprocess.check_output(['python','-c',code],
                universal_newlines=True,
                stderr=subprocess.STDOUT,
                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

讓我們來看看測試:

最終測試

效果不錯!大功告成!
現在把我們的 run_code 函數復制到 online_app.py 里,記得也要導入 subprocess 庫。
最終你的 online_app.py 會是這樣的:

online_app.py

from django.conf import settings
from django.http import HttpResponse, JsonResponse# JsonResponse 用于返回 JSON 數據
from django.conf.urls import url
from django.views.decorators.http import require_POST
import subprocess
setting = {
    'DEBUG':True,
    'ROOT_URLCONF':__name__,
}

settings.configure(**setting)

# 主視圖
def home(request):
    with open('index.html','rb') as f:
        html = f.read()
    return HttpResponse(html)
# 執行客戶端代碼核心函數
def run_code(code):
    try:
        output = subprocess.check_output(['python','-c',code],
                universal_newlines=True,
                stderr=subprocess.STDOUT,
                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
# API 請求視圖
@require_POST
def api(request):
    code = request.POST.get('code')
    output = run_code(code)
    return JsonResponse(data={'output':output})
# URL 配置
urlpatterns = [url('^$',home,name='home'),
               url('^api/$',api,name='api')]

if __name__ == '__main__':
    import sys
    from django.core.management import execute_from_command_line
    execute_from_command_line(sys.argv)

我們的“人肉測試模塊”已經完成它的任務,現在可以刪掉了。現在我們完成了前后端的功能開發,讓我們來試試吧!在根路徑運行 python online_python.py runserver,訪問 http://127.0.0.1:8000 。試試往里面輸入代碼,看看能不能得到想要的結果。然后你會發現輸出框什么變化也沒有!打開控制臺看看,你會發現這個情況:

又出問題了!

請求被禁止了,你可以在 django 的控制臺kan原來是跨域請求錯誤。跨域請求 django 是怎么處理的呢?寫過模板表單的同學都知道,是通過給表單加 {% csrf_tokne %} 來實現的。那現在我們已經是 REST 架構了,已經不需要它了,所以我們就選擇禁用 csrf 功能。修改 onlime_app.py 如下:

現在頂部引入:
online_app.py

from django.views.decorators.csrf import csrf_exempt

把我們的 api 視圖修改為:
online_app.py

@csrf_exempt
@require_POST
def api(request):
    code = request.POST.get('code')
    output = run_code(code)
    return JsonResponse(data={'output':output})

現在趕緊運行 python online_app.py runserver ,訪問 http://127.0.0.1:8000,寫幾行代碼試試,運行一下。

最終效果

恭喜你完成了第一個 REST APP!

下一章做什么?

在本章,我們知道了單個文件的 django 也可以運行,通過單文件的 django 我們大致了解了 django 初始化運行流程是什么,同時我們簡單的了解了 REST 的概念,并構建了一個簡單的 APP 。在下一章,我們將會深入 REST ,我們將會制作一個符合 REST 標準的 APP ,以此來熟悉 REST 標準,同時了解 REST 最核心的概念————一切皆資源。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容