社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

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

GoT阳仔 • 6 年前 • 115 次点击  
阅读 56

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

关于我
编程界的一名小小程序猿,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。
Github: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 学习资料

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/36436
 
115 次点击