這篇博客會涉及一些 WSGI 的知識,不了解的可以看這篇博客,簡單了解一下。
Python 的 WSGI 簡單入門
一、請求在 flask 中的處理過程
我們先來看一下 werkzeug.routing 包下 Map 和 Rule 方法的使用,這里給出一個官方的示例(我進行了一點修改并增加了簡單的運行代碼):
from werkzeug.routing import Map, Rule, Subdomain, NotFound, RequestRedirecturl_map = Map([Rule('/', endpoint='blog/index'),Rule('/<int:year>', endpoint='blog/archive'),Rule('/<int:year>/<int:month>/', endpoint='blog/archive'),Rule('/<int:year>/<int:month>/<int:day>', endpoint='blog/archive'),Rule('/<int:year>/<int:month>/<int:day>/<slug>', endpoint='blog/show_post'),Rule('/about', endpoint='blog/about_me'),Rule('/feeds', endpoint='blog/feeds'),Rule('/feeds/<feed_name>.rss', endpoint='blog/show_feed')
])def application(environ, start_response):urls = url_map.bind_to_environ(environ)try:endpoint, args = urls.match()except HTTPException as e:return e(environ, start_response)start_response('200 OK', [('Content-Type', 'text/plain')])rsp = f'Rule points to {endpoint!r} with arguments {args!r}'return [rsp.encode('utf-8')] # 直接返回字符串會報錯,這里進行一次轉換# provide a basic wsgi for test!
if __name__ == '__main__':from wsgiref.simple_server import make_serverwith make_server('', 8000, application) as httpd:print("Listening on port 8000....")httpd.serve_forever()
flask 的底層也是依賴于 Map 和 Rule,所以我們使用 @route
或者 add_url_rule
最終的目的也是構建類似上面的 url_map
對象,只不過它更加易用。有趣的是,這里并沒有 view_func
函數,所以我們是統一返回了 200 OK
,不過 urls.match
的參數也表明了我們得到的是 endpoint
,這是我們通過它來查找到對應 view_func
的關鍵信息。
要注意這里的 urls.match()
的返回值中這個 args 是指 url 中的定義的參數,下面是幾個示例:
urls = m.bind("example.com", "/")
urls.match("/", "GET")
# ('index', {})
urls.match("/downloads/42")
# ('downloads/show', {'id': 42})
1.1 add_url_rule
方法 和 @route
裝飾器
add_url_rule
: Connects a URL rule. Works exactly like the :meth:route
decorator. If a view_func is provided it will be registered with the endpoint.
連接 URL 規則。其工作原理與 route
裝飾器完全相同。如果提供了 view_func 函數,它會被用 endpoint 來注冊。
基礎示例:
@app.route('/')
def index():pass
等價于以下:
def index():passapp.add_url_rule('/', 'index', index)
如果沒有提供 view_func
函數,需要手動綁定 endpoint
和 view_func
函數。
app.view_function['index'] = index
在內部,route
方法會調用 add_url_rule
方法。
下面我們來看源碼,這里我進行了刪減,對于我認為不重要的部分去掉,我認為這樣可以節約理解設計思路的腦力。
@setupmethod
def add_url_rule(self, rule, endpoint=None, view_func=None,provide_automatic_options=None, **options):# 這里上下省略部分代碼,只保留我認為關鍵的代碼if endpoint is None:endpoint = _endpoint_from_view_func(view_func)rule = self.url_rule_class(rule, methods=methods, **options)self.url_map.add(rule)if view_func is not None:old_func = self.view_functions.get(endpoint)if old_func is not None and old_func != view_func:raise AssertionError('View function mapping is overwriting an ''existing endpoint function: %s' % endpoint)self.view_functions[endpoint] = view_func
說明:首先如果 endpoint
為空,則會使用 view_func
函數的名字,接著使用 add_url_rule
函數的參數創建 Rule
對象,將其加入 self.url_map
中,這是一個 Map
對象。然后會將 endpoint
作為鍵, view_func
作為值,存入 self.view_functions
中,它是一個 dict
對象。
也就是說我們最終得到了下面兩個對象,它們是 Flask 類的兩個實例屬性。還記得上面的 urls.match
方法嗎?當我們獲取到 endpoint 后,就可以它為鍵在 slef.view_functions
中去索引對應的 view_func
函數,然后用它來執行對應路由的請求。
class Flask(_PackageBoundObject):def __init__(self,import_name,static_url_path=None,static_folder='static',static_host=None,host_matching=False,subdomain_matching=False,template_folder='templates',instance_path=None,instance_relative_config=False,root_path=None):#: The :class:`~werkzeug.routing.Map` for this instance.self.url_map = Map()#: A dictionary of all view functions registered. The keys will #: be function names which are also used to generate URLs and #: the values are the function objects themselves.#: To register a view function, use the :meth:`route` decorator.self.view_functions = {}
route 只是一個方便的裝飾器函數,本質上還是調用 add_url_rule
函數。
def route(self, rule, **options):"""A decorator that is used to register a view function for agiven URL rule. This does the same thing as :meth:`add_url_rule`but is intended for decorator usage::@app.route('/')def index():return 'Hello World'"""def decorator(f):endpoint = options.pop('endpoint', None)self.add_url_rule(rule, endpoint, f, **options)return freturn decorator
2.1 Flask 中的請求處理過程
我們創建的 Flask 的實例,最終也是類似于上面的 application 被 wsgi 服務調用,只是更加復雜一些,下面就來看看簡化的流程:
class Flask(_PackageBoundObject):def __call__(self, environ, start_response):"""The WSGI server calls the Flask application object as theWSGI application. This calls :meth:`wsgi_app` which can bewrapped to applying middleware."""return self.wsgi_app(environ, start_response)def wsgi_app(self, environ, start_response):"""The actual WSGI application. This is not implemented in:meth:`__call__` so that middlewares can be applied withoutlosing a reference to the app object. Instead of doing this::app = MyMiddleware(app)It's a better idea to do this instead::app.wsgi_app = MyMiddleware(app.wsgi_app)Then you still have the original application object around andcan continue to call methods on it."""ctx = self.request_context(environ) # 創建請求上下文error = Nonetry:try:ctx.push() # 推入請求上下文response = self.full_dispatch_request() # 分派請求except Exception as e:error = eresponse = self.handle_exception(e)except:error = sys.exc_info()[1]raisereturn response(environ, start_response) # 響應客戶端finally:if self.should_ignore_error(error):error = Nonectx.auto_pop(error) # 彈出,防止積壓,造成資源泄漏def full_dispatch_request(self):"""Dispatches the request and on top of that performs requestpre and postprocessing as well as HTTP exception catching anderror handling... versionadded:: 0.7"""self.try_trigger_before_first_request_functions()try:request_started.send(self)rv = self.preprocess_request() if rv is None:rv = self.dispatch_request() # 這里前后增加了一些資源處理操作,except Exception as e: # 不過不是我們關注的重點,只看rv = self.handle_user_exception(e) # 這一行業務相關的即可return self.finalize_request(rv)def dispatch_request(self):"""Does the request dispatching. Matches the URL and returns thereturn value of the view or error handler. This does not have tobe a response object. In order to convert the return value to aproper response object, call :func:`make_response`."""req = _request_ctx_stack.top.request # 獲取當前請求的信息if req.routing_exception is not None:self.raise_routing_exception(req)rule = req.url_rule # 獲取到 url 對象# if we provide automatic options for this URL and the# request came with the OPTIONS method, reply automaticallyif getattr(rule, 'provide_automatic_options', False) \and req.method == 'OPTIONS':return self.make_default_options_response() # 從 view_function 中找到endpoint對應的# otherwise dispatch to the handler for that endpoint # view_func 函數,通過視圖參數調用它并返回結果,return self.view_functions[rule.endpoint](**req.view_args) # 注意這里返回的并非響應對象。def finalize_request(self, rv, from_error_handler=False):"""Given the return value from a view function this finalizesthe request by converting it into a response and invoking thepostprocessing functions. This is invoked for both normalrequest dispatching as well as error handlers.Because this means that it might be called as a result of afailure a special safe mode is available which can be enabledwith the `from_error_handler` flag. If enabled, failures inresponse processing will be logged and otherwise ignored.:internal:"""response = self.make_response(rv) # 視圖函數的返回結果被傳入了這里,并轉化成響應對象try: # 關于這個 response 對象,這里就不往下繼續了,下面response = self.process_response(response) # 已經很抽象了,我覺得了解到這里即可。request_finished.send(self, response=response)except Exception:if not from_error_handler:raiseself.logger.exception('Request finalizing failed with an ''error while handling an error')return response
總結:flask 實例通過請求的 URL 來查找對應的 endpoint,再通過它來查找到對應的視圖函數 view_func,然后傳入視圖函數的參數進行請求處理。在調用視圖函數之前,它已經把請求上下文推入了,所以我們在視圖函數中可以自由的使用它們,這就是 flask 處理一個請求的大致過程。
關于請求上下文中的全局變量,也就是 request
這些的使用,可以閱讀這篇博客:
對 flask 框架中的全局變量 request 探究