第二章 app 的基本結(jié)構(gòu)

初始化

所有 Flask 程序都必須創(chuàng)建一個(gè) app 實(shí)例。Web 服務(wù)器使用 Web 服務(wù)器網(wǎng)關(guān)接口協(xié)議(Web Server Gateway Interface, WSGI)把接收自客戶端的所有請求都轉(zhuǎn)交給這個(gè)對象處理。 app 實(shí)例是 Flask 類的對象:

from flask import Flask
app = Flask(__name__)

Flask 類的構(gòu)造函數(shù)自有一個(gè)必須指定的參數(shù), 即 app 主模塊或包的名字。

Flask 用 name 這個(gè)參數(shù)決定程序的根目錄, 以便稍后能夠找到相對于程序根目錄的資源文件位置。

路由和視圖函數(shù)

客戶端(例如 Web 瀏覽器)把請求發(fā)送給 Web 服務(wù)器, Web 服務(wù)器再把請求發(fā)送給 Flask app 實(shí)例。 app 實(shí)例需要知道對每個(gè) URL 請求運(yùn)行哪些代碼, 所以 app 實(shí)例保存了一個(gè) URL 到 Python 函數(shù)的映射關(guān)系。處理 URL 和函數(shù)之間關(guān)系的程序稱為路由。

在 Flask 中使用 app 實(shí)例提供的 app.route 裝飾器把所裝飾的函數(shù)注冊為路由:

@app.route('/')
def index():
    return '<h1>Hello, 世界!</h1>'

裝飾器是可以把函數(shù)注冊為事件的處理程序。

前例是把 index() 函數(shù)注冊為 app 根地址的處理程序。如果部署的程序的服務(wù)器域名為 www.example.com, 在瀏覽器中訪問 http://www.example.com 后會(huì)觸發(fā)服務(wù)器執(zhí)行 index() 函數(shù)。這個(gè)函數(shù)的返回值稱為 響應(yīng), 它是客戶端接收到的內(nèi)容。如果客戶端是 Web 瀏覽器, 響應(yīng)就是顯示給用戶看的文檔。

像 index() 這樣的函數(shù)稱之為 視圖函數(shù)(view function)。視圖函數(shù)返回的響應(yīng)可以是包含 HTML 的簡單字符串,也可以是復(fù)雜的表單。

可變 URL:

@app.route('/user/<name>')
def user(name):
    return '<h1>Hello, %s!</h1>' % name

路由中的動(dòng)態(tài)部分默認(rèn)使用字符串, 也可以使用 int/float/path 類型, path 類型也是字符串, 但不把斜線作為分割符, 而將其當(dāng)作動(dòng)態(tài)片段的一部分。

@app.route('/user/<int:id>')

啟動(dòng)服務(wù)器

app 實(shí)例使用 run 方法啟動(dòng) Flask 集成的 Web 服務(wù)器:

if __name__ == '__main__':
    app.run(debug=True)

__name__ == '__main__' 確保了只有直接 執(zhí)行這個(gè)腳本時(shí)才啟動(dòng) Web 服務(wù)器。如果這個(gè)腳本由其它腳本引入, 程序假定父級(jí)腳本會(huì)啟動(dòng)不同的服務(wù)器, 因此不會(huì)執(zhí)行 app.run()。

服務(wù)器啟動(dòng)后會(huì)進(jìn)入輪詢, 等待并處理請求, 輪詢會(huì)一直運(yùn)行,直到程序停止,例如按 Ctrl-C 鍵。

一個(gè)完整的 app

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return '<h1>Hello, 世界!</h1>'

@app.route('user/<name>')
def user(name):
    return '<h1>hello, %s!</h1>' % name

if __name__ == '__main__':
    app.run(debug=True)

啟動(dòng)這個(gè) app:

(venv) $ python hello.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader

在瀏覽器中鍵入:

 http://localhost:5000/user/Dave

會(huì)顯示:

<h1>Hello, Dave!</h1>


請求/響應(yīng)循環(huán)

app 和請求上下文

