Skip to content

块和宏

考虑到模板代码的重用,Jinja2 提供了块 (Block) 和宏 (Macro) 的功能。块功能有些类似于 C 语言中的宏,原理就是代码替换;而宏的功能有些类似于函数,可以传入参数。本篇我们就来介绍下块和宏的用法。

块 (Block)

模板也可以使用继承。首先在父元素定义块({% block block_name %}) ,然后在子模板的开头定义 {% extend 'parent.html' %} 语句来声明继承,此后在子模板中由 {% block block_name %}{% endblock %} 所包括的语句块,将会替换父模板中同样由 {% block block_name %}{% endblock %} 所包括的部分。

这就是块的功能,模板语句的替换。这里要注意几个点:

  1. 模板不支持多继承,也就是子模板中定义的块,不可能同时被两个父模板替换。
  2. 模板中不能定义多个同名的块,子模板和父模板都不行,因为这样无法知道要替换哪一个部分的内容。

另外,我们建议在 endblock 关键字后也加上块名,比如 {% endblock block_name %}。虽然对程序没什么作用,但是当有多个块嵌套时,可读性好很多。

封装模版基类

如果想让子页面可以继承父页面进行使用,则需要先将父页面的内容抽取为块。

html

<body>
<div class="header">头部区域</div>
<div class="main">
    <div class="left">左侧区域</div>
    <div class="center">中间区域</div>
    <div class="right">右侧区域</div>
</div>
<div class="footer">底部区域</div>
</body>
html

<style>
    * {
        padding: 0;
        margin: 0;
    }

    .header {
        height: 60px;
        background-color: lightskyblue;
    }

    .main {
        display: flow;
        flex-direction: column;
        width: 100%;
        height: 400px;
        background-color: cyan;
    }

    .left, .center, .right {
        display: inline-block;
        height: 100%;
    }

    .center {
        width: 49%;
    }

    .right, .left {
        width: 25%;
        background-color: lightyellow;
    }

    .footer {
        height: 60px;
        background-color: lightskyblue;
    }
</style>

保留父模板块的内容

使用 {% extend 'parent.html' %} 语法之后就可以继承父页面,然后再进行重写声明父元素有的块,就可以进行重写。

如果父模板中的块里有内容不想被子模板替换怎么办?可以使用 super() 方法。

先定修改父模版

html
<body>
<div class="header">
    {% block header %}
    头部区域
    {% endblock %}
</div>
<div class="main">
    <div class="left">
        {% block left %}
        左侧区域
        {% endblock %}
    </div>
    <div class="center">
        {% block center %}
        中间区域
        {% endblock %}
    </div>
    <div class="right">
        {% block right %}
        右侧区域
        {% endblock %}
    </div>
</div>
<div class="footer">
    {% block rooter %}
    底部区域
    {% endblock %}
</div>
</body>

并在子模板里,加上 header 块:

html
{% extends "layout.html" %}
{% block header %}
{{ super() }}
<style>
    .header {
        background-color: red;
    }
</style>
{% endblock %}

父模板同子模板的 header 块中都有内容。运行后,你可以看到,父模板中的 header 块语句先被加载,而后是子模板中的 header 块语句。这就得益于我们在子模板的 header 块中加上了表达式 {{ super() }}

宏 (Macro)

Jinja2 的宏功能有些类似于传统程序语言中的函数,既然是函数就有其声明和调用两个部分。那就让我们先声明一个宏:

