模板结构

Jinja2 比传统 HTML 拥有更加强大的功能,其中一个就表现在代码模块化上。HTML 除了通过 iframe 标签在浏览器端动态加载其他网页,几乎不具备任何代码模块化的能力。而 Jinja2 则可以通过宏、模板继承、引入模板等方式实现代码模块化,下面分别进行讲解。

宏和import语句

模板中的宏与 Python 中的函数类似,可以传递参数,但是不能有返回值。可以将一些常用的代码片段放到宏中,然后把一些不固定的值抽取出来当成一个参数,下面用一个例子来阐述宏的用法。

{% macro input(name, value="", type='text') %}
  <input type="{{ type }}" value="{{ value|escape }}" name="{{ name }}">
{% endmacro %}

以上代码通过 macro 标签创建了一个叫作 input 的宏,这个宏接收两个参数,分别是 name 和 type。以后在创建 input 标签时,可以通过以下代码快速创建。

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

在实际的开发工作中,经常会将一些常用的宏单独放到一个文件中,在需要使用时再从这个文件进行导入。导入使用的是 import 语句,import 语句的用法与 Python 中的 import 类似,形式可以直接是 import…as…,也可以是 from…import…,或者是 from…import…as…。下面先创建一个 forms.html,然后添加以下代码。

{% macro input(name, value='', type='text') %}
  <input type="{{ type }}" value="{{ value|escape }}" name="{{ name }}">
{% endmacro %}

{% macro textarea(name, value='', rows=10, cols=40) %}
  <textarea name="{{ name }}" rows="{{ rows }}" cols="{{ cols }}">{{ value|escape }}</textarea>
{% endmacro %}

在 forms.html 中添加了两个宏,分别是 input 和 textarea。下面再在另外一个文件中通过 import 语句进行导入。

  • 通过 import…as… 形式导入。

    {% import 'forms.html' as forms %}
    
    <dl>
      <dt>Username</dt>
      <dd>{{ forms.input('username') }}</dd>
    
      <dt>Password</dt>
      <dd>{{ forms.input('password', type='password') }}</dd>
    </dl>
    
    <p>{{ forms.textarea('comment') }}</p>
  • 通过 from…import…as… 或者 from…import… 形式导入。

    {% from 'forms.html' import input as input_field, textarea %}
    
    <dl>
      <dt>Username</dt>
      <dd>{{ input_field('username') }}</dd>
    
      <dt>Password</dt>
      <dd>{{ input_field('password', type='password') }}</dd>
    </dl>
    
    <p>{{ textarea('comment') }}</p>

需要注意的是,通过 import 导入模板并不会把当前模板的变量添加到被导入的模板中,如果要在被导入的模板中使用当前模板的变量,可以通过以下两种方式实现。

  • 显示地通过参数形式传递变量。

  • 使用 with context 的方式,示例代码如下。

    {% from 'forms.html' import input with context %}

通过以上两种方式,在 forms.html 中的代码也可以使用当前模板的所有变量了。

模板继承

一个网站中的大部分网页的模块是重复的,如顶部的导航栏、底部的备案信息。如果在每个页面中都重复地去写这些代码,会让项目变得臃肿,增加后期的维护成本。比较好的做法是,通过模板继承,把一些重复性的代码写在父模板中,子模板继承父模板后,再分别实现自己页面的代码。首先来看一个父模板 base.html 的例子。

<!DOCTYPE html>
<html lang="en">
  <head>
    <link rel="stylesheet" href="base.css" />
    <title>{% block title %}{% endblock %}</title>
    {% block head %}{% endblock %}
  </head>
  <body>
    <div id="body">
      {% block body %}{% endblock %}
    </div>
    <div id="footer">
      {% block footer %}
        &copy; Copyright 2008 by <a href="http://domain.invalid/">you</a>
      {% endblock %}
    </div>
  </body>
</html>

在以上父模板中编写了网页的整体结构,并且把所有子模板都需要用到的样式文件 base.css 也提前做了引用。针对子模板需要重写的地方,则定义成了 block,如 title、head、body、footer,子模板在继承了父模板后,重写对应 block 的代码,即可完成子模板的渲染。下面以继承 base.html 的方式实现一个 index.html 文件,代码如下。

{% extends "base.html" %}
{% block title %}首页{% endblock %}
{% block head %}
    <style type="text/css">
        .detail {
            color: red;
        }
    </style>
{% endblock %}
{% block content %}
    <h1>这里是首页</h1>
    <p class="detail">首页的内容</p>
{% endblock %}

以上代码首先通过 extends 语法加载父模板,因为 base.html 和 index.html 是在同一级目录,所以直接写 base.html。这里需要注意的是,extends 必须出现在子模板所有代码的最前面。接下来分别实现了 title、head、content 这 3 个 block,实现的 block 中的代码将会被自动填充到父模板指定的位置,并且最终会生成一个完整 HTML 结构的 index.html 文件。

模板中不能出现重名的 block,如果一个地方需要用到另外一个 block 中的内容,可以使用 self.blockname 的方式进行引用,如以下代码所示。

<title>
  {% block title %}
    这是标题
  {% endblock %}
</title>

<h1>{{ self.title() }}</h1>

在以上示例代码中,h1 标签中通过 {{ self.title() }} 把 title 这个 block 中的内容引用到了 h1 标签中。

如果子模板要继承父模板中某个 block 的内容,如以上案例中,如果要继承父模板 footer 这个 block 中已有的内容,则可以通过 super() 来实现,示例代码如下。

{% block footer %}
    {{ super() }}
    // 子模板自己的代码
{% endblock %}

以上代码中,如果没有使用 {{ super() }},那么子模板将不能继承父模板 footer 这个 block 中的内容。

引入模板

有些 HTML 模块需要在几个页面中使用,如果用模板继承会在所有子模板中都加载,不太合适;如果在每个需要使用这个 HTML 结构的页面中都重复相同的代码,会增加后期项目的维护成本。这时就可以通过 include 语法引入模板。如在网站中推广客服二维码联系方式,在有些页面中需要使用,但是并不是所有页面都需要,因此可以把相关代码写成 _contact.html 文件,然后在需要的位置进行引用即可,示例代码如下。

{% include "_contact.html" %}

因为 _contact.html 是作为被引用的模板而存在的,所以一般在命名前加一个下画线,这样可以和普通的页面模板区分开来。