Flask 從客戶端收到請求時(shí), 要讓視圖函數(shù)能訪問一些對象, 這樣才能處理請求。請求對象封裝了客戶端(例如瀏覽器)發(fā)送的 HTTP 請求。

要讓視圖函數(shù)能訪問請求對象, 一個(gè)顯而易見的方式是把請求對象作為參數(shù)傳遞給視圖函數(shù), 不過這會(huì)導(dǎo)致程序中每個(gè)視圖函數(shù)都增加一個(gè)參數(shù)。如果視圖函數(shù)還要訪問其它對象, 那么視圖函數(shù)會(huì)變得越來越臃腫和難以維護(hù)。

為此, Flask 使用 上下文 臨時(shí)把某些對象變?yōu)?em>全局可訪問:

from flask import request
@app.route('/')
def index():
    user_agent = request.headers.get('User-Agent')
    return '<p>你的瀏覽器是 %s</p>' % user_agent

在這個(gè)例子中我們把 request 當(dāng)作全局變量來使用。事實(shí)上, request 不可能是全局變量, 你想想, 在多個(gè)線程同時(shí)處理不同客戶端發(fā)送的不同請求時(shí), 每個(gè)線程看到的 request 對象必然不同。 Flask 使用上下文讓特定的變量在每一個(gè)線程中全局可訪問, 與此同時(shí)卻不會(huì)干擾其它線程。

多線程 Web 服務(wù)器會(huì)創(chuàng)建一個(gè)線程池, 再從線程池中選擇一個(gè)線程用于處理接收到的請求。

在 Flask 中有兩種上下文: app 上下文請求上下文。下表列出了這兩種上下文提供的全局變量:

變量名 上下文 說明
current_app app上下文 當(dāng)前所激活app的app實(shí)例
g app上下文 處理請求時(shí)用作臨時(shí)存儲(chǔ)的對象。每次請求都會(huì)重設(shè)這個(gè)變量
request 請求上下文 請求對象, 封裝了客戶端發(fā)出的 HTTP 請求中的內(nèi)容
session 請求上下文 用戶會(huì)話, 用于存儲(chǔ)請求之間需要"記住"的值的字典

Flask 在分發(fā)請求之前激活(或推送)app上下文和請求上下文, 請求處理完成后再將其刪除。 app 上下文在被推送后, 就可以在線程中使用 current_appg 變量。類似地, 在請求上下文被推送后, 就可以使用 requestsession 變量。如果我們使用這些變量時(shí)沒有激活 app 上下文或請求上下文, 那么程序就會(huì)出錯(cuò)。

激活虛擬環(huán)境后進(jìn)入 Python shell, 下面演示app上下文的使用方法:

>>> from hello import app
>>> from flask import current_app
>>> current_app.name
...
RuntimeError: Working outside of application context.
>>> app_ctx = app.app_context()
>>> app_ctx.push() # 推送 app 上下文
>>> current_app.name
'hello'
>>> app_ctx.pop() # 彈出 app 上下文

在這個(gè)例子中, 沒有激活 app 上下文之前就調(diào)用 current_app.name 就會(huì)導(dǎo)致錯(cuò)誤, 但是推送完上下文之后就可以調(diào)用了。

注意, 在 app 實(shí)例上調(diào)用 .app_context() 方法便獲得了一個(gè)程序上下文。

請求調(diào)度

程序收到客戶端發(fā)來的請求時(shí), 要找到處理該請求的視圖函數(shù)。Flask 通過在 app 的 URL 映射中查找請求的 URL 來完成這個(gè)任務(wù)。 URL 映射是 URL 和視圖函數(shù)之間的對應(yīng)關(guān)系。 Flask 使用 app.route 裝飾器/非裝飾器形式的 app.add_url_rule() 生成映射。

我們在 Python shell 中查看 Flask 的 app 中映射是什么樣子的:(所有操作請確保你已經(jīng)激活了虛擬環(huán)境)

