一個Flask應用執行過程鳥瞰
相信很多初學Flask的同學(包括我自己),在閱讀官方文件或者Flask的學習資料時,對於它的認識是從以下的一段程式碼開始的:
from
flask
import
Flask
app
=
Flask
(
__name__
)
@app。route
(
‘/’
)
def
index
():
return
“Hello World!”
if
__name__
==
‘__main__’
:
app
。
run
()
執行如上程式碼,在瀏覽器中訪問http://localhost:5000/,便可以看到Hello World!出現了。這是一個很簡單的Flask的應用。
然而,這段程式碼怎麼執行起來的呢?一個Flask應用運轉的背後又有哪些邏輯呢?如果你只關心Web應用,那對這些問題不關注也可以,但從整個Web程式設計的角度來看,這些問題非常有意義。本文就主要針對一個Flask應用的執行過程進行簡要分析,後續文章還會對Flask框架的一些具體問題進行分析。
為了分析方便,本文采用
Flask 0.1版本
的原始碼進行相關問題的探索。
一些準備知識
在正式分析Flask之前,有一些準備知識需要先了解一下:
使用Flask框架開發的屬於Web應用。由於Python使用WSGI閘道器,所以這個應用也可以叫WSGI應用;
伺服器、Web應用的設計應該遵循閘道器介面的一些規範。對於WSGI閘道器,要求Web應用實現一個函式或者一個可呼叫物件webapp(environ, start_response)。伺服器或閘道器中要定義start_response函式並且呼叫Web應用。關於這部分的內容可以參考:wsgiref包——符合WSGI標準的Web服務實現(一)。
Flask依賴於底層庫werkzeug。相關內容可以參考:- Werkzeug庫簡介。
本文暫時不對伺服器或閘道器的具體內容進行介紹,只需對伺服器、閘道器、Web應用之間有怎樣的關係,以及它們之間如何呼叫有一個瞭解即可。
一個Flask應用執行的過程
1。 例項化一個Flask應用
使用app = Flask(__name__),可以例項化一個Flask應用。例項化的Flask應用有一些要點或特性需要注意一下:
對於請求和響應的處理,Flask使用werkzeug庫中的Request類和Response類。對於這兩個類的相關內容可以參考:Werkzeug庫——wrappers模組。
對於URL模式的處理,Flask應用使用werkzeug庫中的Map類和Rule類,每一個URL模式對應一個Rule例項,這些Rule例項最終會作為引數傳遞給Map類構造包含所有URL模式的一個“地圖”。這個地圖可以用來匹配請求中的URL資訊,關於Map類和Rule類的相關知識可以參考:Werkzeug庫——routing模組。
當例項化一個Flask應用app(這個應用的名字可以隨便定義)之後,對於如何新增URL模式,Flask採取了一種更加優雅的模式,對於這點可以和Django的做法進行比較。Flask採取裝飾器的方法,將URL規則和檢視函式結合在一起寫,其中主要的函式是route。在上面例子中:
@app。route
(
‘/’
)
def
index
():
pass
這樣寫檢視函式,會將‘/’這條URL規則和檢視函式index()聯絡起來,並且會形成一個Rule例項,再新增進Map例項中去。當訪問‘/’時,會執行index()。關於Flask匹配URL的內容,可以參考後續文章。
例項化Flask應用時,會創造一個Jinja環境,這是Flask自帶的一種模板引擎。可以檢視Jinja文件,這裡先暫時不做相關介紹。
例項化的Flask應用是一個可呼叫物件。在前面講到,Web應用要遵循WSGI規範,就要實現一個函式或者一個可呼叫物件webapp(environ, start_response),以方便伺服器或閘道器呼叫。Flask應用透過__call__(environ, start_response)方法可以讓它被伺服器或閘道器呼叫。
def
__call__
(
self
,
environ
,
start_response
):
“”“Shortcut for :attr:`wsgi_app`”“”
return
self
。
wsgi_app
(
environ
,
start_response
)
注意到呼叫該方法會執行wsgi_app(environ, start_response)方法,之所以這樣設計是為了在應用正式處理請求之前,可以載入一些“中介軟體”,以此改變Flask應用的相關特性。對於這一點後續會詳細分析。
Flask應用還有一些其他的屬性或方法,用於整個請求和響應過程。
2。呼叫Flask應用時會發生什麼
上面部分分析了例項化的Flask應用長什麼樣子。當一個完整的Flask應用例項化後,可以透過呼叫app。run()方法執行這個應用。
Flask應用的run()方法會呼叫werkzeug。serving模組中的run_simple方法。這個方法會建立一個本地的測試伺服器,並且在這個伺服器中執行Flask應用。關於伺服器的建立這裡不做說明,可以檢視werkzeug。serving模組的有關文件。
當伺服器開始呼叫Flask應用後,便會觸發Flask應用的__call__(environ, start_response)方法。其中environ由伺服器產生,start_response在伺服器中定義。
上面我們分析到當Flask應用被呼叫時會執行wsgi_app(environ, start_response)方法。可以看出,wsgi_app是真正被呼叫的WSGI應用,之所以這樣設計,就是為了在應用正式處理請求之前,wsgi_app可以被一些“中介軟體”裝飾,以便先行處理一些操作。為了便於理解,這裡先舉兩個例子進行說明。
例子一:
中介軟體SharedDataMiddleware
中介軟體SharedDataMiddleware是werkzeug。wsgi模組中的一個類。該類可以為Web應用提供靜態內容的支援。例如:
import
os
from
werkzeug。wsgi
import
SharedDataMiddleware
app
=
SharedDataMiddleware
(
app
,
{
‘/shared’
:
os
。
path
。
join
(
os
。
path
。
dirname
(
__file__
),
‘shared’
)
})
Flask應用透過以上的程式碼,app便會成為一個SharedDataMiddleware例項,之後便可以在http://example。com/shared/中訪問shared資料夾下的內容。
對於中介軟體SharedDataMiddleware,Flask應用在初始例項化的時候便有所應用。其中有這樣一段程式碼:
self。wsgi_app = SharedDataMiddleware(self。wsgi_app, {
self。static_path: target
})
這段程式碼顯然會將wsgi_app變成一個SharedDataMiddleware物件,這個物件為Flask應用提供一個靜態資料夾/static。這樣,當整個Flask應用被呼叫時,self。wsgi_app(environ, start_response)會執行。由於此時self。wsgi_app是一個SharedDataMiddleware物件,所以會先觸發SharedDataMiddleware物件的__call__(environ, start_response)方法。如果此時的請示是要訪問/static這個資料夾,SharedDataMiddleware物件會直接返回響應;如果不是,則才會呼叫Flask應用的wsgi_app(environ。start_response)方法繼續處理請求。
例子二:
中介軟體DispatcherMiddleware
中介軟體DispatcherMiddleware也是werkzeug。wsgi模組中的一個類。這個類可以講不同的應用“合併”起來。以下是一個使用中介軟體DispatcherMiddleware的例子。
from
flask
import
Flask
from
werkzeug
import
DispatcherMiddleware
app1
=
Flask
(
__name__
)
app2
=
Flask
(
__name__
)
app
=
Flask
(
__name__
)
@app1。route
(
‘/’
)
def
index
():
return
“This is app1!”
@app2。route
(
‘/’
)
def
index
():
return
“This is app2!”
@app。route
(
‘/’
)
def
index
():
return
“This is app!”
app
=
DispatcherMiddleware
(
app
,
{
‘/app1’
:
app1
,
‘/app2’
:
app2
})
if
__name__
==
‘__main__’
:
from
werkzeug。serving
import
run_simple
run_simple
(
‘localhost’
,
5000
,
app
)
在上面的例子中,我們首先建立了三個不同的Flask應用,併為每個應用建立了一個檢視函式。但是,我們使用了DispatcherMiddleware,將app1、app2和app合併起來。這樣,此時的app便成為一個DispatcherMiddleware物件。
當在伺服器中呼叫app時,由於它是一個DispatcherMiddleware物件,所以首先會觸發它的__call__(environ, start_response)方法。然後根據請求URL中的資訊來確定要呼叫哪個應用。例如:
如果訪問/,則會觸發app(environ, start_response)(
注意:
此時app是一個Flask物件),進而處理要訪問app的請求;
如果訪問/app1,則會觸發app1(environ, start_response),進而處理要訪問app1的請求。訪問/app2同理。
3。 和請求處理相關的上下文物件
當Flask應用真正處理請求時,wsgi_app(environ, start_response)被呼叫。這個函式是按照下面的方式執行的:
def
wsgi_app
(
environ
,
start_response
):
with
self
。
request_context
(
environ
):
。。。
請求上下文
可以看到,當Flask應用處理一個請求時,會構造一個上下文物件。所有的請求處理過程,都會在這個上下文物件中進行。這個上下文物件是_RequestContext類的例項。
class
_RequestContext
(
object
):
“”“The request context contains all request relevant information。 It is
created at the beginning of the request and pushed to the
`_request_ctx_stack` and removed at the end of it。 It will create the
URL adapter and request object for the WSGI environment provided。
”“”
def
__init__
(
self
,
app
,
environ
):
self
。
app
=
app
self
。
url_adapter
=
app
。
url_map
。
bind_to_environ
(
environ
)
self
。
request
=
app
。
request_class
(
environ
)
self
。
session
=
app
。
open_session
(
self
。
request
)
self
。
g
=
_RequestGlobals
()
self
。
flashes
=
None
def
__enter__
(
self
):
_request_ctx_stack
。
push
(
self
)
def
__exit__
(
self
,
exc_type
,
exc_value
,
tb
):
# do not pop the request stack if we are in debug mode and an
# exception happened。 This will allow the debugger to still
# access the request object in the interactive shell。
if
tb
is
None
or
not
self
。
app
。
debug
:
_request_ctx_stack
。
pop
()
根據_RequestContext上下文物件的定義,可以發現,在構造這個物件的時候添加了和Flask應用相關的一些屬性:
app ——上下文物件的app屬性是當前的Flask應用;
url_adapter ——上下文物件的url_adapter屬性是透過Flask應用中的Map例項構造成一個MapAdapter例項,主要功能是將請求中的URL和Map例項中的URL規則進行匹配;
request ——上下文物件的request屬性是透過Request類構造的例項,反映請求的資訊;
session ——上下文物件的session屬性儲存請求的會話資訊;
g ——上下文物件的g屬性可以儲存全域性的一些變數。
flashes ——訊息閃現的資訊。
LocalStack和一些“全域性變數”
注意:
當進入這個上下文物件時,會觸發_request_ctx_stack。push(self)。在這裡需要注意Flask中使用了werkzeug庫中定義的一種資料結構LocalStack。
_request_ctx_stack
=
LocalStack
()
關於LocalStack,可以參考:Werkzeug庫——local模組。LocalStack是一種棧結構,每當處理一個請求時,請求上下文物件_RequestContext會被放入這個棧結構中。資料在棧中儲存的形式表現成如下:
{
880
:
{
‘stack’
:
[
<
flask
。
_RequestContext
object
>
]},
13232
:
{
‘stack’
:
[
<
flask
。
_RequestContext
object
>
]}}
這是一個字典形式的結構,鍵代表當前執行緒/協程的標識數值,值代表當前執行緒/協程儲存的變數。werkzeug。local模組構造的這種結構,很容易實現執行緒/協程的分離。也正是這種特性,使得可以在Flask中訪問以下的“全域性變數”:
current_app
=
LocalProxy
(
lambda
:
_request_ctx_stack
。
top
。
app
)
request
=
LocalProxy
(
lambda
:
_request_ctx_stack
。
top
。
request
)
session
=
LocalProxy
(
lambda
:
_request_ctx_stack
。
top
。
session
)
g
=
LocalProxy
(
lambda
:
_request_ctx_stack
。
top
。
g
)
其中_request_ctx_stack。top始終指向當前執行緒/協程中儲存的“請求上下文”,這樣像app、request、session、g等都可以以“全域性”的形式存在。這裡“全域性”是指在當前執行緒或協程當中。
由此可以看出,當處理請求時:
首先,會生成一個請求上下文物件,這個上下文物件包含請求相關的資訊。並且在進入上下文環境時,LocalStack會將這個上下文物件推入棧結構中以儲存這個物件;
在這個上下文環境中可以進行請求處理過程,這個稍後再介紹。不過可以以一種“全域性”的方式訪問上下文物件中的變數,例如app、request、session、g等;
當請求結束,退出上下文環境時,LocalStack會清理當前執行緒/協程產生的資料(請求上下文物件);
Flask 0。1版本只有“請求上下文”的概念,在Flask 0。9版本中又增加了“應用上下文”的概念。關於“應用上下文”,以後再加以分析。
4。 在上下文環境中處理請求
處理請求的過程定義在wsgi_app方法中,具體如下:
def
wsgi_app
(
environ
,
start_response
):
with
self
。
request_context
(
environ
):
rv
=
self
。
preprocess_request
()
if
rv
is
None
:
rv
=
self
。
dispatch_request
()
response
=
self
。
make_response
(
rv
)
response
=
self
。
process_response
(
response
)
return
response
(
environ
,
start_response
)
從程式碼可以看出,在上下文物件中處理請求的過程分為以下幾個步驟:
在請求正式被處理之前的一些操作,呼叫preprocess_request()方法,例如開啟一個數據庫連線等操作;
正式處理請求。這個過程呼叫dispatch_request()方法,這個方法會根據URL匹配的情況呼叫相關的檢視函式;
將從檢視函式返回的值轉變為一個Response物件;
在響應被髮送到WSGI伺服器之前,呼叫process_response(response)做一些後續處理過程;
呼叫response(environ, start_response)方法將響應傳送回WSGI伺服器。關於此方法的使用,可以參考:Werkzeug庫——wrappers模組;
退出上下文環境時,LocalStack會清理當前執行緒/協程產生的資料(請求上下文物件)。