flask 源碼解析:響應

這是 flask 源碼解析系列文章的其中一篇,本系列全部文章列表:html

response 簡介

在 flask 應用中,咱們只須要編寫 view 函數,並不須要直接和響應(response)打交道,flask 會自動生成響應返回給客戶端。post

The return value from a view function is automatically converted into a response object for you.
—— Flask docsui

咱們知道 HTTP 響應分爲三個部分:
狀態欄(HTTP 版本、狀態碼和說明)、頭部(以冒號隔開的字符對,用於各類控制和協商)、body(服務端返回的數據)。好比下面訪問博客首頁的響應:this

HTTP/1.1 200 OK

Access-Control-Allow-Origin: *
Cache-Control: max-age=600
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Wed, 15 Feb 2017 07:50:41 GMT
Expires: Wed, 15 Feb 2017 08:00:41 GMT
Last-Modified: Wed, 15 Feb 2017 07:46:56 GMT
Server: GitHub.com
Transfer-Encoding: chunked
X-GitHub-Request-Id: D2A7:7B6B:33C0628:47C44B9:58A40851

<BODY>

flask 天然也會提供全部這些數據的操做,視圖函數就支持返回三個值:第一個是返回的數據,第二個是狀態碼,第三個是頭部字典。好比:

@app.route('/')
def hello_world():
    return 'Hello, World!', 201, {'X-Foo': 'bar'}

這篇文章就講講這背後的魔法。

flask 響應(response)

flask 源碼解析:應用啓動流程 的最後,咱們講到 full_dsipatch_request 在調用路由到視圖函數以後,會調用 finalize_request 進行最後的處理,在這個方法裏就包含了 response 對象的生成和處理邏輯。

finalize_request 的代碼以下:

def finalize_request(self, rv, from_error_handler=False):
    """Given the return value from a view function this finalizes
    the request by converting it into a response and invoking the
    postprocessing functions.  This is invoked for both normal
    request dispatching as well as error handlers.
    """
    response = self.make_response(rv)
    try:
        response = self.process_response(response)
        request_finished.send(self, response=response)
    except Exception:
        if not from_error_handler:
            raise
        self.logger.exception('Request finalizing failed with an '
                              'error while handling an error')
    return response

裏面有兩個方法調用:make_response 根據視圖函數的返回值生成 response 對象,process_response 對 response 作一些後續的處理(好比執行 hooks 函數)。咱們先來看看 make_response

def make_response(self, rv):
    """Converts the return value from a view function to a real
    response object that is an instance of :attr:`response_class`.
    """
    status_or_headers = headers = None
    if isinstance(rv, tuple):
        rv, status_or_headers, headers = rv + (None,) * (3 - len(rv))

    if isinstance(status_or_headers, (dict, list)):
        headers, status_or_headers = status_or_headers, None

    if not isinstance(rv, self.response_class):
        # When we create a response object directly, we let the constructor
        # set the headers and status.  We do this because there can be
        # some extra logic involved when creating these objects with
        # specific values (like default content type selection).
        if isinstance(rv, (text_type, bytes, bytearray)):
            rv = self.response_class(rv, headers=headers,
                                     status=status_or_headers)
            headers = status_or_headers = None

    if status_or_headers is not None:
        if isinstance(status_or_headers, string_types):
            rv.status = status_or_headers
        else:
            rv.status_code = status_or_headers
    if headers:
        rv.headers.extend(headers)

    return rv

make_response 是視圖函數能返回多個不一樣數量和類型值的關鍵,由於它能處理這些狀況,統一把它們轉換成 response。
若是返回值自己就是 Response 實例,就直接使用它;若是返回值是字符串類型,就把它做爲響應的 body,並自動設置狀態碼和頭部信息;
若是返回值是 tuple,會嘗試用 (response, status, headers) 或者 (response, headers) 去解析。

NOTE:由於視圖函數能夠返回 Response 對象,所以咱們能夠直接操做 Response

無論視圖函數返回的是什麼,最終都會變成 Response 對象,那麼咱們就來看看 Response 的定義:

from werkzeug.wrappers import Response as ResponseBase


class Response(ResponseBase):
    """The response object that is used by default in Flask.  Works like the
    response object from Werkzeug but is set to have an HTML mimetype by
    default.  Quite often you don't have to create this object yourself because
    :meth:`~flask.Flask.make_response` will take care of that for you.

    If you want to replace the response object used you can subclass this and
    set :attr:`~flask.Flask.response_class` to your subclass.
    """
    default_mimetype = 'text/html'

Flask 的 Response 類很是簡單,它只是繼承了 werkzeug.wrappers:Response,而後設置默認返回類型爲 html。
不過從註釋中,咱們獲得兩條頗有用的信息:

  1. 通常狀況下不要直接操做 Response 對象,而是使用 make_response 方法來生成它

  2. 若是須要使用自定義的響應對象,能夠覆蓋 flask app 對象的 response_class 屬性。

繼續,下面就要分析 werkzeug 對應的代碼了。

werkzeug response

werkzeug 實現的 response 定義在 werkzeug/wrappers.py 文件中:

class Response(BaseResponse, ETagResponseMixin, ResponseStreamMixin,
               CommonResponseDescriptorsMixin,
               WWWAuthenticateMixin):

    """Full featured response object implementing the following mixins:

    - :class:`ETagResponseMixin` for etag and cache control handling
    - :class:`ResponseStreamMixin` to add support for the `stream` property
    - :class:`CommonResponseDescriptorsMixin` for various HTTP descriptors
    - :class:`WWWAuthenticateMixin` for HTTP authentication support
    """