(venv) $ python
>>> from hello import app
>>> app.url_map
>>> app.url_map
Map([<Rule '/' (HEAD, OPTIONS, GET) -> index>,
<Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
<Rule '/user/<name>' (HEAD, OPTIONS, GET) -> user>])

//usr/<name> 路由在 app 中使用 app.route 裝飾器定義。 /static/<filename> 路由是 Flask 添加的特殊路由, 用于訪問靜態(tài)文件。

URL 映射中的 HEAD、OPTIONS、GET 是請求方法。Flask 為每個(gè)路由都指定了請求方法, 這樣不同的請求發(fā)送到相同的 URL 上時(shí), 會(huì)使用不同的視圖函數(shù)進(jìn)行處理。 HEAD 和 OPTIONS 方法由 Flask 自動(dòng)處理, 因此可以說上面的 app 中 URL 映射中的 3 個(gè)路由都使用 GET 方法。

請求鉤子

有時(shí)需要在請求之前或之后執(zhí)行代碼會(huì)很有用。例如, 在請求開始時(shí)我們可能需要?jiǎng)?chuàng)建數(shù)據(jù)庫連接/認(rèn)證發(fā)起請求的用戶。為了避免在每個(gè)視圖函數(shù)中都使用重復(fù)的代碼, Flask 提供了注冊通用函數(shù)的功能, 注冊的函數(shù)可在請求被分發(fā)到視圖函數(shù)之前/之后被調(diào)用。

請求鉤子 使用裝飾器實(shí)現(xiàn)。 Flask 支持以下 4 種鉤子:

  • before_first_request: 注冊一個(gè)函數(shù), 在處理第一個(gè)請求之前運(yùn)行。
  • before_request: 注冊一個(gè)函數(shù), 在每次請求之前運(yùn)行。
  • after_request: 注冊一個(gè)函數(shù), 如果沒有未處理的異常拋出, 則在每次請求之后運(yùn)行。
  • teardown_request: 注冊一個(gè)函數(shù), 即使有未處理的異常拋出, 也在每次請求之后運(yùn)行。

在請求鉤子和視圖函數(shù)之間共享數(shù)據(jù)一般使用上下文全局變量 g。 例如 before_request 處理程序可以從數(shù)據(jù)庫中加載已登錄用戶, 并將其保存到 g.user 中。隨后調(diào)用視圖函數(shù)時(shí), 視圖函數(shù)再使用 g.user 獲取用戶。

響應(yīng)

Flask 調(diào)用視圖函數(shù)后, 會(huì)將其返回值作為響應(yīng)的內(nèi)容。大多數(shù)情況下, 響應(yīng)就是一個(gè)簡單的字符串, 作為 HTML 頁面回送客戶端。

在響應(yīng)的文本之后添加一個(gè)狀態(tài)碼:

@app.route('/')
def index():
    return '<h1>Bad Request</h1>', 400

表示請求無效。

視圖函數(shù)返回的響應(yīng)還可以接收第三個(gè)參數(shù), 這是一個(gè)由首部(header)組成的字典, 可以添加到 HTTP 響應(yīng)中。

Flask 還可以返回 Response 對象。 make_response() 函數(shù)可接受 1/2/3 個(gè)參數(shù)(和視圖函數(shù)的返回值一樣), 并返回一個(gè) Response 對象。

from flask import make_response
@app.route('/')
def index():
    response = make_response('<h1>This document carries a cookie!</h1>')
    response.set_cookie('answer', '42')
    return response

重定向是一種特殊的響應(yīng)類型。這種響應(yīng)類型沒有頁面文檔, 只告訴瀏覽器一個(gè)新的地址用以加載新頁面。Flask 已經(jīng)提供了 redirect() 函數(shù):

from flask import redirect
@app.route('/')
def index():
    return redirect('http://www.example.com')

還有一種特殊的 abort 響應(yīng), 用于處理錯(cuò)誤:

from flask import abort
@app.route('/user/<id>')
def get_user(id):
    user = load_user(id):
    if not user:
        abort(404)
    return '<h1>Hello, %s!</h1>' % user.name

