jinja2
fastapi中的Jinja2Templates

概述

在之前的文章Python中的template语言中介绍了python string模块提供的Template类template strings和mustache的python实现chevron,这篇我们来看下Python下一个被广泛应用的模版引擎Jinja2

Jinja是一个灵活快速可扩展的模板引擎。在模板中的特殊占位符内可编写类似于 Python 语法的代码,然后数据就会被传递来作为最终呈现

许多Python web 框架如 Django、Flask、Fastapi使用它作为模板呈现引擎。模板语言允许创建基于文本的文档,其中一些内容可以动态生成,其思想是在代码中捕获业务逻辑,同时给予模板设计工具来控制流和结束文档的布局。

Jinja特性:

  1. 强大的流程控制
  2. 丰富的内置过滤器及自定义过滤器
  3. 模板继承
  4. html模板中可以自动转义防止xss攻击
  5. 易调试

官方建议Isn’t it a bad idea to put logic in templates?
Without a doubt you should try to remove as much logic from templates as possible...

相关文档

jinja的使用

Use the methods on Environment to create or load templates. The environment is used to configure how templates are compiled and behave.
It is also possible to create a template object directly. This is not usually recommended. The constructor takes most of the same arguments as Environment. All templates created with the same environment arguments share the same ephemeral Environment instance behind the scenes.

  • jinja2.Environment([options])
class Environment(object):
    r"""The core component of Jinja is the `Environment`.  It contains
    important shared variables like configuration, filters, tests,
    globals and others.  Instances of this class may be modified if
    they are not shared and if no template was loaded so far.
    Modifications on environments after the first template was loaded
    will lead to surprising effects and undefined behavior.

    Here are the possible initialization parameters:
        ...
        ...
        `autoescape`
            If set to ``True`` the XML/HTML autoescaping feature is enabled by
            default.  For more details about autoescaping see
            :class:`~markupsafe.Markup`.  As of Jinja 2.4 this can also
            be a callable that is passed the template name and has to
            return ``True`` or ``False`` depending on autoescape should be
            enabled by default.

            .. versionchanged:: 2.4
               `autoescape` can now be a function

        `loader`
            The template loader for this environment.
        ...
        ...
    """
...
...
  • jinja2.PackageLoader(package_name, package_path='templates', encoding='utf-8')从python包目录中加载模板
    • package_name:包含 template 目录的包名
    • package_path:template目录名
  • Environment.get_template(name, parent=None, globals=None)使用loader通过模板名字加载模板,如果模板不存在则会抛出TemplateNotFound异常
  • Template.render([context])
    • 参数接受dict或dict子类或kw参数,返回字符串
# 以下两种方式是等效的
template.render(knights='that say nih')
template.render({'knights': 'that say nih'})

使用示例

  • 目录结构
$ tree .
├── pkg
│   ├── __init__.py
│   └── templates
│       └── test.html
├── test.py
  • test.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
{{ the }} {{ go }}
</body>
</html>
  • test.py
from jinja2 import Environment, PackageLoader, select_autoescape

loader = PackageLoader('pkg', 'templates')
print(1, loader, type(loader))

env = Environment(
    loader=loader,
    autoescape=select_autoescape()
)
print(2, env, type(env))

template = env.get_template('test.html')
print(3, template, type(template), sep='\n')

result = template.render(the="variables", go="here")
print(4, result, type(result))
################输出结果################
1 <jinja2.loaders.PackageLoader object at 0x10821ecd0> <class 'jinja2.loaders.PackageLoader'>
2 <jinja2.environment.Environment object at 0x108a8b5b0> <class 'jinja2.environment.Environment'>
3
<Template 'test.html'>
<class 'jinja2.environment.Template'>
4 <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
variables here
</body>
</html> <class 'str'>

jinja模板引擎语法

