flask流程详解

流程图

../_images/037DA780-F8DA-47EF-AE7E-3D10A9380CD1.png

问题汇总

  1. 为什么能够 from flask import Flask ,明明Flask定义在flask.app里面?
答:因为init模块import了Flask,这样对于使用者而言简单明了,因为使用者不需要明白flask里面有什么模块,要什么都从flask里面引入就行了。
这又引出了一条包的设计原则,如果你设计的是一个供人使用的工具,那么如果没有必要(需要声明这个工具的归属类别),就将所有的开放工具全部定义在init当中,这样可用性更强
  1. signal究竟有什么用途
  2. url_adapter 如何运作
  3. 为何最终要返回一个迭代器
  4. 这么多代理究竟起到的作用是什么
  5. logger究竟是如何初始化的,完全看不明白

后续版本

  1. 详细解读一个flask app的初始化过程 Done
  2. 详细解读一个request的封装过程
  3. 使用blueprint会如何
  4. 使用应用内跳转会如何
  5. 渲染模板如何实现

初始化

  1. 巨长无比的init方法,把注释部分全部精简掉了
def __init__(self, import_name, static_path=None, static_url_path=None,
         static_folder='static', template_folder='templates',
         instance_path=None, instance_relative_config=False,
         root_path=None):
_PackageBoundObject.__init__(self, import_name,
                             template_folder=template_folder,
                             root_path=root_path)
if static_path is not None:
    from warnings import warn
    warn(DeprecationWarning('static_path is now called '
                            'static_url_path'), stacklevel=2)
    static_url_path = static_path

if static_url_path is not None:
    self.static_url_path = static_url_path
if static_folder is not None:
    self.static_folder = static_folder
if instance_path is None:
    instance_path = self.auto_find_instance_path()
elif not os.path.isabs(instance_path):
    raise ValueError('If an instance path is provided it must be '
                     'absolute.  A relative path was given instead.')

self.instance_path = instance_path

self.config = self.make_config(instance_relative_config)

self._logger = None
self.logger_name = self.import_name

self.view_functions = {}

self._error_handlers = {}

self.error_handler_spec = {None: self._error_handlers}

self.url_build_error_handlers = []

self.before_request_funcs = {}

self.before_first_request_funcs = []

self.after_request_funcs = {}

self.teardown_request_funcs = {}

self.teardown_appcontext_funcs = []

self.url_value_preprocessors = {}

self.url_default_functions = {}

self.template_context_processors = {
    None: [_default_template_ctx_processor]
}

self.shell_context_processors = []

self.blueprints = {}
self._blueprint_order = []

self.extensions = {}

self.url_map = Map()

self._got_first_request = False
self._before_request_lock = Lock()

if self.has_static_folder:
    self.add_url_rule(self.static_url_path + '/<path:filename>',
                      endpoint='static',
                      view_func=self.send_static_file)

self.cli = cli.AppGroup(self.name)
  1. 先讲第一个部分
(1)首先flask初始化的参数来解释一下,import_name是指app的名称,两个path倒是不太清楚,两个folder就是文件夹的名称了,默认应当是在app下的,后面乱七八糟的path我也不知道什么意思
(2)之后是调用父类的init方法,这个父类直觉上来看应该是用来将app与包绑定的
(3)后面就是对各种path进行初始化,如果不是None就使用默认的配置
def __init__(self, import_name, static_path=None, static_url_path=None,
         static_folder='static', template_folder='templates',
         instance_path=None, instance_relative_config=False,
         root_path=None):
_PackageBoundObject.__init__(self, import_name,
                             template_folder=template_folder,
                             root_path=root_path)
if static_path is not None:
    from warnings import warn
    warn(DeprecationWarning('static_path is now called '
                            'static_url_path'), stacklevel=2)
    static_url_path = static_path

if static_url_path is not None:
    self.static_url_path = static_url_path
if static_folder is not None:
    self.static_folder = static_folder
if instance_path is None:
    instance_path = self.auto_find_instance_path()
elif not os.path.isabs(instance_path):
    raise ValueError('If an instance path is provided it must be '
                     'absolute.  A relative path was given instead.')
  1. 继续将第二部分,配置初始化
(1)调用make_config方法创建一个配置对象
(2)默认配置来自于self.default_config,根据代码可以看到,都是非常基础的配置项
#: The configuration dictionary as :class:`Config`.  This behaves
#: exactly like a regular dictionary but supports additional methods
#: to load a config from files.
self.config = self.make_config(instance_relative_config)

