如何自定義Flask中的響應類(譯文)

有翻譯或理解不對的地方,望你們指正!json

Flask框架中的響應類,命名很貼切,叫Response。不過Flask應用中不多直接調用這個類。而是將其做爲路由函數所返回響應數據的內部容器,容器裏還包含了用於建立HTTP響應的其餘信息。flask

可是沒多少人知道,Flask框架其實容許應用將默認的響應類,替換爲自定義類。這就給了咱們研究小竅門的機會。在本文中,我將展現如何利用Flask的這個特性,簡化你的代碼。網絡

Flask中的響應類是如何工做的?

大部分應用並不直接使用Flask中的響應類(Response class),但這並非說這個類沒有用武之地;實際上,Flask會爲每一個請求建立響應對象。那麼,它是如何實現的呢?session

Flask用來處理請求的函數返回時,響應週期就開始了。在網絡應用中,路由一般最後會調用render_template函數,渲染引用的模板文件,將其做爲字符串返回:併發

@app.route('/index')
def index():
    # ...
    return render_template('index.html')

可是,你可能也知道,Flask的路由函數能夠選擇額外返回兩個值,這兩個值將被分別設爲HTTP狀態碼和自定義的HTTP響應標頭:app

@app.route('/data')
def index():
    # ...
    return render_template('data.json'), 201, {'Content-Type': 'application/json'}

在上面的例子中,狀態碼被設爲201,取代了Flask默認的200,即請求被成功處理的狀態碼。這個例子還定義了內容類型標頭(Content-Type header),代表HTTP響應中包含JSON數據,由於若是你不明確設置內容類型的話,Flask會默認設置爲HTML。框架

上面的例子介紹了HTTP響應的三個基本組成部分,即數據或正文、狀態碼和標頭。Flask的應用實例擁有一個make_response函數,能夠接受路由函數的返回值(能夠是單個值,也能夠是有1-3個值的元組),並將其填入響應對象(Response object)中。函數

你能夠經過Python控制檯會話(console session),看看整個過程。首先建立一個虛擬環境,並安裝Flask,而後開啓Python會話,並輸入下面的代碼:

>>> from flask import Flask
>>> app = Flask(__name__)
>>> app.make_response('Hello, World')
<Response 12 bytes [200 OK]>
>>> app.make_response(('Hello, World', 201))
<Response 12 bytes [201 CREATED]>

這裏,我建立了一個簡單的Flask應用實例,以後調用了make_response()方法建立響應類對象。第一次調用時,我傳了一個字符串做爲參數,因此響應對象中使用了默認的狀態碼和標頭。第二次調用時,我傳入了有兩個值的元組,強制返回了非默認的狀態碼。注意,第二次調用時使用了兩個括號,裏層的括號將字符串和狀態碼包在了元組中。因爲make_response()函數只接受一個參數,因此必需要這樣作。

Flask在建立了表明路由函數返回值的響應對象(Response object)以後,還會作一些處理。包括將響應對象傳入自定義的after_request處理程序(handlers),在這一步,應用還有有機會插入或修改標頭、更改正文或狀態碼,若是願意的話,甚至是啓用嶄新的的響應對象取而代之。最後,Flask會獲取最終的響應對象,渲染成HTTP響應,併發送給客戶端。

Flask中的響應類

咱們來看看響應類中最有趣的特性。下面的類定義,展現了我眼中這個類所具有的靈活屬性和方法:

class Response:
    charset = 'utf-8'
    default_status = 200
    default_mimetype = 'text/html'

    def __init__(self, response=None, status=None, headers=None,
                 mimetype=None, content_type=None, direct_passthrough=False):
        pass

    @classmethod
    def force_type(cls, response, environ=None):
        pass

注意,若是你去翻閱Flask的源碼,是找不到上述類定義的。Flask中的Response類,實際上衍生自Werkzeug庫中的一個同名類。而Werzeug中的Response類繼承的是BaseResponse類,這個類中就包含了上述定義。

charsetdefault_statusdefault_mimetype這三個類屬性定義了相應的默認值。若是任何一個默認值不適用你的應用,那麼你能夠建立Response類的子類,定義你本身的默認值,而沒必要在每個響應對象中設置自定義值。例如,若是你的應用是一個全部的路由均返回XML格式數據的API接口,你就能夠在自定義的類中,將default_mimetype改成application/xml,這樣Flask就會默認返回XML響應。稍後你會看到如何實現。

這裏,我不會詳細介紹__init__構造函數(你能夠閱讀Werkzeug的文檔),但請注意,Flask響應對象中的三個重要元素,即響應正文、狀態碼和標頭,是做爲參數傳入的。在子類中,構造函數能夠改變建立響應的相應規則。

響應類中的force_type()類方法,是惟一比較複雜,但又很重要的元素。有時候,Werkzeug或是Flask須要自行建立響應對象,好比出現應用錯誤,並須要將其返回給客戶端時。在這種狀況下,響應對象不是應用提供的,而是由框架建立的。在使用自定義響應類的應用中,Flask和Werkzeug沒法知道自定義類的細節,因此它們使用標準響應類來建立響應。響應類中的force_type()方法,被設計爲能夠接受不一樣響應類的實例,並會將其轉換成自身的格式。

我敢確定,你必定被force_type()方法的描述搞糊塗了。說白了,就是若是Flask碰到了一個不是其指望的響應對象,就會使用該方法進行轉換。我下面要講的第三個使用場景,就利用了這個特色,讓Flask的路由函數返回諸如字典、列表或者是其餘任何自定義對象,做爲請求的響應對象。

好了,理論就講這麼多了。接下來,我來告訴你們如何應用上面有關響應類的小技巧。準備好了嗎?