基本语法

  • 注释{# ... #}
  • 变量{{ ... }}
  • 流程控制语句{% ... %}
    • for:{% for... in ... %} {% endfor %}
    • if:{% if ... %} {% elif... %} {% else %} {% endif %}
  • 示例
    • 修改如上例子中的test.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Great Books of All Time</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
          integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
</head>
<body>
<div class="container">
    <h1>Great Books of All Time</h1>
    <table class="table">
        <thead>
        <tr>
            <th scope="col">#</th>
            <th scope="col">Author</th>
            <th scope="col">Book Title</th>
        </tr>
        </thead>
        <tbody>

        {% for item in details %}
        <tr>
            <td>{{ item.index + 1 }}</td>
            <td>{{ item.author }}</td>
            {% if item.author == "jerry" %}
                <td> Monkey Jerry's Book</td>
            {% else %}
                <td>{{ item.book_title }}</td>
            {% endif %}
        </tr>
        {% endfor %}

        </tbody>
    </table>
</div>
</body>
</html>
  • test.py如下
from jinja2 import Environment, PackageLoader, select_autoescape

env = Environment(
    loader=PackageLoader('pkg', 'templates'),
    autoescape=select_autoescape()
)

template = env.get_template('test.html')

details = [
    {"author": "Dale Carnegie", "book_title": "How To Win Friends And Influence People"},
    {"author": "Daniel Kahneman", "book_title": "Thinking, Fast and Slow"},
    {"author": "jerry"}
]

for k, v in enumerate(details):
    if isinstance(v, dict):
        v["index"] = k

# print(details)

result = template.render(details=details)
with open("result.html", "w") as f:
    f.write(result)

模板继承

  • 父模板 {% block ... %} {% endblock %}
  • 子模板 {% extends "..." %} {{ super() }}
<!-- 修改如上的test.html为base.html,内容如下,添加 {% block content %}   {% endblock %} -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title> Great Books of All Time </title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
          integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
</head>

<body>

<div class="container">
    <h1> Great Books of All Time </h1>
    <table class="table">
        <thead>
        <tr>
            <th scope="col">#</th>
            <th scope="col">Author</th>
            <th scope="col">Book Title</th>
        </tr>
        </thead>
        <tbody>
        {% block content%}
        {% for item in details %}
        <tr>
            <td>{{ item.index + 1 }}</td>
            <td>{{ item.author }}</td>
            {% if item.author == "jerry" %}
            <td> Monkey Jerry's Book</td>
            {% else %}
            <td>{{ item.book_title }}</td>
            {% endif %}
        </tr>
        {% endfor %}
        {% endblock %}
        </tbody>
    </table>
</div>
</body>
</html>

则 child.html(如要在表格后面再增加一行):

{% extends "base.html" %}
{% block content %}
{{ super() }}
 <tr>
        <td>end</td>
        <td>tom</td>
        <td> Tom's Book</td>
 </tr>
{% endblock %}

test.py为

from jinja2 import Environment, PackageLoader, select_autoescape

env = Environment(
    loader=PackageLoader('pkg', 'templates'),
    autoescape=select_autoescape()
)

template = env.get_template('child.html')  # 仅此处变化

details = [
    {"author": "Dale Carnegie", "book_title": "How To Win Friends And Influence People"},
    {"author": "Daniel Kahneman", "book_title": "Thinking, Fast and Slow"},
    {"author": "jerry"}
]

for k, v in enumerate(details):
    if isinstance(v, dict):
        v["index"] = k

# print(details)

result = template.render(details=details)
with open("result.html", "w") as f:
    f.write(result)

运算符与表达式

  • 数学运算符:+ - / // % * **
  • 比较运算符:== != > >= < <=
  • 逻辑运算符:and or not
  • 其他操作符:in is 等
  • if 表达式:如{{ "[{}]".format(page.title) if page.title }}
  • python方法:如 {{ page.title.capitalize() }} {{ f.bar(value) }}

更多高级功能可参考 Template Designer Documentation
其实很多高级功能最好在代码中去处理掉,不建议在模板引擎中去处理

fastapi中的Jinja2Templates

  1. Import Jinja2Templates
  2. 创建templates
  3. 在路径操作中声明Request参数,这个函数返回TemplateResponse
  4. 将request作为kv对参数传入jinja2的context中来呈现你创建的template
  • 演示示例
  1. 文件目录
$ tree .
├── templates
│   ├── base.html
├── static
│   ├── styles.css
├── test.py
  1. 使用Jinja2Templates,test.py内容如下
import uvicorn
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

app = FastAPI()

app.mount('/static', StaticFiles(directory="static"), name='static')  # 注意

templates = Jinja2Templates(directory='templates')

details = [
    {"author": "Dale Carnegie", "book_title": "How To Win Friends And Influence People"},
    {"author": "Daniel Kahneman", "book_title": "Thinking, Fast and Slow"},
    {"author": "jerry"}
]

for k, v in enumerate(details):
    if isinstance(v, dict):
        v["index"] = k


@app.get("/books/{id}", response_class=HTMLResponse)   # 声明response_class=HTMLResponse 是为了让fastapi docs知道响应是html
async def get_books(request: Request, id: int):
    print(id)
    return templates.TemplateResponse("base.html", {
        "request": request,
        "details": details,
        "id": id
    })


if __name__ == '__main__':
    uvicorn.run('test:app', host='0.0.0.0', port=8000, reload=True, debug=True, workers=1)
  1. 编写template,base.html内容如下:
<!DOCTYPE html>
<!--<html lang="en">-->
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title> Great Books of All Time </title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
          integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
    <link href="{{ url_for('static', path='/styles.css') }}" rel="stylesheet">

</head>

<body>

<div class="container">
    <h1> Great Books of All Time </h1>
    <table class="table">
        <thead>
        <tr>
            <th scope="col">#</th>
            <th scope="col">Author</th>
            <th scope="col">Book Title</th>
        </tr>
        </thead>
        <tbody>
        {% for item in details %}
        <tr>
            {% if item.index + 1 == id %}
                <td>{{ item.index + 1 }}</td>
                <td>{{ item.author }}</td>
                <td>{{ item.book_title }}</td>
            {% endif %}
        </tr>
        {% endfor %}
        </tbody>
    </table>
</div>
</body>

<div class="site-footer">
    © 2019-2021 &nbsp <a href="https://monkeyjerry.top/post/about">小猴子jerry</a>
    <a class="icp" href="https://beian.miit.gov.cn" target="_blank">沪ICP备2021037059号</a>
</div>
</html>

注意css静态文件的引入,使用url_for()

styles.css内容如下:

.site-footer {
    padding: 30px 0;
    color: #bbb;
    text-align: center;
    font-size: 12px;
}
  1. 最后在浏览器中输入http://127.0.0.1:8000/books/1 http://127.0.0.1:8000/books/2则会返回当前序号的书

参考及扩展阅读