Web 應用框架,簡稱爲 web
框架
,是編寫 web 應用程序的基石。不管簡單的博客系統,還是 Ajax 爲主的應用,網絡上所有的頁面都是代碼構成的。進來我發現,很多想學習諸如 Flask 或者 Django 等 web 框架的開發者,並不很瞭解 web 框架是什麼,它們的作用和工作原理。這篇文章,我將會講一下這個通常會被忽略的話題。希望讀完這篇文章,你能比較深刻地理解 web 框架到底是什麼,還有爲什麼會有 web 框架。這些知識將有利於你學習新的 web 框架,而且在選擇 web 框架的時候有法可依。
WEB 是什麼工作的?
在討論框架之前,我們要先了解一下網頁是怎麼工作。我們就從你在瀏覽器輸入一個網址,摁下 enter 鍵說起。打開你的瀏覽器,輸入 http://jeffknupp.com
(譯者注:原作者的個人網站首頁),我們來看看你的瀏覽器做了那些事情(DNS
查詢的 buffer 就略過),才能顯示你看到的網頁。
web servers,和 web … servers …
瀏覽器接接收到的網頁都是 HTML
文件,HTML
是一種描述網頁內容和結構的語言。負責給瀏覽器發送 HTML
的程序稱爲
web server,容易混淆的是,這個應用程序所在的機器通常也被稱爲 web server。
最重要的一點是,所有的 web 應用做的事情就是把 HTML
內容發送給瀏覽器。不論這個
web 應用有多麼複雜,最終的任務都是把 HTML
(我故意忽略掉其他格式的內容,比如
JSON,CSS 文件,因爲原理都是一樣的)發送給瀏覽器。
問題來了:web 應用如何知道要發送什麼內容給瀏覽器呢?答案:它會發送瀏覽器請求的內容。
HTTP
瀏覽器從 web server 下載內容所用的是 HTTP
協議(協議在計算機科學中,指的是雙方通信所共同遵循的數據格式和通信步驟)。HTTP
協議的基礎是 請求-應答
(request-response
)
模型。客戶端(你的瀏覽器)請求
某臺物理機上
web 應用的數據,web server 則負責 應答
請求的數據。
有個重要的事情是:所有的通信都是客戶端(你的瀏覽器)發起的。服務端(web server)是不可能主動連接你,發送沒有請求的數據的。如果你收到了數據,只是因爲你的瀏覽器主動請求了這些數據。
HTTP 方法
HTTP
協議的每條消息都有對應的方法(method),不同的方法對應了客戶端能發起的不同請求,也對應了客戶端不同的意圖。比如,請求 網頁的
HTML 和提交一個表格在邏輯上是不同的,所以這兩種方法需要兩種不同的方法。
HTTP GET
顧名思義,GET
方法就是從
web server 獲取(get)數據,GET
請求也是目前最常用的 HTTP
請求。
處理 GET
請求的過程中,web
應用只需要返回請求的數據,無需其他操作。尤其是,不應該修改應用的狀態(比如, GET
請求不應該導致一個新用戶被創建)。因爲這個原因,GET
請求通常被看做是 安全
的。
HTTP POST
和網站的交互,明顯不只是查看網頁的。我們還會通過表格等形式發送數據給 web 應用,這些操作需要用到另外一種請求:POST
。POST
請求通常會傳遞用戶創建的信息,導致
web 應用執行某些動作。輸入自己的信息,來註冊某個網站就會用到 POST
請求,請求中會包含你輸入的數據。
和 GET
請求不同的是, POST
請求通常會導致
web 應用狀態的改變。上面提及的例子中,表單被提交後,一個新的用戶會被創建。還有一點不同,POST
請求的結果可能不會返回
HTML 數據給客戶端,客戶端需要通過 response
code
來判斷操作是否成功。
HTTP response code
正常情況下,web server 會返回 200 的 response code,意思是:我已經完成了你要我做的事情,並且一切都沒有問題。response
code
是三位的數字,每次應答都要包含一個 response code,來標識請求的結果。200
表示
OK,是 GET
方法常見的返回值。POST
請求經常會返還 204
(No
contnet),表示:一切正常,但是我沒有數據可以展示給你。
還需要注意的是:POST
請求發送給的
url,可能和數據發送出去的 url 不同。繼續以我們的註冊頁面爲例,註冊表可能位於 http://foo.com/signup
,點擊 submit
之後,包含着註冊數據的 POST
請求可能被髮送到 http://foo.com/process_signup
。POST
請求要發送到的地址,一般在註冊表格的 HTML
源碼裏指定。
Web 應用
掌握 GET
和 POST
方法就能做很多事情,因爲它們是
web 上最常用的兩個方法。總結一下,web 應用就是接收 HTTP
請求,然後返回 HTTP
應答,一般是包含請求數據的 HTML
。POST
方法會導致
web 應用執行某些動作,例如在數據庫添加一條記錄。當然還有其他的 HTTP
方法,但目前我們只需要關心 GET
和 POST
就足夠啦。
最簡單的 web 應用長什麼樣呢?我們就來寫一個監聽在 80
端口的
web 應用,一旦和客戶端建立連接,就等待客戶端發起請求,並返回非常簡單的 HTML
。
這個程序是這樣的:
import socket
HOST = ''
PORT = 80
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.bind((HOST, PORT))
listen_socket.listen(1)
connection, address = listen_socket.accept()
request = connection.recv(1024)
connection.sendall("""HTTP/1.1 200 OK
Content-type: text/html
<html>
<body>
<h1>Hello, World!</h1>
</body>
</html>""")
connection.close()
(如果上面的程序報端口錯誤,可以把 PORT
的值修改成其他值,比如 8080
。)
上面的代碼只會接收一個連接和一個請求,不管請求的 URL 是什麼,都會返回同樣的 HTTP
內容,response
code 是 200
。(很明顯,這不算真正的
web server)。在這個例子,我們告訴客戶端,返回的數據格式爲 HTML
,而不是其他的格式,比如 JSON
。
request 請求解析
如果看一下上面例子中發送的 HTTP
請求(譯者注:可以使用
chrome 的 inspect elements -> Network,或者抓包工具 tcpdump 等工具查看發送的 HTTP 請求),就會發現它和應答很相似。請求的第一行是:
<HTTP Method> <URL> <HTTP version>
在這個例子裏就是 GET /
HTTP/1.1
。第一行後面跟着的是請求的頭部(headers),比如 Accept:
*/*
(表示可以接收任何格式的內容作爲應答)。基本上就這麼多。
我們發送的應答也是類似的格式,第一行是:
<HTTP version> <HTTP Status-Code> <Status-Code Reason-Phrase>
這個例子中就是 HTTP/1.1
200 OK
。然後是頭部,和請求的頭部一樣。最後是應答的實際內容。注意:應答也可能是字符串或者二進制對象,頭部的 Content-typ
就是來標識應答內容,讓客戶端來解析的。
web server 更多瑣碎的細節
要在上面的例子基礎上繼續擴展的話,還有很多需要我們來解決的問題:
- 怎麼查看請求的 URL,然後返回不同的頁面?
- 除了
GET
請求,怎麼處理POST
請求? - 怎麼處理很複雜的概念,比如 sessions 和 cookies?
- 如何擴展這個應用,讓它可以同時處理數千條連接?
可以想象,沒人會願意每次編寫 web 應用的時候都要自己處理這些問題。爲了解決這個難題,就會存在很多的軟件包幫你處理這些煩人的細節,讓開發者可以把心思放到業務邏輯上。記住,不管 web 框架多麼負責,其最核心的功能和我們上面的例子是一樣的:監聽客戶端請求,然後返回 HTML
給客戶端。
NOTE:客戶端的框架和上面的內容迥然不同。
解決兩個難題:路由(routing)和模板(templates)
在構建 web 應用的所有的問題中,有兩個比較突出:
- 怎麼把請求 URL 和處理它的那部分代碼對應起來?
- 怎麼動態地生產請求內容?包括所有要計算的值,和從數據庫獲取的信息?
每個 web 框架解決這兩個問題的方法都不太相同,我們就舉 Flask 和 Django 的例子來說明這個問題。首先,我們還要來說一下 MVC
模式。
Django 中的 MVC
Django 採用 MVC 模式, 所以要求使用這個框架的代碼都遵循這個模式。MVC,是 Model-View-Controller 的縮寫,用來分離應用的不同責任。數據庫表所代表的資源用 models 來表示,controllers 負責應用的業務邏輯和操作 models。Views 則負責動態生成代表頁面的 HTML
。
不過容易讓人混淆的是,django 中 controllers 被稱作 views,而 views 被稱爲 templates。除了命名外,django 算是比較直接的 MVC 架構。
Django 的路由(routing)機制
這裏說的路由(routing)就是把請求的 URL 對應到處理生成相關 HTML
的代碼。最簡單的例子,所有的請求都是相同的代碼處理(就是我們之前編寫的代碼)。複雜一點呢,每個的
URL 都對應一個不同的 view
function
。比如,有個地方的邏輯是接收到 www.foo.com/bar
請求,就把它交給 handle_bar()
函數處理。我們可以這樣依次編寫出所有
url 對應的處理函數。
不過,這個方法有個致命傷:沒有辦法處理帶有動態數據的 URL,比如說資源的 ID(例如 http://www.foo.com/users/3
)。我們怎麼把這個
URL 映射到函數,同時能傳過去用戶 ID 信息呢?
Django 採用的方法是利用正則表達式:用正則表達式匹配 URL,然後把匹配的數據作爲參數傳遞給處理函數。比如,我可以說匹配 ^/users/(?P<id>\d+)/$
的
URL 會調用 display_user(id)
函數,其中 id
就是正則表達式括號裏匹配的內容。利用這種方式,任何 /users/<some
number>
類型的 URL 都能對應到 display_user
函數,並且正則表達式可以無限複雜,包含任意的關鍵字和未知參數。
Flask 的 路由機制
Flask 採用的是另外一種方法。把 url 對應到函數參照的是 route()
裝飾器。下面的
Flask 代碼和上面提到的正則表達式代碼功能相同:
@app.route('/users/<id:int>/')
def display_user(id):
# ...
如你所見,裝飾器使用的是簡化版的正則表達式來傳遞參數,參數被 route 參數中 <name:type>
的指令捕獲。要路由 /info/about.html
這樣的頁面,就需要 @app.route('/info/about_us.html')
。
根據模板生成 HTML
繼續上面的例子,一旦我們知道怎麼把 URL 對應到邏輯代碼,那麼要怎麼動態地生成 HTML,並且方便開發者手動編輯呢?Django 和 Flask 兩者這次方法一樣,那就是 —— HTML 模板。
HTML 模板
有點像 string.format()
:預期的輸出首先要用站位標識,然後再填入動態的數據。可以把這個網頁想象成一個字符串,裏面用括號標識動態的數據,最後調用 str.format()
生成最終的結果。Django
的 模板引擎和 Flask 採用的 jinja2 都是這個原理。
不過,並不是所有的模板引擎地位都一樣。Django 的模板只支持簡單的變成,而 Jinja2 卻能讓你執行任意的代碼(當然併發完全可以,不過已經很近似)。Jinja2 很會 cache 渲染的結果,下次有同樣的參數傳過來的時候,就會直接從 cache 獲取結果,而不需要重新渲染。
數據庫集成
Django,宣稱“自帶電池”(batteries included),然後也會包含 ORM
(Object
Relational Mapper)。ORM
的目的有兩個:把
python 的類映射到數據的表結構,和通過封裝隱藏不同數據庫之間的差異(第一點是它更主要的功能)。沒有人喜歡 ORM
(因爲不同域之間的
mapping 從不完美),不過這些缺點都是可以接受的。 Django 功能比較全面,Flask 作爲一個微框架,並不自帶 ORM
(不過它很好兼容
SQLAlchemy,Django ORM 最大的競爭者)。
因爲包含 ORM,Django 能夠創建功能齊全的 CRUD 應用。 CRUD(Create Read Update Delete)是 web 框架(服務器端)最美好的地方,Django 和 Flask-SQLAlchemy 使得 CRUD 操作很直接。
Web 框架總結
寫到這,web 框架出現的目的也比較明確了:隱藏基礎而又煩人的處理 HTTP 請求和應答的代碼。至於要隱藏多少內容,就要看框架啦。Django 和 Flask 代表了兩個極端。Django 每種情況都有涉及,而 Flask 標榜自己是“微框架“,只處理 web 程序最核心的功能,依賴其他三方插件來完成其他不常用的工作。
寫了這麼多,記住,所有的 python web 框架功能方式都一樣:它們接收 HTTP
請求,然後分發任務,並生成
HTML,然後返回包含 HTML 的 HTTP 應答。事實上,所有的 server 端框架(除了 Javascript 框架)都是這麼工作的。希望,看完這篇文章,你已經知道 web 框架的目的,也知道怎麼去選擇 web 框架啦。
翻譯:
https://jeffknupp.com/blog/2014/03/03/what-is-a-web-framework/
參考:
http://cizixs.com/2015/09/21/what-is-a-web-framework