使用自定義的響應類

到如今爲止,我肯定你也會認爲:在部分有趣的場景下,使用自定義的響應類是有利的。在給出實際例子以前,我想告訴你在Flask中設置並使用自定義的響應類是多麼的簡單。請看下面的這個例子:

from flask import Flask, Response

class MyResponse(Response):
    pass

app = Flask(__name__)
app.response_class = MyResponse

# ...

在上面的代碼中,我定義了一個名叫MyResponse的自定義響應類。一般,自定義響應類會增長或修改默認類的行爲,因此通常都會經過建立Flask中Response類的子類來實現。要想讓Flask使用自定義類,我只須要設置app.response_class便可。

Flask類中的response_class是一個類屬性,因此咱們能夠稍微修改上面的例子,建立一個設置了自定義響應類的Flask子類:

from flask import Flask, Response

class MyResponse(Response):
    pass

class MyFlask(Flask)
    response_class = MyResponse

app = MyFlask(__name__)

# ...

例1:更改響應對象的默認值

第一個例子極其簡單。假設你的應用中大部分或所有端點(endpoints)都返回的是XML。對於這樣的應用,將默認的內容類型設置爲application/xml是合理的。能夠經過下面這個僅有兩行代碼的響應類輕鬆實現:

class MyResponse(Response):
    default_mimetype = 'application/xml'

容易,對吧?若是將其設爲應用的默認響應類,那麼你在編寫返回XML的函數時,就不用擔憂忘記設置內容類型了。舉個例子:

@app.route('/data')
def get_data():
    return '''<?xml version="1.0" encoding="UTF-8"?>
<person>
    <name>John Smith</name>
</person>
'''

上面這個路由使用的是默認響應類,其內容類型會被設置爲text/html,由於那是默認類型。使用自定義響應類,能夠免去你在全部XML路由的返回語句中,額外加上標頭的麻煩。另外,若是有的路由須要其餘的內容類型,你仍能夠替換掉默認值,就像對待通常的響應類同樣。

@app.route('/')
def index():
    return '<h1>Hello, World!</h1>', {'Content-Type': 'text/html'}

例2:自動決定內容類型

下一個例子更復雜一點。假設咱們的應用中HTML路由與XML路由的數量差很少,因此按照第一個例子的作法就不行了,由於無論你選用哪一種默認類型,都會有一半的路由須要替換內容類型。

更好的解決辦法,則是建立一個可以經過分析響應文本,決定正確的內容類型的響應類。下面的這個類實現了該功能:

class MyResponse(Response):
    def __init__(self, response, **kwargs):
        if 'mimetype' not in kwargs and 'contenttype' not in kwargs:
            if response.startswith('<?xml'):
                kwargs['mimetype'] = 'application/xml'
        return super(MyResponse, self).__init__(response, **kwargs)

在這個簡單的例子中,我首先確保響應對象中沒有明確設置內容類型。而後,我檢查響應的正文是否以<?xml開頭,是的話就意味着數據是XML文檔格式。若是兩個條件同時成立,我會在傳入父類構造函數的參數中,插入XML內容類型。

有了這個自定義響應類,任何知足XML格式要求的文檔都會自動被標記爲XML內容類型,而其餘響應則會繼續得到默認的內容類型。並且,在全部的類中,我仍然能夠在必要時聲明內容類型。

例3:自動返回JSON響應

最後一個例子,針對的是利用Flask建立API接口時常見的一個小問題。API接口一般返回的是JSON淨負荷(JSON Payload,這就要求你使用jsonify()函數將Python字典類型轉換成JSON數據,而且還得在響應對象中將內容類型設置爲JSON內容類型。請看下面這個例子:

@app.route('/data')
def get_data():
    return jsonify({'foo': 'bar'})

問題是,每一個返回JSON的路由都須要這樣處理,那麼對接口數量衆多的的API來講,你就得大量重複調用jsonify()函數。從代碼可讀性角度來說,你按照下面的方式處理是否是更好?

@app.route('/data')
def get_data():
    return {'foo': 'bar'}

下面是一個支持使用上述語法的自定義響應類,它不會影響應用中使用其餘內容類型的路由正常工做:

class MyResponse(Response):
    @classmethod
    def force_type(cls, rv, environ=None):
        if isinstance(rv, dict):
            rv = jsonify(rv)
        return super(MyResponse, cls).force_type(rv, environ)

這個例子須要稍微解釋一下,由於比較複雜。Flask僅承認一小部分的類型,做爲路由函數可以返回的有效響應類型。基本上,你能夠返回任意與字符串和二進制相關的類型(strunicodebytesbytearray)。若是你喜歡,甚至能夠返回一個已經建立好的響應對象。若是你返回的是字符串或二進制類型,Flask會發現這些是響應類知道如何處理的類型,並會將你返回的數據直接傳入響應類的構造函數。

可是,若是你返回的是不支持的類型,好比說上述例子中的字典,會發生什麼狀況?若是返回的響應類型不是Flask預期的,那麼Flask就會默認它是未知響應對象,不會以其爲參數建立響應對象了,而是使用響應類的force_type()類方法,強制轉換未知類型。上面的例子中,響應子類替換了該方法,但僅僅是經過調用jsonify()進行轉換,以後就會讓基類接手處理,就好像什麼都沒發生同樣。

是個很好的竅門吧?尤爲是這樣作不會影響其餘響應的正常工做。對於返回正常響應類型的路由,該子類不會作任何處理,全部的調用請求會所有傳入父類中。

結語

我但願本文可以幫助你們更好地理解FLask中響應對象的工做原理。若是你知道其餘使用Flask響應類的小竅門,請務必與我分享!

相關文章
相關標籤/搜索