這是 flask 原始碼解析系列文章的其中一篇,本系列所有文章列表:
response 簡介
在 flask 應用中,我們只需要編寫 view 函式,並不需要直接和響應(response)打交道,flask 會自動生成響應返回給客戶端。
The return value from a view function is automatically converted into a response object for you.
—— Flask docs
我們知道 HTTP 響應分為三個部分:
狀態列(HTTP 版本、狀態碼和說明)、頭部(以冒號隔開的字元對,用於各種控制和協商)、body(服務端返回的資料)。比如下面訪問部落格首頁的響應:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 自然也會提供所有這些資料的操作,檢視函式就支援返回三個值:第一個是返回的資料,第二個是狀態碼,第三個是頭部字典。比如:
1 2 3 |
@app.route('/') def hello_world(): return 'Hello, World!', 201, {'X-Foo': 'bar'} |
這篇文章就講講這背後的魔法。
flask 響應(response)
在 flask 原始碼解析:應用啟動流程 的最後,我們講到 full_dsipatch_request
在呼叫路由到檢視函式之後,會呼叫 finalize_request
進行最後的處理,在這個方法裡就包含了 response 物件的生成和處理邏輯。
finalize_request
的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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
的定義:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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。
不過從註釋中,我們得到兩條很有用的資訊:
- 一般情況下不要直接操作
Response
物件,而是使用make_response
方法來生成它 - 如果需要使用自定義的響應物件,可以覆蓋 flask app 物件的
response_class
屬性。
繼續,下面就要分析 werkzeug 對應的程式碼了。
werkzeug response
werkzeug 實現的 response 定義在 werkzeug/wrappers.py
檔案中:
1 2 3 4 5 6 7 8 9 10 11 |
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
精簡後的大概框架如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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頭部)。那麼這個看起比較特殊的字典是怎麼實現的呢?來看程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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
屬性就可以了,比如:
1 2 3 4 5 6 7 |
from flask import Flask, Response class MyResponse(Response): pass app = Flask(__name__) app.response_class = MyResponse |
更多可能的用法,可以參考文章末尾的參考資料。
參考資料
請使用手機”掃一掃”x