• Python Web Flask源码解读(三)——模板渲染过程


    关于我
    一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。
    Github:https://github.com/hylinux1024
    微信公众号:终身开发者(angrycode)

    前面对Flask启动流程路由原理都进行了源码走读。今天我们看看模板渲染的过程。

    0x00 使用模板

    首先看一个来自官方文档使用模板渲染的例子

    from flask import render_template
    
    @app.route('/hello/')
    @app.route('/hello/<name>')
    def hello(name=None):
        return render_template('hello.html', name=name)
    

    在项目目录下需要有一个templates目录,并创建了一个hello.html文件

    /templates
        /hello.html
    

    hello.html的内容为

    <!doctype html>
    <title>Hello from Flask</title>
    {% if name %}
      <h1>Hello {{ name }}!</h1>
    {% else %}
      <h1>Hello, World!</h1>
    {% endif %}
    

    这个模板中name是参数,通过调用render_template方法就可以根据参数实现html模板文件的渲染。

    0x01 Flask.render_template

    def render_template(template_name, **context):
        """Renders a template from the template folder with the given
        context.
    
        :param template_name: the name of the template to be rendered
        :param context: the variables that should be available in the
                        context of the template.
        """
        current_app.update_template_context(context)
        return current_app.jinja_env.get_template(template_name).render(context)
    

    方法的注释很清楚,从templates文件夹中找到名称为template_name的文件进行渲染。其中current_app是通过以下语句初始化

    _request_ctx_stack = LocalStack()
    current_app = LocalProxy(lambda: _request_ctx_stack.top.app)
    

    LocalStack就是一个栈的实现类。而_request_ctx_stack是在Flask.request_context()方法中将当前的上下文实例push到栈里面的

    def request_context(self, environ):
        """Creates a request context from the given environment and binds
        it to the current context.  This must be used in combination with
        the `with` statement because the request is only bound to the
        current context for the duration of the `with` block.
    
        Example usage::
    
            with app.request_context(environ):
                do_something_with(request)
    
        :params environ: a WSGI environment
        """
        return _RequestContext(self, environ)
    

    _RequestContext类实现了上下文管理器协议,它可以在with语句中使用

    class _RequestContext(object):
        """The request context contains all request relevant information.  It is
        created at the beginning of the request and pushed to the
        `_request_ctx_stack` and removed at the end of it.  It will create the
        URL adapter and request object for the WSGI environment provided.
        """
    
        def __init__(self, app, environ):
            self.app = app
            self.url_adapter = app.url_map.bind_to_environ(environ)
            self.request = app.request_class(environ)
            self.session = app.open_session(self.request)
            self.g = _RequestGlobals()
            self.flashes = None
    
        def __enter__(self):
            _request_ctx_stack.push(self)
    
        def __exit__(self, exc_type, exc_value, tb):
            # do not pop the request stack if we are in debug mode and an
            # exception happened.  This will allow the debugger to still
            # access the request object in the interactive shell.
            if tb is None or not self.app.debug:
                _request_ctx_stack.pop()
    

    执行__enter__()时操作push,退出with语句时就执行pop操作。
    回到request_context()方法,它是在wsgi_app()中被调用的

    def wsgi_app(self, environ, start_response):
        """The actual WSGI application.  This is not implemented in
        `__call__` so that middlewares can be applied:
    
            app.wsgi_app = MyMiddleware(app.wsgi_app)
    
        :param environ: a WSGI environment
        :param start_response: a callable accepting a status code,
                               a list of headers and an optional
                               exception context to start the response
        """
        with self.request_context(environ):
            rv = self.preprocess_request()
            if rv is None:
                rv = self.dispatch_request()
            response = self.make_response(rv)
            response = self.process_response(response)
            return response(environ, start_response)
    

    路由原理文章的分析知道,wsgi_app()在服务端接收到客户端请求时就会执行。
    所以当请求来临时,就会把当前Flask实例的请求上下文实例保存到栈实例_request_ctx_stack中;请求处理后,就从栈里面弹出当前请求的上下文实例。

    LocalProxy是一个代理类,它的构造函数传递了一个lambda表达式:lambda: _request_ctx_stack.top.app
    这个操作就把当前的上下文实例通过LocalProxy进行了封装,即current_app是当前Flask实例的上下文的代理。
    所以当current_app.jinja_env这个语句其实就是访问Flask的实例属性jinja_env,这个属性是在Flask的构造函数中进行初始化的。

    class Flask(object):
        ...
        #: 源码太长了省略
        #: options that are passed directly to the Jinja2 environment
        jinja_options = dict(
            autoescape=True,
            extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_']
        )
    
        def __init__(self, package_name):
            ...
            #: 源码太长省略部分源码
            #: the Jinja2 environment.  It is created from the
            #: :attr:`jinja_options` and the loader that is returned
            #: by the :meth:`create_jinja_loader` function.
            self.jinja_env = Environment(loader=self.create_jinja_loader(),
                                         **self.jinja_options)
            self.jinja_env.globals.update(
                url_for=url_for,
                get_flashed_messages=get_flashed_messages
            )
    

    jinja_env是一个Environment实例。这个是jinja模板引擎提供的类,Flask框架的模板渲染就是通过jinja来实现的。
    Environment需要一个loader,是通过以下方法获取的

    def create_jinja_loader(self):
        """Creates the Jinja loader.  By default just a package loader for
        the configured package is returned that looks up templates in the
        `templates` folder.  To add other loaders it's possible to
        override this method.
        """
        if pkg_resources is None:
            return FileSystemLoader(os.path.join(self.root_path, 'templates'))
        return PackageLoader(self.package_name)
    

    默认情况下是从templates目录下构造一个FileSystemLoader的实例,这个类的作用就是从文件系统中加载模板文件的。

    0x02 Environment.get_template

    @internalcode
    def get_template(self, name, parent=None, globals=None):
        """Load a template from the loader.  If a loader is configured this
        method ask the loader for the template and returns a :class:`Template`.
        If the `parent` parameter is not `None`, :meth:`join_path` is called
        to get the real template name before loading.
    
        The `globals` parameter can be used to provide template wide globals.
        These variables are available in the context at render time.
    
        If the template does not exist a :exc:`TemplateNotFound` exception is
        raised.
    
        .. versionchanged:: 2.4
           If `name` is a :class:`Template` object it is returned from the
           function unchanged.
        """
        if isinstance(name, Template):
            return name
        if parent is not None:
            name = self.join_path(name, parent)
        return self._load_template(name, self.make_globals(globals))
    

    get_template()方法内部调用了_load_template()方法

    @internalcode
    def _load_template(self, name, globals):
        if self.loader is None:
            raise TypeError('no loader for this environment specified')
        if self.cache is not None:
            template = self.cache.get(name)
            if template is not None and (not self.auto_reload or 
                                         template.is_up_to_date):
                return template
        template = self.loader.load(self, name, globals)
        if self.cache is not None:
            self.cache[name] = template
        return template
    

    _load_template()方法首先会检查是否有缓存,如果缓存可用就使用缓存;缓存不可用就使用loader加载模板,这个loader就是前面提到的FileSystemLoader的实例(默认情况下)。

    0x03 BaseLoader.load

    @internalcode
    def load(self, environment, name, globals=None):
        ...
        # 省略部分源码
        return environment.template_class.from_code(environment, code, globals, uptodate)
    

    BaseLoaderFileSystemLoader的基类。这个load方法实现了模板的编译、加载等逻辑。最后是使用environment.template_class.from_code()方法。其中template_classTemplate类,它代表编译后的模板对象。
    from_codeTemplate类的静态方法,可以用来创建一个Template实例。当load方法返回时,就得到了一个Template对象。
    最后回到render_template方法

    def render_template(template_name, **context):
        ...
        return current_app.jinja_env.get_template(template_name).render(context)
    

    执行了Template对象的render()方法。

    0x04 Template.render

    def render(self, *args, **kwargs):
        """This function accepts either a dict or some keyword arguments which
        will then be the context the template is evaluated in.  The return
        value will be the rendered template.
    
        :param context: the function accepts the same arguments as the
                        :class:`dict` constructor.
        :return: the rendered template as string
        """
        ns = self.default_context.copy()
        if len(args) == 1 and isinstance(args[0], utils.MultiDict):
            ns.update(args[0].to_dict(flat=True))
        else:
            ns.update(dict(*args))
        if kwargs:
            ns.update(kwargs)
        context = Context(ns, self.charset, self.errors)
        exec self.code in context.runtime, context
        return context.get_value(self.unicode_mode)
    

    这个方法接收一个dict类型参数,用于给模板传递参数。该方法的核心是执行exec函数。execPython内置函数,它可以动态的执行Python代码。

    0x05 总结一下

    Flask使用Jinja作为模板引擎。执行路径为

    Flask.render_template => Environment.get_template => Template.render => exec
    

    0x06 学习资料

  • 相关阅读:
    C语言32个关键字详解
    C语言格式控制符
    c++关键字详解
    多码流简介
    Jtag管脚定义
    关于RGB信号的电平
    缩略语MSPS
    【转】松下18650的容量判别方法
    电信号在FR4材料中的传播速度
    dropout voltage
  • 原文地址:https://www.cnblogs.com/angrycode/p/11449210.html
Copyright © 2020-2023  润新知