什么是 Web 框架?

可不要被名字迷惑,它可不是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_signupPOST請求被發送到的地址對于表單的HTML是特定的。

Web應用程序

只要用到HTTP GETPOST,你就能夠做很多事情,因為它們是最常用的HTTP方法。web應用就是用來接受HTTP請求并且用通常包含HTML表示的請求頁面的HTTP響應進行回復。POST請求引起web應用產生一些行為,如在數據庫里添加一條新的記錄。還有很多其他HTTP方法,但目前我們僅聚焦于GETPOST方法。

最簡單的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-typeheader讓客戶端知道如何解釋響應。

web服務器的fatigue

如果我們接著上述的例子繼續講解web應用,隨之而來有很多問題需要解決:

  1. 我們要怎么檢測所需要的URL并且返回合適的網頁?
  2. 除了簡單的GET請求以外,我們如何處理POST請求?
  3. 怎么處理一些像sessions和cookies等更高級的概念?
  4. 如何描述能夠處理數以千計并發連接的應用?

如你能想象,沒有人愿意每次構建服務器時都要逐一對付這些問題,因此就有了能夠處理HTTP協議細節和統一解決上述問題的包。然而要記住,它們的核心就跟我們上述提到的例子一樣:監聽請求并且發送帶有HTML的HTTP響應。

注意客戶端 web 框架(如前端當前流行的三大框架:React、Vue以及Angular)是另一個不同的龐然大物,與我們上述講到的大不相同。

解決兩個主要問題:路由與模版

在構建一個web應用涉及到的一切問題中,有兩個是重中之重:

  1. 如何將一個被請求的URL定位到用于處理它的代碼?
  2. 如何動態創建被請求的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/

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

推薦閱讀更多精彩內容