如何自定義flask的響應類(customizing-the-flask-response-class)

Response是Flask中響應客戶端請求的類,然而在Flask應用中很少直接使用Response。Flask使用Response作爲響應數據的容器,在響應客戶端的請求時會添加一些創建HTTP響應所需要的附加信息。flask的響應數據是由應用的路由函數返回給客戶端。然而,Flask也爲應用提供了一種選擇,來使開發者自己定義一些response類。本文將利用這一點來展示如何簡化你應用的代碼。

Flask的Response是如何工作的?

大多數的應用不會直接使用Flask的Response類,但是這不意味着框架中不使用Response。實際上,Flask爲每個請求都創建響應對象。於是問題來了,它到底是如何工作的呢?
響應週期的起始時刻即:當Flask調用一個函數去處理請求的返回值的這個時間點。在web應用中,路由處理的最後都會調用render_template函數,render_template的主要功能是渲染引用的模板文件並作爲字符串返回給客戶端:

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

但是你可能知道,一個Flask路由處理函數有兩個可選的附加參數返回,一個是響應狀態的編碼,另一個是自定義的HTTP的報文類型:

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

Flask默認的Content-Type值是HTML,請求處理成功的默認編碼值是200,在上邊的例子中,Flaks將狀態編碼設置爲201,響應內容的類型設置爲JSON格式。
響應的內容由三個基本的單元組成:數據或者html的body,狀態編碼,Http報文類型。Flask應用實例中的make_response()函數實現了將路由函數中的返回值存儲在Response對象中的功能。
你可以在Python的console中輸入如下的代碼來觀察上述的處理過程:

>>> 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()方法創建了兩個Response對象。第一個例子中,將一個字符串作爲參數,response中的狀態碼和報文類型使用的是默認值。第二個例子中,參數是一個包含兩個值的元組參數,並將狀態碼設置成爲非默認值。
Flask將Response對象作爲路由函數的響應的同時,有許多細節可以處理。例如,在after_request函數中可以用來插入或者修改http報文類型,改變html的內容或者狀態碼,甚至可以自定義一個完全不同的響應class來替代默認的Response。最後,Flask會將修改之後的響應對象返回給客戶端。

Flask的Response類

下面我們來看看響應類中最核心的部分。下面這個類給出了我所認爲的最核心的屬性和方法:

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 實際上非常簡潔,這是由於Response 的核心功能是由Werkzeug框架中的Response 實現的。Werkzeug中的Response類繼承自BaseResponse 類,
上邊的屬性和方法是由BaseResponse 定義的。

這三個類屬性:charset, default_status 和default_mimetype定義了一些默認值。如果你的應用配置和默認值不符,可以通過繼承Response 類來自定義屬性,這樣可以避免每次在響應時修改這些值。例如,如果你的應用是API,所有的路由返回的都是XML,你可以在自己定製的響應類中修改default_mimetype 爲application/xml,這樣Flask將默認的返回XML格式的響應。

我不會再詳細的介紹init構造方法,但是需要注意的是Response 的構造方法接收三個重要的參數,響應主體,狀態碼和報頭。在子類中,構造方法可以改變這個規則來創建特定的響應。

force_type()方法在響應類中扮演了十分重要的作用。在某些情況下,Werkzeug或Flask需要創建自己的響應對象,例如應用程序發生錯誤時,需要將錯誤響應返回給客戶端。在這種情況下,響應對象是由框架創建的,而不是由應用產生的。當你在一個應用中創建特定的響應類型時,Flask和Werkzeug並不知道這個特定的響應類型的詳細信息,因而框架還是默認的創建標準的響應類型。響應類中的force_type()方法負責將不同的響應實例轉換成自己需要的形式。

我相信你一定會對force_type()的作用感到迷惑。實際上,每當Flask遇到響應對象與期望的類不符時,使用這個方法可以轉換響應對象。在下邊的第三個例子中,我將展示如何利用這個方法使得Flask的路由函數爲每一個請求都返回一個支持諸如字典,列表,或者其他任何形式的特定對象的響應。
上邊已經講述了足夠多的原理,在下邊的章節中我會將這些編程技巧用代碼實現。