default_config = ImmutableDict({
'DEBUG':                                get_debug_flag(default=False),
'TESTING':                              False,
'PROPAGATE_EXCEPTIONS':                 None,
'PRESERVE_CONTEXT_ON_EXCEPTION':        None,
'SECRET_KEY':                           None,
'PERMANENT_SESSION_LIFETIME':           timedelta(days=31),
'USE_X_SENDFILE':                       False,
'LOGGER_NAME':                          None,
'LOGGER_HANDLER_POLICY':               'always',
'SERVER_NAME':                          None,
'APPLICATION_ROOT':                     None,
'SESSION_COOKIE_NAME':                  'session',
'SESSION_COOKIE_DOMAIN':                None,
'SESSION_COOKIE_PATH':                  None,
'SESSION_COOKIE_HTTPONLY':              True,
'SESSION_COOKIE_SECURE':                False,
'SESSION_REFRESH_EACH_REQUEST':         True,
'MAX_CONTENT_LENGTH':                   None,
'SEND_FILE_MAX_AGE_DEFAULT':            timedelta(hours=12),
'TRAP_BAD_REQUEST_ERRORS':              False,
'TRAP_HTTP_EXCEPTIONS':                 False,
'EXPLAIN_TEMPLATE_LOADING':             False,
'PREFERRED_URL_SCHEME':                 'http',
'JSON_AS_ASCII':                        True,
'JSON_SORT_KEYS':                       True,
'JSONIFY_PRETTYPRINT_REGULAR':          True,
'JSONIFY_MIMETYPE':                     'application/json',
'TEMPLATES_AUTO_RELOAD':                None,
})
  1. 初始化logger,值得一提的是logger,这个self._logger是如何创建的呢?噢噢,明白了,其实_logger一直是None,当self.logger这个descriptor被调用的时候_logger才真正创建。可能的原因是线程锁的关系,暂时先不管,等了解了线程锁之后再考虑这个问题
  2. 初始化各种类型的处理器,都是一个列表用来存储处理函数
  3. 初始化blueprints、extensions以及url_map
  4. 对于static文件夹注册一条url_rule
# register the static folder for the application.  Do that even
# if the folder does not exist.  First of all it might be created
# while the server is running (usually happens during development)
# but also because google appengine stores static files somewhere
# else when mapped with the .yml file.
if self.has_static_folder:
    self.add_url_rule(self.static_url_path + '/<path:filename>',
                      endpoint='static',
                      view_func=self.send_static_file)
  1. 整个过程感觉上还是比较简单的,除了logger的初始化没有什么令人费解的地方

最简版本

代码如下

from flask import Flask

app = Flask(__name__)


@app.route('/')
def index():
    return 'hello world'


if __name__ == '__main__':
    app.run()

运行流程

  1. app初始化
  2. HTTP服务器通过WSGI接口调用app的call方法,传入标准的 environstart_response 回调 详解:environ变量本身是一个dict,包含所有请求的信息
def __call__(self, environ, start_response):
    """Shortcut for :attr:`wsgi_app`."""
    return self.wsgi_app(environ, start_response)
  1. call方法调用 app.wsgi_app 方法
  2. 方法内部,首先生成一个 请求上下文对象,这个对象封装了请求信息,并且是一个上下文对象(新版本上下文对象这个特性没什么用了,因为是直接调用push方法的)
>>> ctx = self.request_context(environ)
    ctx.push()
  1. 这个上下文的内部存储了如下几个重要的变量 其中 self.request 是通过 request_class (默认的就是Request类,继承至werkzeug的BaseRequest类)这个类进行实例化产生的 而 self.url_adapter 是一个绑定了请求信息的url适配器(MapAdpter),具体情况下面说
def __init__(self, app, environ, request=None):
    self.app = app
    if request is None:
        request = app.request_class(environ)
    self.request = request
    self.url_adapter = app.create_url_adapter(self.request)
    self.flashes = None
    self.session = None
  1. 调用该上下文对象的push方法
    ctx = self.request_context(environ)