和咱們在 flask 請求分析的 Request 類同樣,這裏使用了 Mixin 機制。BaseResponse 精簡後的大概框架以下:

class BaseResponse(object):
    """Base response class.  The most important fact about a response object
    is that it's a regular WSGI application.  It's initialized with a couple
    of response parameters (headers, body, status code etc.) and will start a
    valid WSGI response when called with the environ and start response
    callable.
    """

    charset = 'utf-8'
    default_status = 200
    default_mimetype = 'text/plain'
    automatically_set_content_length = True

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

BaseResponse 有一些類屬性,定義了默認的值,好比默認字符編碼是 utf-8,默認狀態碼是 200 等。實例化的時候接受的參數有:

  • response: 字符串或者其餘 iterable 對象,做爲響應的 body

  • status: 狀態碼,能夠是整數,也能夠是字符串

  • headers: 響應的頭部,能夠是個列表,也能夠是 werkzeug.datastructures.Headers 對象

  • mimetype: mimetype 類型,告訴客戶端響應 body 的格式,默認是文本格式

  • content_type: 響應頭部的 Content-Type 內容

全部這些參數都是可選的,默認狀況下會生成一個狀態碼爲 200,沒有任何 body 的響應。status、status_code 做爲 Response 的屬性,能夠直接讀取和修改。body 數據在內部保存爲 iterable 的類型,
可是對外也提供了直接讀寫的接口 self.data

def get_data(self, as_text=False):
        """The string representation of the request body.  Whenever you call
        this property the request iterable is encoded and flattened.
        """
        self._ensure_sequence()
        rv = b''.join(self.iter_encoded())
        if as_text:
            rv = rv.decode(self.charset)
        return rv

    def set_data(self, value):
        """Sets a new string as response.  The value set must either by a
        unicode or bytestring.
        """
        if isinstance(value, text_type):
            value = value.encode(self.charset)
        else:
            value = bytes(value)
        self.response = [value]
        if self.automatically_set_content_length:
            self.headers['Content-Length'] = str(len(value))

    data = property(get_data, set_data, doc='''
        A descriptor that calls :meth:`get_data` and :meth:`set_data`.  This
        should not be used and will eventually get deprecated.
        ''')

body 字符的編碼和長度都是自動設置的,用戶不須要手動處理。

至於頭部的存儲,werkzeug 使用的是相似於字典的 werkzeug.datastructures:Headers 類。在flask 源碼解析:請求這篇文章中,咱們沒有詳細
解釋頭部的存儲,那麼這篇文章就具體分析一下吧。

Headers 這個類的提供了不少和字典相同的接口:keys、values、iterms,可是和字典的區別在於它保存的值是有序的,並且容許相同 key 的值存在。
爲何這麼設計呢?由於着更符合 HTTP 頭部的特性。先來看看有序,在 HTTP 傳送的過程當中,若是頭部各個 key-value 鍵值對順序發生變化,有些代理或者客戶端等組件會認爲請求被篡改而丟棄或者拒絕請求的處理,因此最好把頭部設置爲有序的,用戶按照什麼順序設置的,就按照什麼順序存儲;再說說相同 key 的問題,這是由於 HTTP 頭部同一個 key 可能有多個 value(好比 Accept、SetCookie頭部)。那麼這個看起比較特殊的字典是怎麼實現的呢?來看代碼:

class Headers(object):
    """An object that stores some headers.  It has a dict-like interface
    but is ordered and can store the same keys multiple times.
    """

    def __init__(self, defaults=None):
        self._list = []
        if defaults is not None:
            if isinstance(defaults, (list, Headers)):
                self._list.extend(defaults)
            else:
                self.extend(defaults)

    def __getitem__(self, key, _get_mode=False):
        if not _get_mode:
            if isinstance(key, integer_types):
                return self._list[key]
            elif isinstance(key, slice):
                return self.__class__(self._list[key])
        if not isinstance(key, string_types):
            raise exceptions.BadRequestKeyError(key)
        ikey = key.lower()
        for k, v in self._list:
            if k.lower() == ikey:
                return v
        if _get_mode:
            raise KeyError()
        raise exceptions.BadRequestKeyError(key)

能夠看到,頭部信息在內部存儲爲二元組構成的列表,這樣就能同時保證它的有序性和重複性。一個核心的方法是 __getitem__,它定義瞭如何獲取頭部中的信息:

  • 經過下標 header[3],直接返回對應未知存儲的鍵值對元組

  • 經過 key,返回 value header['Accept'],返回匹配的第一個 value 值

  • 經過 slice header[3:7],返回另一個 Headers 對象,保存了 slice 中全部的數據

而後實現 keys()items()pop()setdefault() 等方法讓它表現出來字典的特性,除此以外還有 add()extend()add_header() 等和字典無關的方法方便操做。

自定義 response

若是須要擴展 flask Response 的功能,或者乾脆把它替換掉,只要修改 flask app 的 response_class 屬性就能夠了,好比:

from flask import Flask, Response

class MyResponse(Response):
    pass

app = Flask(__name__)
app.response_class = MyResponse

更多可能的用法,能夠參考文章末尾的參考資料。

參考資料

相關文章
相關標籤/搜索