使用定製的Response類

 爲一個Flask應用配置一個定製的響應類是非常簡單的。讓我們看看下邊這個例子:
from flask import Flask, Response
class MyResponse(Response):
    pass
app = Flask(__name__)
app.response_class = MyResponse
# …

這裏我自己定義了一個名字是MyResponse 的響應類。通常我們定義自己的響應類時,只需要自定義一個Flask的Response 類的子類,並在此基礎上添加或者修改某些行爲。之後將app.response_class屬性指定爲自定義的類,就可以使Flask使用我們自己的響應類型。

Flask 類的屬性response_class是類屬性,因此作爲從上邊例子的變體,你可以創建一個Flask的子類來使用你自己的響應類:

from flask import Flask, Response
class MyResponse(Response):
    pass
class MyFlask(Flask)
    response_class = MyResponse
app = MyFlask(__name__)
例子#1:改變Response 的默認值

第一個例子十分簡單。比如你的應用在所有的endpoints都返回XML.對於這種情形,我們只需要將默認的content type設置爲application/xml即可。這個只需要兩行代碼就可以實現。

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

很簡單,對不對?如果將這個類設置爲應用的默認響應,你可以在路由函數中返回XML而不必設置
content type。例如:

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

如果使用默認的響應類,路由將會默認接收 application/xml格式的內容類型。如果你需要不同的
content type,你只需要在正常的路由函數返回值裏覆蓋一下默認值即可,

@app.route('/')
def index():
    return '<h1>Hello, World!</h1>', {'Content-Type': 'text/html'}
例子#2:自動地決定Content Type

下邊的這個例子有一些複雜。假定這個應用中的HTML 和XML路由數量相當,這時使用第一種方法就十分低效,因爲你無論怎麼設置默認的格式,都有一半數量的路由需要覆蓋默認的content type。
一個比較好的方案是:創建一個響應類型可以依據響應的內容自行決定使用的content type。下邊給出一個實例:

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)

這個例子中,一開始response中沒有指定content type。接下來,如果response文本是以<?xml
開頭,這說明數據是以XML文檔格式編碼。如果上邊的兩個條件均爲真,將XML content type以
字典的形式作爲參數發送給父類的構造方法.
在這個response類中,任何XML格式的文檔都會自動的接收XML content type,而其他的響應將繼續使用默認的content type。而且在所有的類中,我們仍然可以自動的指定我們需要的content type類型。

例子#3:自動地設定JSON響應

最後的例子將會處理Flask的中非常麻煩的情形。使用Flask的API返回JSON格式是一種非常普遍的情形,這需要你調用jsonify()函數將Python 字典轉換成JSON 格式,並在響應中設置content type爲JSON.下邊是一個路由處理的例子:

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

遺憾的是,所有需要返回JSON的路由函數都需要這麼做,這會導致大量API的結尾都會重複的調用jsonify()函數。從代碼可讀性的角度考慮,是否有方法可以替代?

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

這裏定義了一個響應類,可以支持上邊的語法,使得所有的路由函數不需要再返回JSON:

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 中路由handler 函數只能處理有限的幾種類型。這些類型包括str, unicode, bytes, bytearray
但是當你返回一些不支持的類型,例如上面例子中的字典,Flask會如何處理呢?如果返回的響應類型與期望的類型不符,Flask將其認定爲未知的響應類型,此時不會創建response對象返回信息,而是調用類方法force_type()強制轉換這個未知類型。在這個例子中,子類自定義的響應類覆蓋重寫這個方法,自動轉換返回值是字典的情形。轉換是通過在調用父類方法之前,先調用jsonify()方法來實現。
上面的這種技巧不會影響其他正常的響應函數的功能。這是由於對於任何返回正常響應類來說,子類沒有做任何事情,所有的請求透明的傳遞給父類。

原文鏈接

https://blog.miguelgrinberg.com/post/customizing-the-flask-response-class

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章