>>> ctx.push()
  1. 这里要好好讲一下
  1. _request_ctx_stack 是一个werkzeug当中的LocalStack对象,其可以看成是一个存储了请求上下文的栈,通过top方法可以查看栈顶元素,所以第一段就是查看栈顶是否有元素(请求上下文),如果有的话将其pop掉
  2. 下一步查看应用上下文栈当中是否有应用上下文,如果其中没有应用上下文,或者有应用上下文但是不是当前应用的上下文,那么久将当前应用的上下文压栈
  3. 接下来将当前这个请求上下文(self)压如请求上下文栈
  4. 最后一步是从请求的cookies当中加载session,使得session变量可用
top = _request_ctx_stack.top
if top is not None and top.preserved:
    top.pop(top._preserved_exc)

# Before we push the request context we have to ensure that there
# is an application context.
app_ctx = _app_ctx_stack.top
if app_ctx is None or app_ctx.app != self.app:
    app_ctx = self.app.app_context()
    app_ctx.push()
    self._implicit_app_ctx_stack.append(app_ctx)
else:
    self._implicit_app_ctx_stack.append(None)

if hasattr(sys, 'exc_clear'):
    sys.exc_clear()

_request_ctx_stack.push(self)

# Open the session at the moment that the request context is
# available. This allows a custom open_session method to use the
# request context (e.g. code that access database information
# stored on `g` instead of the appcontext).
self.session = self.app.open_session(self.request)
if self.session is None:
    self.session = self.app.make_null_session()
  1. 有一个问题: _request_ctx_stack 作为一个全局变量,如何能只保存当前线程的上下文呢?其实看看他是如何存储上下文对象的就知道了,进程当中所有的上下文对象都存储在 _local 这个属性当中,而这个属性是一个Local对象,看看其内部实现
  1. 初始化Local的时候设置了两个类变量(为什么给父类设置,这里不太懂),一个是 __storage__ = {} ,另外一个是 __indent_func__ = get_ident
  2. 当要从Local当中取值 stack 的时候,由于Local实例本身没有该属性,因此会调用 __getattr__ 方法,从 self.__storage__[self.__ident_func__()] 当中取,而 self.__ident_func__() 会返回一个代表当前线程的一个整数,所以你在这个线程当中只能取这个线程的上下文
class Local(object):
    __slots__ = ('__storage__', '__ident_func__')

    def __init__(self):
        object.__setattr__(self, '__storage__', {})
        object.__setattr__(self, '__ident_func__', get_ident)

    def __iter__(self):
        return iter(self.__storage__.items())

    def __call__(self, proxy):
        """Create a proxy for a name."""
        return LocalProxy(self, proxy)

    def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)

    def __getattr__(self, name):
        try:
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

    def __delattr__(self, name):
        try:
            del self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)
  1. 用一个实例来说明一下上文的问题,pycharm不知道怎么回事,看不到Local的内部,使用python解释器看一下 看下下面这个代码片段就知道了, app_context 是可以重复压栈的,开头那个数字指的是线程号
In [15]: app1.app_context().push()

In [16]: ctx_storage
Out[16]:
{140735911060416: {'stack': [<flask.ctx.AppContext at 0x10ad69160>,
   <flask.ctx.AppContext at 0x10b607550>,
   <flask.ctx.AppContext at 0x10b8c52e8>]}}

In [17]:

新开一个线程进行压栈,会发现有两个线程好,足以说明这个问题了

In [20]: Thread(target=start_app).start()

In [21]: ctx_storage
Out[21]:
{123145445822464: {'stack': [<flask.ctx.AppContext at 0x10b86f160>]},
 140735911060416: {'stack': [<flask.ctx.AppContext at 0x10ad69160>,
   <flask.ctx.AppContext at 0x10b607550>,
   <flask.ctx.AppContext at 0x10b8c52e8>]}}
  1. 顺着第七条继续往下说 接下来所有的流程全部在这个try finally语句当中实现,首先是调用 full_dispatch_request 方法,这个方法会调用视图函数对请求进行处理,先进入这个函数看看
        ctx = self.request_context(environ)
        ctx.push()
        error = None
        try:
            try:
>>>             response = self.full_dispatch_request()
            except Exception as e:
                error = e
                response = self.handle_exception(e)
            except:
                error = sys.exc_info()[1]
                raise
            return response(environ, start_response)
        finally:
            if self.should_ignore_error(error):
                error = None
            ctx.auto_pop(error)

11. 进入 full_dispatch_request() 内部 先看看注释,说是分发请求,并且在这基础之上进行了请求的预处理以及请求后的异常捕获和错误处理工作,所以说是full,因此不仅仅是分发嘛