html
{# 定义 input 宏 #}
{% macro input (name, type='text', value='') -%}
  <input type="{{type}}" name="{{name}}" value="{{value|e}}">
{%- endmacro %}

代码中,宏的名称就是 input ,它有三个参数分别是 name, “typevalue`,后两个参数有默认值。现在可以直接使用表达式来调用这个宏:

html
<p>{{input ('username', value='user') }}</p>
<p>{{input ('password', 'password') }}</p>
<p>{{input ('submit', 'submit', 'Submit') }}</p>

可以在页面上看到一个文本输入框,一个密码输入框及一个提交按钮。是不是同函数一样啊?其实它还有比函数更丰富的功能,之后我们来介绍。

宏的导入

一个宏可以被不同的模板使用,所以我们建议将其声明在一个单独的模板文件中。需要使用时导入进来即可,而导入的方法也非常类似于 Python 中的 import。让我们将第一个例子中 input 宏的声明放到一个 form.html 模板文件中,然后将调用的代码改为:

html
{% block body %}
<hr>
<h2> 宏 (Macro)</h2>
{% import '06 块和宏 /form.html' as form %}
<p>{{form.input ('username', value='user') }}</p>
<p>{{form.input ('password', 'password') }}</p>
<p>{{form.input ('submit', 'submit', 'Submit') }}</p>

{% from '06 块和宏 /form.html' import input %}
<p>{{input ('username', value='user') }}</p>
<p>{{input ('password', 'password') }}</p>
<hr>
{% endblock body %}

运行下,效果是不是同之前的一样?

宏的内部变量

上例中,我们看到宏的内部可以使用 caller( ) 方法获取调用者的内容。此外宏还提供了两个内部变量:

  • varargs 这是一个列表。如果调用宏时传入的参数多于宏声明时的参数,多出来的 没指定参数名 的参数就会保存在这个列表中。
  • kwargs 这是一个字典。如果调用宏时传入的参数多于宏声明时的参数,多出来的 指定了参数名 的参数就会保存在这个字典中。

让我们回到第一个例子 input 宏,在调用时增加其传入的参数,并在宏内将上述两个变量打印出来:

html
{% macro input (name, type='text', value='') -%}
  <input type="{{ type}}" name="{{name}}" value="{{value|e}}">
  <br/> {{varargs}}
  <br/> {{kwargs}}
{%- endmacro %}


<p>{{input ('submit', 'submit', 'Submit', 'more arg1', 'more arg2', ext='more arg3') }}</p>

可以看到,varargs 变量存了参数列表 ['more arg1', 'more arg2'] ,而 kwargs 字典存了参数 {'ext':'more arg3'}

访问调用者内容

我们先来创建个宏 list_users

html
{% macro list_users(users) -%}
  <table>
      <tr>
          <th> 姓名</th>
          <th> 操作</th>
      </tr>
      {%- for user in users %}
        <tr>
            <td>{{user.name |e}}</td>
            {{caller () }}
        </tr>
      {%- endfor %}
  </table>
{%- endmacro %}

宏的作用就是将用户列表显示在表格里,表格每一行用户名称后面调用了 {{caller() }} 方法,这个有什么用呢?先别急,我们来写调用者的代码:

html
{% set users=[
  {'name':'Tom','gender':'M','age':20},
  {'name':'John','gender':'M','age':18},
  {'name':'Mary','gender':'F','age':24}
]
%}

{% call list_users(users) %}
<td><input name="delete" type="button" value="Delete"></td>
{% endcall %}

与上例不同,这里我们使用了 {% call %} 语句块来调用宏,语句块中包括了一段生成Delete 按钮的代码。运行下试试,你会发现每个用户名后面都出现了Delete 按钮,也就是 {{caller () }} 部分被调用者 {% call %} 语句块内部的内容替代了。不明觉厉吧!其实吧,这个跟函数传个参数进去没啥大区别,个人觉得,主要是有些时候 HTML 语句太复杂(如上例),不方便写在调用参数上,所以就写在 {% call %} 语句块里了。

Jinja2 的宏不但能访问调用者语句块的内容,还能给调用者传递参数。 这又是个什么鬼?我们来扩展下上面的例子。首先,我们将表格增加一列性别,并在宏里调用 caller() 方法时,传入一个变量 user.gender

html
{% macro list_users2(users) -%}
<table>
    <tr>
        <th> 姓名</th>
        <th> 性别</th>
        <th> 操作</th>
    </tr>
    {%- for user in users %}
      <tr>
          <td>{{ user.name |e }}</td>
          {{ caller(user.gender) }}
      </tr>
    {%- endfor %}
</table>
{%- endmacro %}

然后,我们修改下调用者语句块:

html
{% call (gender) list_users(users) %}
  <td>
      {% if gender == 'M' %}
      <img src="{{ url_for ('static', filename='img/male.png') }}" width="20px">
      {% else %}
      <img src="{{ url_for ('static', filename='img/female.png') }}" width="20px">
      {% endif %}
  </td>
  <td><input name="delete" type="button" value="Delete"></td>
{% endcall %}

大家注意到,我们在使用 {% call %} 语句时,将其改为了 {% call (gender) ... %},这个括号中的 gender 就是用来接受宏里传来的 user.gender 变量。因此我们就可以在 {% call %} 语句中使用这个 gender 变量来判断用户性别。这样宏就成功地向调用者传递了参数。

包含 (Include)

这里我们再介绍一个 Jinja2 模板中代码重用的功能,就是包含 (Include),使用的方法就是 {% include %} 语句。其功能就是将另一个模板加载到当前模板中,并直接渲染在当前位置上。它同导入 import 不一样,import 之后你还需要调用宏来渲染你的内容,include 是直接将目标模板渲染出来。它同 block 块继承也不一样,它一次渲染整个模板文件内容,不分块。

我们可以创建一个 footer.html 模板,并在 "layout.html" 中包含这个模板:

html

<body>
...
{% include 'footer.html' %}
</body>

include 的模板文件不存在时,程序会抛出异常。你可以加上 ignore missing 关键字,这样如果模板不存在,就会忽略这段 {% include %} 语句。

html
{% include '06 块和宏 /footer.html' %}

{% include %} 语句还可以跟一个模板列表:

html
{% include ['footer.html','bottom.html','end.html'] ignore missing %}

上例中,程序会按顺序寻找模板文件,第一个被找到的模板即被加载,而其后的模板都会被忽略。如果都没找到,那整个语句都会被忽略。