可不要被名字迷惑,它可不是web網頁的框架,而是服務器用來產生web網頁時用到的工具。
Web應用框架(簡稱Web框架),是用來構建web支持下的應用程序的實踐方式。從簡單的博客到復雜的富Ajax應用,web上的每個頁面都是通過代碼構建起來的。最近我發現很多對web框架(如Flask、Django)感興趣的開發者沒有真正地理解什么是web框架——它們的目的是什么、它們怎么運行。因此,我將在本文中討論web框架這個經常被忽略的基礎話題。通讀本文,你會對什么web框架、為什么它們一開始就存在等問題有深入的理解,這也會讓你學習一個新的web框架以及使用哪個框架時的決定大為輕松。
Web如何工作
在我們討論具體的框架以前,需要理解web如何工作,為此我們將深入探討當你在瀏覽器中輸入URL地址并按下回車鍵時到你的瀏覽器在呈現頁面的過程中經過的步驟(不包括DNS查表)。
Web服務器及web提供的服務
每個頁面都以HTML
文件發送到你的瀏覽器中,HTML
是一種瀏覽器用來描述內容和頁面結構的一種語言,把HTML
發送到你的瀏覽器中的應用程序就是Web服務器。同時,這個應用程序所在的機器也叫做Web服務器。
HTTP
瀏覽器使用HTTP
協議(協議,在編程領域中是通信雙方約定的數據格式和通信步驟)從Web服務器(或叫應用程序服務器)中下載頁面,HTTP
協議基于請求—響應
模型,客戶端(你的瀏覽器)向在運行在一臺物理機器上的網頁應用程序請求數據,web應用程序接著就用你瀏覽器請求的數據來響應這個請求。
有一點需要記住的是,通信總是由客戶端(你的瀏覽器)發起的,服務器(這里是web服務器)沒有任何方式發起連接或者主動給你的瀏覽器發送未請求的信息,如果你從一個網頁服務器中收到了數據,那一定是因為你的瀏覽器發出了請求。
HTTP方法
在HTTP
協議中的每一個信息都有相關的方法(或動作),各不相同的HTTP
方法對應于客戶端根據不同的需要而發出的不同邏輯的請求,比如請求一個網頁的HTML就和提交一個表單在邏輯上不一樣,所以處理這兩種的請求就需要不同的方法。
HTTP GET
GET
方法做的事就跟它說的一樣:從網頁服務器要求(請求)數據,GET
請求是目前最常見的HTTP
請求,在一個GET
請求過程中,網頁應用程序除了將所需要頁面的HTML響應給這個請求外不做任何其他事情。這里特意指出,在處理GET
請求過程中網頁應用程序不應改變自身任何狀態(比如,它不能基于一個GET
請求就創建一個新的用戶帳戶),因為這個原因,GET
請求通常被認為是“安全”的,因為它們不會導致驅動網站的應用程序的任何變化。
HTTP POST
顯然,除了單純地看看頁面意外還有更多與網站交互的方式,我們也可以向web應用程序發送數據,比如說一個表單,要完成這個工作,需要另一個不同的請求:POST
。POST
請求通常會攜帶用戶輸入的數據,繼而會引起web應用采取一些行為。在網站的表單上輸入你的信息來注冊就是通過POST
請求把表單上的數據傳遞給web應用的。
與GET
請求不同,POST
請求通常會導致web應用的狀態改變,在上面的例子中,當一個表單被POST
以后,就創建了一個新的用戶帳戶,其次,POST
請求也不總是會讓一個新的HTML頁面發送給客戶端,客戶端通過響應的響應碼來決定服務器上的操作是否進行順利。
HTTP 響應碼
在最常見的情況中,web服務器會返回一個響應碼200,意思是“我做了你要求做的,并且一切進行順利”,響應碼總是一個三位的數字。web應用必須為每個響應發送一個響應碼來表明對一個請求的處理情況。200
意為“OK”,并且是響應一個GET
請求最常用到的,而一個POST
請求則有可能返回響應碼204
(“沒有內容”),意思是“一切事情都干得很順利,但我沒啥可以呈現給你看的”。
值得注意的是,POST
請求還會被發送到一個與提交表單的網頁不同的URL,用上面那個注冊用戶的例子來說,就是表單所在的網址是www.foo.com/signup
,你在這個頁面點擊了提交
,而這個POST
請求會發送到www.foo.com/process_signup
,POST
請求被發送到的地址對于表單的HTML
是特定的。
Web應用程序
只要用到HTTP
GET
和POST
,你就能夠做很多事情,因為它們是最常用的HTTP
方法。web應用就是用來接受HTTP
請求并且用通常包含HTML表示的請求頁面的HTTP
響應進行回復。POST
請求引起web應用產生一些行為,如在數據庫里添加一條新的記錄。還有很多其他HTTP
方法,但目前我們僅聚焦于GET
和POST
方法。
最簡單的web應用長什么樣?我們可以寫一個監聽80
端口連接的應用,一旦它監聽到了一個連接,它會等待客戶端發送一個請求,然后它會用非常簡單的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 200
的響應(它不是一個真正意義上的web服務器),Content-type: text/html
這一行代表header區域,header用來提供請求或者響應的元信息,在這個例子中,我們告訴客戶端,發送過去的數據是HTML(而不是JSON)
對一個請求的分析
仔細觀察我用來測試上述程序的HTTP
請求,我發現它和響應很相似,第一行的格式是
<HTTP Method> <URL> <HTTP version>
在本例中,是GET / HTTP/1.1
,在第一行接下來是諸如Accept: */*
(表示我們接收任何響應里面的內容)的頭部,這是一個請求的基本情況。
我們發送的響應有著類似格式,如:
<HTTP version> <HTTP Status-Code> <Status-Code Reason-Phrase>
在本例中,是HTTP/1.1 200 OK
,接下來headers,格式跟請求的headers一樣,最后包含了響應的實際內容。注意這些都可以用一個字符串或者二進制對象進行編碼,Content-type
header讓客戶端知道如何解釋響應。
web服務器的fatigue
如果我們接著上述的例子繼續講解web應用,隨之而來有很多問題需要解決:
- 我們要怎么檢測所需要的URL并且返回合適的網頁?
- 除了簡單的
GET
請求以外,我們如何處理POST
請求? - 怎么處理一些像sessions和cookies等更高級的概念?
- 如何描述能夠處理數以千計并發連接的應用?
如你能想象,沒有人愿意每次構建服務器時都要逐一對付這些問題,因此就有了能夠處理HTTP
協議細節和統一解決上述問題的包。然而要記住,它們的核心就跟我們上述提到的例子一樣:監聽請求并且發送帶有HTML的HTTP
響應。
注意客戶端 web 框架(如前端當前流行的三大框架:React、Vue以及Angular)是另一個不同的龐然大物,與我們上述講到的大不相同。
解決兩個主要問題:路由與模版
在構建一個web應用涉及到的一切問題中,有兩個是重中之重:
- 如何將一個被請求的URL定位到用于處理它的代碼?
- 如何動態創建被請求的HTML,在其中加入從數據庫讀取的計算值或信息?
每個web框架都用某些方式來解決這些問題,并且有很多不同的方法。接下來我討論了Django和Flask用來解決這些問題的方式,首先我們要簡要討論MVC架構。
Django中的MVC
Django遵從MVC架構并且要求使用該框架的代碼也使用該架構,MVC即“模型—視圖—控制 (Model-View-Controller)”的縮寫,用來表示web應用需要負責的不同方面。與數據庫有關的資源是用模型來表示(類似的,Python中常用class
來表示一些真實世界中的對象),控制包含應用的業務邏輯和對模型的操作,視圖接受所有用來動態生成HTML頁面所需要的信息。
令初學者疑惑的是,在Django中,MVC架構中的控制叫做視圖,視圖叫做模版,除去命名上的古怪,Django是非常典型的MVC架構的部署方式。
Django中的路由
路由是將被請求的URL定位到負責生成相關HTML的代碼的過程,最簡單的例子是所有請求都用同一個代碼進行處理(如我們之前所舉的例子),稍微復雜一點,每一個URL按照1:1地對應到視圖函數
中,比如,我們可以在某個代碼來實現如下功能,如果請求URLwww.foo.com/bar
,就讓handle_bar()
函數來負責處理進行響應,以此類推,我們可以為所有web應用支持的URLs建立對應的處理函數。
但是,如果URLs中包含了有用的數據,比如某個資源的ID(按上面的例子來說,如果URL是www.foo.com/users/3/
),這種路由方法就會失敗,那么我們怎么樣將URL對應到一個視圖函數的同時呈現ID為3
的用戶頁面呢?
Django的處理方式是用URL正則表達式定位到能夠接受參數的視圖函數,舉例來說,我可以說符合^/users/(?P<id>\d+)/$
格式的URLs會調用display_user(id)
函數,函數中的id
變量會用正則表達式中的id
進行替換,通過這種處理,任何/users/<some number>/
格式的URL會定位到display_user
函數中,這些正則表達式可以寫得非常復雜,并且同時包含鍵盤和位置參數。
Flask中的路由
Flask則用了不太一樣的處理方式,它通過使用route()
修飾器將一個被請求的URL和函數連接起來。下面的Flask代碼跟上面提到的正則表達式與函數的功能相同:
@app.route('/users/<id:int>/')
def display_user(id):
# ...
如你所見,修飾器簡化了將URLs對應到處理函數的正則表達式寫法中(用/
來分離),參數通過包括一個傳遞到route()
的URL中的<name:type>
命令被獲取,路由到像/info/about_us.html
之類的靜態URLs的方式也不難想到:
@app.route('/info/about_us.html')
通過模版生成HTML
繼續上面的例子,我們一旦將合適的代碼定位到正確的URL以后,我們怎樣動態生成支持web開發者修改的HTML呢?Django和Flask的處理方式都是通過HTML 模版
HTML 模版類似于使用str.format()
,先通過占位符來寫需要的輸出,后面可以被替換成傳入str.format()
函數的變量,想象一下將整個網頁寫成一個字符串,用括號標記動態數據,最后調用str.format()
,Django模版和Flask使用的模版引擎jinja2都是這么工作的。
然而,不是所有的模版引擎都進行相同的創建,Django對于模版編程提供了基本支持,而Jinja2基本上讓你自由發揮(不一定準確,但基本上是這個意思)。Jinja2會緩存繪制模版的結果,這樣接下來如果有相同變量的請求則會直接從緩存中返回,而不是重新繪制。
服務器交互
Django由于其“一條龍服務 batteries included”哲學,還包含了一個ORM
(object relational mapper, 對象關系映射器),ORM
的目的有兩個:將Python類定位到數據庫表、把各種不同數據庫引擎的差別抽象掉(前者是其最基本的功能)。人們都不太喜歡ORMs(由于這種定位從來做不到完善),但是還是可以接受的。
Django是功能齊備,相比之下,Flask作為一個“微框架”則沒有ORM(與Django最大也是唯一的競爭者SQLAlchemy相同)
由于囊括了一個ORM
,Django得以創建全功能CRUD
應用,CRUD
(Create Read Update Delete 增刪改查)應用似乎是web框架的有效切入點(從服務器端來看)。Django(以及Flask-SQLAlchemy)為每個模型創建了很多不同的CRUD
操作。
Web框架總結
講到這里,web框架的目的應該已經明確了:作為HTTP
請求響應和相應底層代碼之間的接口,即把底層代碼隱藏起來,藏到什么程度就看不同的框架如何處理了。Django和Flask代表了兩種極端,Django幾乎涉及到了每種情況,這都快成為它的包袱了,Flask將自己定位為一個“微框架”,它僅保留了web框架最核心的規模,而依賴于第三方包來處理那些web框架不太常用的任務。
記住,所有Python web框架工作的本質都是一樣的:它們接收HTTP
請求,將之分配到生成HTML的代碼中,并且用相應內容生成HTTP
響應,實際上,所有主流服務器端的框架都這樣進行工作(包括Nodejs 的框架)。通過上文了解了它們的目的后,希望你現在對web框架進行選擇時能心里有數。
原文地址:https://jeffknupp.com/blog/2014/03/03/what-is-a-web-framework/