(1)这里是调用 flask.before_first_request 当中的函数,如果不是 first_request 内部会做判断,不必担心
(2)这里是发送一个 reqeust_started 信号(signal),之后再说
(3)这里是调用 flask.preprocess_request 方法,里面其实就是执行一遍 flask.url_value_preprocessors 以及 flask.before_request_funcs 当中的函数,blueprint那部分先不管
(4)如果这些函数没有返回值,那么久进入该语句,这里做的就是真正的请求分发了,进到里面去看看
def full_dispatch_request(self):
    """Dispatches the request and on top of that performs request
    pre and postprocessing as well as HTTP exception catching and
    error handling.

    .. versionadded:: 0.7
    """
    self.try_trigger_before_first_request_functions()                   (1)
    try:
        request_started.send(self)                                      (2)
        rv = self.preprocess_request()                                  (3)
        if rv is None:
            rv = self.dispatch_request()                                (4)
    except Exception as e:
        rv = self.handle_user_exception(e)
    return self.finalize_request(rv)
  1. 进入 dispatch_request 的内部
  1. 根据在请求上下文当中存储的请求信息进行处理
  2. 这个 routing_exception 先不管
  3. 这个request中包含了 url_rule ,其实应该是来自app当中的,这个request如何形成的需要分析一下
  4. 根据endpoint在注册是 view_functions 当中进行调用,并传递 view_args ,返回调用结果
def dispatch_request(self):
    """Does the request dispatching.  Matches the URL and returns the
    return value of the view or error handler.  This does not have to
    be a response object.  In order to convert the return value to a
    proper response object, call :func:`make_response`.

    .. versionchanged:: 0.7
       This no longer does the exception handling, this code was
       moved to the new :meth:`full_dispatch_request`.
    """
    req = _request_ctx_stack.top.request                               (1)
    if req.routing_exception is not None:                              (2)
        self.raise_routing_exception(req)
    rule = req.url_rule                                                (3)
    # if we provide automatic options for this URL and the
    # request came with the OPTIONS method, reply automatically
    if getattr(rule, 'provide_automatic_options', False) \
       and req.method == 'OPTIONS':
        return self.make_default_options_response()
    # otherwise dispatch to the handler for that endpoint
    return self.view_functions[rule.endpoint](**req.view_args)         (4)
  1. 再回到 full_dispatch_request 当中,如果之前的try块引发任何异常的话,都会通过 flask.handle_user_exception 来处理异常。内部如果有对定义专门的异常处理器的话会用该处理器,没有的话则会用默认的异常处理器进行处理
        except Exception as e:
>>>         rv = self.handle_user_exception(e)
        return self.finalize_request(rv)
  1. 最后调用 finalize_request(rv),进入内部看一下
  1. 可以看到首先会对请求使用make_response方法进行处理,该方法会将视图函数的返回值(response对象或者一个元组)封装成一个response对象
  2. 调用 process_response 方法,使用 after_request_functions 当中的函数进行处理,返回处理过后的response
  3. 如果抛出异常,raise
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.

    Because this means that it might be called as a result of a
    failure a special safe mode is available which can be enabled
    with the `from_error_handler` flag.  If enabled, failures in
    response processing will be logged and otherwise ignored.

    :internal:
    """
    response = self.make_response(rv)                               (1)
    try:
        response = self.process_response(response)                  (2)
        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
  1. finalize_request(rv) 返回一个response对象,所以最终不管怎么样 full_dispatch_request 函数总会返回一个response对象,或者从 finalize_request 当中抛出异常
  1. wsgi_app 当中会最终处理任何抛出的异常,并产生一个新的请求
  2. 这个是用来捕捉非普通异常,比如KeyboardInterrupt,SystemExit之类的,可以直接退出
  3. 如果不是产生了Exception级别以上的异常,那么都会产生一个response对象,最后一步是调用这个response的 __call__ 方法
  4. response的call方法封装了标准的wsgi接口,调用了 start_response 并返回一个迭代器(可能是因为性能比较好吧,我该看看wsgi的文档了
  5. 不管如何,请求上下文都要出栈
try:
    try:
        response = self.full_dispatch_request()
    except Exception as e:
        error = e
        response = self.handle_exception(e)                         (1)
    except:
        error = sys.exc_info()[1]
        raise                                                       (2)
    return response(environ, start_response)                        (3)
finally:
    if self.should_ignore_error(error):
        error = None
    ctx.auto_pop(error)                                             (4)