************* flask流程详解 ************* ====== 流程图 ====== .. image:: _static/037DA780-F8DA-47EF-AE7E-3D10A9380CD1.png ======== 问题汇总 ======== 1. 为什么能够 ``from flask import Flask`` ,明明Flask定义在flask.app里面? | 答:因为init模块import了Flask,这样对于使用者而言简单明了,因为使用者不需要明白flask里面有什么模块,要什么都从flask里面引入就行了。 | 这又引出了一条包的设计原则,如果你设计的是一个供人使用的工具,那么如果没有必要(需要声明这个工具的归属类别),就将所有的开放工具全部定义在init当中,这样可用性更强 2. signal究竟有什么用途 3. ``url_adapter`` 如何运作 4. 为何最终要返回一个迭代器 5. 这么多代理究竟起到的作用是什么 6. logger究竟是如何初始化的,完全看不明白 ======== 后续版本 ======== 1. 详细解读一个flask app的初始化过程 **Done** 2. 详细解读一个request的封装过程 3. 使用blueprint会如何 4. 使用应用内跳转会如何 5. 渲染模板如何实现 ====== 初始化 ====== 1. 巨长无比的init方法,把注释部分全部精简掉了 .. code-block:: python 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 + '/', endpoint='static', view_func=self.send_static_file) self.cli = cli.AppGroup(self.name) 2. 先讲第一个部分 | (1)首先flask初始化的参数来解释一下,import_name是指app的名称,两个path倒是不太清楚,两个folder就是文件夹的名称了,默认应当是在app下的,后面乱七八糟的path我也不知道什么意思 | (2)之后是调用父类的init方法,这个父类直觉上来看应该是用来将app与包绑定的 | (3)后面就是对各种path进行初始化,如果不是None就使用默认的配置 .. code-block:: python 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.') 3. 继续将第二部分,配置初始化 | (1)调用make_config方法创建一个配置对象 | (2)默认配置来自于self.default_config,根据代码可以看到,都是非常基础的配置项 .. code-block:: python #: 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, }) 4. 初始化logger,值得一提的是logger,这个self._logger是如何创建的呢?噢噢,明白了,其实_logger一直是None,当self.logger这个descriptor被调用的时候_logger才真正创建。**可能的原因是线程锁的关系,暂时先不管,等了解了线程锁之后再考虑这个问题** 5. 初始化各种类型的处理器,都是一个列表用来存储处理函数 6. 初始化blueprints、extensions以及url_map 7. 对于static文件夹注册一条url_rule .. code-block:: python # 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 + '/', endpoint='static', view_func=self.send_static_file) 8. 整个过程感觉上还是比较简单的,除了logger的初始化没有什么令人费解的地方 ======== 最简版本 ======== 代码如下 -------- .. code:: python 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方法,传入标准的 ``environ`` 和 ``start_response`` 回调 详解:environ变量本身是一个dict,包含所有请求的信息 .. code:: python def __call__(self, environ, start_response): """Shortcut for :attr:`wsgi_app`.""" return self.wsgi_app(environ, start_response) 3. call方法调用 ``app.wsgi_app`` 方法 4. 方法内部,首先生成一个 **请求上下文对象**,这个对象封装了请求信息,并且是一个上下文对象(新版本上下文对象这个特性没什么用了,因为是直接调用push方法的) .. code:: python >>> ctx = self.request_context(environ) ctx.push() 5. 这个上下文的内部存储了如下几个重要的变量 其中 ``self.request`` 是通过 ``request_class`` (默认的就是Request类,继承至werkzeug的BaseRequest类)这个类进行实例化产生的 而 ``self.url_adapter`` 是一个绑定了请求信息的url适配器(MapAdpter),**具体情况下面说** .. code:: python 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 6. 调用该上下文对象的push方法 .. code:: python ctx = self.request_context(environ) >>> ctx.push() 7. 这里要好好讲一下 a. ``_request_ctx_stack`` 是一个werkzeug当中的LocalStack对象,其可以看成是一个存储了请求上下文的栈,通过top方法可以查看栈顶元素,所以第一段就是查看栈顶是否有元素(请求上下文),如果有的话将其pop掉 #. 下一步查看应用上下文栈当中是否有应用上下文,如果其中没有应用上下文,或者有应用上下文但是不是当前应用的上下文,那么久将当前应用的上下文压栈 #. 接下来将当前这个请求上下文(self)压如请求上下文栈 #. 最后一步是从请求的cookies当中加载session,使得session变量可用 .. code:: python 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() 8. 有一个问题: ``_request_ctx_stack`` 作为一个全局变量,如何能只保存当前线程的上下文呢?其实看看他是如何存储上下文对象的就知道了,进程当中所有的上下文对象都存储在 ``_local`` 这个属性当中,而这个属性是一个Local对象,看看其内部实现 a. 初始化Local的时候设置了两个类变量(为什么给父类设置,这里不太懂),一个是 ``__storage__ = {}`` ,另外一个是 ``__indent_func__ = get_ident`` #. 当要从Local当中取值 ``stack`` 的时候,由于Local实例本身没有该属性,因此会调用 ``__getattr__`` 方法,从 ``self.__storage__[self.__ident_func__()]`` 当中取,而 ``self.__ident_func__()`` 会返回一个代表当前线程的一个整数,所以你在这个线程当中只能取这个线程的上下文 .. code:: python 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) 9. 用一个实例来说明一下上文的问题,pycharm不知道怎么回事,看不到Local的内部,使用python解释器看一下 看下下面这个代码片段就知道了, ``app_context`` 是可以重复压栈的,开头那个数字指的是线程号 .. code:: python In [15]: app1.app_context().push() In [16]: ctx_storage Out[16]: {140735911060416: {'stack': [, , ]}} In [17]: 新开一个线程进行压栈,会发现有两个线程好,足以说明这个问题了 .. code:: python In [20]: Thread(target=start_app).start() In [21]: ctx_storage Out[21]: {123145445822464: {'stack': []}, 140735911060416: {'stack': [, , ]}} 10. 顺着第七条继续往下说 接下来所有的流程全部在这个try finally语句当中实现,首先是调用 ``full_dispatch_request`` 方法,这个方法会调用视图函数对请求进行处理,先进入这个函数看看 .. code:: python 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)如果这些函数没有返回值,那么久进入该语句,这里做的就是真正的请求分发了,进到里面去看看 .. code:: python 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) 12. 进入 ``dispatch_request`` 的内部 a. 根据在请求上下文当中存储的请求信息进行处理 #. 这个 ``routing_exception`` 先不管 #. 这个request中包含了 ``url_rule`` ,其实应该是来自app当中的,**这个request如何形成的需要分析一下** #. 根据endpoint在注册是 ``view_functions`` 当中进行调用,并传递 ``view_args`` ,返回调用结果 .. code:: python 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) 13. 再回到 ``full_dispatch_request`` 当中,如果之前的try块引发任何异常的话,都会通过 ``flask.handle_user_exception`` 来处理异常。内部如果有对定义专门的异常处理器的话会用该处理器,没有的话则会用默认的异常处理器进行处理 .. code:: python except Exception as e: >>> rv = self.handle_user_exception(e) return self.finalize_request(rv) 14. 最后调用 ``finalize_request(rv)``,进入内部看一下 a. 可以看到首先会对请求使用make_response方法进行处理,该方法会将视图函数的返回值(response对象或者一个元组)封装成一个response对象 #. 调用 ``process_response`` 方法,使用 ``after_request_functions`` 当中的函数进行处理,返回处理过后的response #. 如果抛出异常,raise .. code:: python 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 15. ``finalize_request(rv)`` 返回一个response对象,所以最终不管怎么样 ``full_dispatch_request`` 函数总会返回一个response对象,或者从 ``finalize_request`` 当中抛出异常 a. ``wsgi_app`` 当中会最终处理任何抛出的异常,并产生一个新的请求 #. 这个是用来捕捉非普通异常,比如KeyboardInterrupt,SystemExit之类的,可以直接退出 #. 如果不是产生了Exception级别以上的异常,那么都会产生一个response对象,最后一步是调用这个response的 ``__call__`` 方法 #. response的call方法封装了标准的wsgi接口,调用了 ``start_response`` 并返回一个迭代器(可能是因为性能比较好吧,我该看看wsgi的文档了 #. 不管如何,请求上下文都要出栈 .. code:: python 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)