Back to home

Jinja2 macro 和 call

Jinja2 macro 和 call

简介

Jinja2 作为模板引擎提供了丰富的内置语法为渲染逻辑提供支持。碰到一个需求,对于一大段文本内部进行拆分,然后插入模板中定义的一小段HTML。直觉是自定义一个Filter再进行调用。但是Filter调用的语法不支持HTML片段(内部实现均为一个Python函数,所以执行环境是支持的,只是语法规则限制)。

Macro

宏(Macro)作为很多语言的内置功能,能够很方便的重用一段程序代码。Jinja2提供类似的功能。

在Jinja2中声明一个Macro:

{% macro input(name, value='', type='text', size=20) -%}
    <input type="{{ type }}" name="{{ name }}" value="{{
        value|e }}" size="{{ size }}">
{%- endmacro %}

然后就能像函数调用一样,进行片段的复用:

<p>{{ input('username') }}</p>
<p>{{ input('password', type='password') }}</p>

如果你需要跨模板文件引用Macro,则需要导入

{% from 'forms.html' import input as input_field %}

和Python很相似?因为每一个Jinja2模板在“编译”后就是一个单独的Python模块。Jinja 会在第一次渲染的时候执行这个翻译。模板渲染只是字符串拼接工作,大部分模板框架根据语言本身做足优化。

在官方文档中Marco内部能够使用一些特殊的函数比如caller,可以调用被调用函数,借而实现传递参数。有必要用那么复杂?

Call

在一个Macro中(匿名)调用另外一个Macro。比如渲染一个用户列表:

{% macro dump_users(users) -%}
    <ul>
    {%- for user in users %}
        <li><p>{{ user.username|e }}</p>{{ caller(user) }}</li>
    {%- endfor %}
    </ul>
{%- endmacro %}


{% call(user) dump_users(list_of_user) %}
    <dl>
        <dl>Realname</dl>
        <dd>{{ user.realname|e }}</dd>
        <dl>Description</dl>
        <dd>{{ user.description }}</dd>
    </dl>
{% endcall %}
  1. 首先调用 dump_users(list_of_user)
  2. dump_users 中获得模板框架,然后针对列表中的元素再调用上层
  3. 上层(也就是调用dump_users的Macro)得到元素,然后分别渲染

这里你会直接问,为什么不直接将循环中解决?确实是因为例子不够好。

那我们来一个非Call不可的例子。

有记 是一个依托云笔记平台的个人博客站点。内容大部分为笔记内容。可是目前用户一般都是将自己采集到笔记中内容呈现,而不是原创的。这里的产品问题姑且不讨论,技术上产生了一个问题,采集的文章通常是包含标题的,如果直接渲染标题,会使得页面上有多个标题,有碍观瞻。

8af3e3405ad9997c24cd6ed6bc4090f6.png

解决起来还算好办,查内容中是否有和标题一样的<h*>标记,如果有就不显示了。但是原来标题下面的时间和标签怎么办?拆出来插进去?但是这些元素和模板绑定,并不是统一的。

我们需要一种能够在模板中定义的片段,通过模板调用,插入到原有的大段字符串中。

Jinja2的Macro和Call能够提供这样的功能,那么问题是如何在模板中实现这段逻辑?
还记得我说Jinja2会将模板编译为Python模块吗?这些插件(Filter,BuildIn Fuction)实际上都是Jinja2的全局函数,所以我们可以在Python代码中提供这个功能。

{% call(before, after) title_slice(ct, title) -%}
<div class="header bounder">
  {{ before|safe }}
  <h1>{{ note.title }}</h1>
  <div class="post-date"><span>{{ note.date_created|date("%d %b %Y") }}</span></div>
  <div class="tags">{% for tag in note.tags %}<a>{{tag}}</a>{% endfor %}</div>
 {{ after|safe }}
<div>
{%- endcall %}
re_title = re.compile(r"<h\d[^>]*?\>(?P<title>[^<]+)</h\d>", re.I|re.M)
def title_slice(html, title, caller):
    logger.debug(html, title)
    html_len = len(html)
    title_matcher = re_title.search(html, 0, html_len/3)
    before, after = "", html
    if title_matcher and title_matcher.group("title").strip() == title:
        span_start, span_end = title_matcher.span()
        before = html[0:span_start]
        after = html[span_end:]
    return Markup(caller(before, after))

最后在Jinja2环境实例中注册这个函数app.add_template_global(title_slice, "title_slice”)

所有Macro函数需要返回字符串,否则会导致渲染失败,因为最后都会拼接到一个大字符串中去(u’’.join(html_lists))。

结论

模板引擎,其实都是Python代码,比较来说,Jinja封装的更高。这是对我的前同事说的。当时正为模板引擎的选型争论不休。我推荐另外一款更为轻量级的pyTenjin。优点是它就是Python字符串拼接,API非常粗糙,但是几乎没有学习成本。缺点是语法丑陋,编辑器高亮不支持,而且多语句代码嵌套竟然也有缩进问题(因为只是字符串拼接)。同事认为可能会写逻辑代码,导致模板难用。

他们倾向于用工具解决问题。我则认为这都是人的问题。团队内部对于分层没有一个统一的认识,无论用什么样的框架,最后结果只能是混乱不堪,再推倒重来。