Flask 擴(kuò)展

使用Flask-Script支持命令行選項(xiàng)

Flask 的開發(fā) Web 服務(wù)器支持很多啟動(dòng)設(shè)置選項(xiàng),但只能在腳本中作為參數(shù)傳給 app.run()函數(shù)。這種方式并不十分方便,傳遞設(shè)置選項(xiàng)的理想方式是使用命令行參數(shù)。

Flask-Script 是一個(gè) Flask 擴(kuò)展,為 Flask 程序添加了一個(gè)命令行解析器。Flask-Script 自帶了一組常用選項(xiàng),而且還支持自定義命令。

# 安裝
(venv) $ pip install flask-script

要使用 flask-script 需要在 hello.py 修改下程序:

from flask import Flask
from flask.ext.script import Manager

app = Flask(__name__)
manager = Manager(app)

@app.route('/')
def index():
    return '<h1>Hello,World</h1>'
    
if __name__ == '__main__':
    manager.run()

專為 Flask 開發(fā)的擴(kuò)展都暴漏在 flask.ext 命名空間下。Flask-Script 輸出了一個(gè)名為Manager 的類,可從 flask.ext.script 中引入。

這個(gè)擴(kuò)展的初始化方法也適用于其他很多擴(kuò)展:把 app 實(shí)例作為參數(shù)傳給擴(kuò)展的構(gòu)造函數(shù),初始化主類的實(shí)例。創(chuàng)建的對象可以在各個(gè)擴(kuò)展中使用。在這里,服務(wù)器由 manager.run() 啟動(dòng),啟動(dòng)后就能解析命令行了。

注意, 在 Python 3 中要這樣導(dǎo)入 flask-script 擴(kuò)展, from flask_script import Manager

現(xiàn)在運(yùn)行 hello.py,會(huì)顯示一個(gè)幫助消息:

$ python hello.py
usage: hello.py [-h] {shell,runserver} ...

positional arguments:
  {shell,runserver}
      shell           在 Flask 應(yīng)用上下文中運(yùn)行 Python shell
      runserver  運(yùn)行 Flask 開發(fā)服務(wù)器:app.run()

optional arguments:
  -h, --help       顯示幫助信息并退出

shell 命令用于在程序的上下文中啟動(dòng) Python shell 會(huì)話。你可以使用這個(gè)會(huì)話中運(yùn)行維護(hù)任務(wù)或測試,還可調(diào)試異常。顧名思義, runserver 命令用來啟動(dòng) Web 服務(wù)器。運(yùn)行 python hello.py runserver 將以調(diào)試模式啟動(dòng) Web 服務(wù)器,但是我們還有很多選項(xiàng)可用:

$ python hello.py runserver --help
usage: hello.py runserver [-?] [-h HOST] [-p PORT] [--threaded]
                          [--processes PROCESSES] [--passthrough-errors] [-d]
                          [-D] [-r] [-R]

--host 參數(shù)是個(gè)很有用的選項(xiàng),它告訴 Web 服務(wù)器在哪個(gè)網(wǎng)絡(luò)接口上監(jiān)聽來自客戶端的連接。默認(rèn)情況下,F(xiàn)lask 開發(fā) Web 服務(wù)器監(jiān)聽 localhost 上的連接,所以只接受來自服務(wù)器所在計(jì)算機(jī)發(fā)起的連接。下述命令讓 Web 服務(wù)器監(jiān)聽公共網(wǎng)絡(luò)接口上的連接,允許同網(wǎng)中的其他計(jì)算機(jī)連接服務(wù)器:

(venv) $ python hello.py runserver --host 0.0.0.0
* Running on http://0.0.0.0:5000/
* Restarting with reloader

現(xiàn)在,Web 服務(wù)器可使用 http://a.b.c.d:5000/ 網(wǎng)絡(luò)中的任一臺(tái)電腦進(jìn)行訪問,其中 “a.b.c.d” 是服務(wù)器所在計(jì)算機(jī)的外網(wǎng) IP 地址。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容