使用 jinja2 渲染 HTML 模板

前言

首先,如果一个正常的flask带路由的接口,我们是不需要关心上下文对象的,Flask做了很多“魔术”的方法,当一个Flask应用接收到一个请求的时候,它会在将逻辑委托给你的视图函数之前,创建好一个上下文对象。

当我们返回的时候调用render_template(template, **context),就可以正常的渲染界面返回,在这个函数中,如果看一下源码就会发现,返回渲染之前,会创建一个ctx去获得当前环境的app变量。然后通过这个ctx去渲染传进来的context参数列表。

def render_template(template_name_or_list, **context):
    """Renders a template from the template folder with the given
    context.

    :param template_name_or_list: the name of the template to be
                                  rendered, or an iterable with template names
                                  the first one existing will be rendered
    :param context: the variables that should be available in the
                    context of the template.
    """
    ctx = _app_ctx_stack.top
    ctx.app.update_template_context(context)
    return _render(ctx.app.jinja_env.get_or_select_template(template_name_or_list),
                   context, ctx.app)

基于之前知道有这个函数可以替换html中的模板变量,今天在写代码的过程中,需要将一个html文件中变量动态更新后通过邮件发送走。出于惯性思维,理所当然的就直接撸代码:

from flask import render_template
# 要更新模板中的用户名和密码,返回要邮件发送的内容
message = render_template("email.html", name="xxx", password='xxx')

一运行结果就报错:

Traceback (most recent call last):
  File "xxx.py", line 14, in <module>
    message = render_template("email.html", name="xxx", password='xxx'))
  File "/usr/local/lib/python2.7/dist-packages/flask/templating.py", line 123, in render_template
    ctx.app.update_template_context(context)
AttributeError: 'NoneType' object has no attribute 'app'

经过一番摸索,大概知道这个ctx为嘛是个None,在werkzeug/local.py中有说明:

@property
def top(self):
    """The topmost item on the stack.  If the stack is empty,
    `None` is returned.
    """
    try:
        return self._local.stack[-1]
    except (AttributeError, IndexError):
        return None

果不其然,在这段代码中,我直接调用了render_template(),并没有通过Flask路由,所以,上下文对象并没有被创建。而render_template()尝试去通过ctx获取当前应用的app对象,所以ctx被赋值为None,因而出现上面的报错信息。

提出问题

这个问题该如何解决呢?我在这个地方,并不需要创建也不应该创建一个flask应用,但我又想使用jinja2的模板语言去更新数据。

解决问题

首先,发生这个错误,是因为Flask帮我们代理了jinja2的渲染动作,提供出一些更适合开发者使用的接口。也就是Flask把jinja2藏在了它内部,包装了一下,我们想直接用里面的东西,Flask就不干了。明白人都看得出来,一切都是Flask引起的,负全责,jinja2是清白的。既然这样,我们的解决方法就是拨调flask的包装, 直接去调用jinja2的渲染机制,这个过程就需要我们自己来写个小函数实现渲染功能。

示例代码如下:

import jinja2

def render_without_request(template_name, **context):
    """
    用法同 flask.render_template:

    render_without_request('template.html', var1='foo', var2='bar')
    """
    env = jinja2.Environment(
        loader=jinja2.PackageLoader('package','templates')
    )
    template = env.get_template(template_name)
    return template.render(**context)

这个函数假设你的应用程序文件结构普通的那种。具体来说,你的项目结构应该是这个样子:

package/
├── __init__.py <--- init文件必须保留,python包的标志
└── templates/
    └── template.html

这里需要主要就是关注一下jinja2的PackageLoader这个函数,设计到文件之间的引用关系,可以参考一下源码,在jinjia2/loaders.py文件:

class PackageLoader(BaseLoader):
    """Load templates from python eggs or packages.  It is constructed with
    the name of the python package and the path to the templates in that
    package::

        loader = PackageLoader('mypackage', 'views')

    If the package path is not given, ``'templates'`` is assumed.

    Per default the template encoding is ``'utf-8'`` which can be changed
    by setting the `encoding` parameter to something else.  Due to the nature
    of eggs it's only possible to reload templates if the package was loaded
    from the file system and not a zip file.
    """

初始化这个类的时候,第一个参数是模板所在的目录包名,第二个是模板目录名,可选,默认为templates,第三个可选参数是encoding,默认为utf-8

至此我们这个函数写好以后,就可以直接像flask.render_template()一样来调用了,它可以单纯的帮助我们把html模板渲染出来啦!

单纯多美好~~^.^

本文章首发在 PythonCaff

To be a full stack man.