5. 模板

5.1. 模板简介

后台框架两个核心功能:

  • 处理逻辑
  • 返回结果

返回的结果根据要求一般分为两种:

  • 前后端分离下的数据:json/xml等
  • 渲染好的页面: 渲染好好的模板

本章主要要就的是如何利用处理结果渲染完整的页面并返回,即不考虑前后端分离的情况。

5.1.1. 模板

  • 模板是一个包含占位符的前端页面文件, 模板中除了占位符可能包括HTML,CSS,JavaScript
  • 需要的返回结果使用真实值替换模板中的占位符,这个过程称为“渲染”
  • Flask是使用 Jinja2 这个模板引擎来渲染模板

5.1.2. 使用模板的好处

  • 视图函数只负责业务逻辑和数据处理
  • 而模板则只负责用视图函数的数据结果进行展示
  • 代码结构清晰,耦合度低
  • 逻辑处理和显示分离

5.1.3. Jinja2

  • Jinja2:
    • Python下一个被广泛应用的模板引擎
    • 由Python实现的模板语言
    • 设计思想来源于Django的模板引擎
    • 是Flask内置的模板语言

5.1.4. 模板语言

  • 一种被设计来自动生成文档的简单文本格式
  • 渲染就是把变量传给模板,利用模板语言用变量值替换占位符

5.1.5. 渲染模版函数

  • Flask使用render_template函数渲染模板
  • render_template函数
    • 第一个参数是模板的文件名
    • 后面的参数都是键值对
    • 表示的是模板中变量对应的真实值

5.2. 模板简单使用

5.2.1. 模板的基本规则

  • {{ xxx }} 表示值内容

    • 模板文件可能的内容是:

        <h1>{{ post.title }}</h1>
      
    • {{ xxx }} 来表示变量名,基两个大括号内就是所谓的模板语言

    • 这种 {{ }} 语法叫做变量代码块

    • 渲染就是用变量值替换两个大括号和里面的内容

    • Jinja2 模版中的变量代码块可以是任意 Python 类型或者对象,只要它能够被 Python 的 str() 方法转换为一个字符串就可以

        {{your_dict['key']}}
        {{your_list[0]}}
      
  • 用 {% xxx %} 定义的控制代码块,可以实现一些语言层次的功能,比如循环或者if语句

      {% if student %}
          {{ student }}
      {% else %}
          匿名用户登录!
      <ul>
          {% for index in indexs %}
    
              <li> {{ index }} </li>
    
          {% endfor %}
      </ul>
    
  • 注释

    • 使用 {# #} 进行注释,注释的内容不会在html中被渲染出来

        {# 来自北京图灵学院的注释内容 #}
      

5.2.2. 第一个模板渲染

5.2.2.1. 模板设置

flask中使用模板一般要求有一个独立的文件夹用来存放模板,暂时我们都放在 templates文件夹下,不在区分子文件夹。

设置文件夹可以在生成Flask实例的时候用参数指明:

#  导入用来渲染模板的函数
from flask import render_template

#  参数指明存放模板的文件夹
app = Flask(__name__, template_folder='templates')

5.2.2.2. 生成模板文件

我们的主程序同级文件夹下添加文件夹templates

templates 文件夹下生成html文件hello.html, 内容参看以下代码:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>北京图灵学院</title>
    </head>
    <body>
    <h1>Hello world</h1> <br>
    -- 来自北京图灵学院的问候
    </body>
    </html>

5.2.2.3. 编写代码

编写python代码,指明需要渲染的文件名称,使用render_template进行渲染并返回即可。

@app.route("/")
def hello():
    #  不需要指明地址,直接自动去默认地址选相应的模板
    return render_template("hello.html")

5.2.2.4. 测试返回

运行程序,在浏览器中输入相应地址即可看到返回的模板内容。

tpl

5.3. 过滤器

过滤器可以理解成是python函数, 过滤器以竖线符号前面的内容为参数,把返回结果作为内容显示。

过滤器的形式为:

  • 滤器的使用方式为:{{变量名 | 过滤器}}

      {{variable | filter_name(*args)}}
      # 过滤器filter_name的参数为 variable和 args
    
  • 如果没有任何参数传给过滤器,则可以把括号省略掉

      {{variable | filter_name}}
      #  此时过滤器的参数为variable
    
  • 过滤器可以练市调用,即多个过滤器串联使用

      {{ variable | filter1 | filter2 | filter3 ... }}
      # filter1的参数是variable
      # filter2的参数是filter1的返回值
      # filter3的参数是filter2的返回值 
      # ... ...
    

5.3.1. 常见的内建过滤器

为了方便使用,程序内置了一些常用的过滤器。

内建过滤器并不复杂,基本可以理解成字符串或者列表的内置函数。

5.3.1.1. 字符串操作类

  • safe:禁用转义

    <p>{{ '<em>Hello world</em>' | safe }}</p>
    
  • capitalize:把变量值的首字母转成大写,其余字母转小写

    <p>{{ 'hello' | capitalize }}</p>
    
  • lower:把值转成小写

    <p>{{ 'HELLO' | lower }}</p>
    
  • upper:把值转成大写

      <p>{{ 'hello' | upper }}</p>
    
  • title:把值中的每个单词的首字母都转成大写

      <p>{{ 'hello' | title }}</p>
    
  • reverse:字符串反转

      <p>{{ 'holle' | reverse }}</p>
    
  • format:格式化输出

      <p>{{ '%s is %d' | format('name',17) }}</p>
    
  • striptags:渲染之前把值中所有的HTML标签都删掉

     <p>{{ '<em>hello</em>' | striptags }}</p>
    
  • truncate: 字符串截断

      <p>{{ 'hello every one' | truncate(9)}}</p>
    

5.3.1.2. 列表操作

  • first:取第一个元素

      <p>{{ [1,2,3,4,5,6] | first }}</p>
    
  • last:取最后一个元素

      <p>{{ [1,2,3,4,5,6] | last }}</p>
    
  • length:获取列表长度

      <p>{{ [1,2,3,4,5,6] | length }}</p>
    
  • sum:列表求和

      <p>{{ [1,2,3,4,5,6] | sum }}</p>
    
  • sort:列表排序

      <p>{{ [6,2,3,1,5,4] | sort }}</p>
    

5.3.2. 语句块过滤

此过滤器可以对一个语句块内的内容起作用,执行过滤操作。

        {% filter upper %}
        #一大堆文字#
        {% endfilter %}

5.3.3. 自定义过滤器

过滤器就是放在模板里的python函数,我们上面有内置的过滤器,如果内置的 过滤器不能满足需求的话,我们可以自己来定义符合自己要求的过滤器(函数).

自定义过滤器一般两种方法:

  • 通过Flask应用对象的 add_template_filter 方法
  • 通过装饰器来实现自定义过滤器

需要特别说明的是, 自定义的过滤器名称如果和内置的过滤器重名, 会覆盖内置的过滤器, 这个跟我们的函数定义一致,重名函数定义后面的 会覆盖前面的。

5.3.3.1. 使用add_template_filter

5.3.3.1.1. 使用python定义过滤器

此时需要考虑清楚, 过滤器的参数和返回值是什么

def find_biggest(li):

    assert  li
    rst  = li[0]
    for i in li:
        if i > rst:
            rst = i
    return rst
5.3.3.1.2. 系统注册

使用add_template_filter进行注册,注册函数第一个参数是定义的 函数,第二个是过滤器的名称,在模板中用的别名.

   app.add_template_filter(find_biggest, 'biggest')
5.3.3.1.3. 编写模板

在模板中正常使用即可,只要参数和返回值对应上即可。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>北京图灵学院</title>
    </head>
    <body>

    {%  for i in li %}
        {{i}}
    {% endfor %}


    <h1>最大的数是: {{li | biggest}}</h1>

    </body>
    </html>
5.3.3.1.4. 渲染模板
@app.route('/biggest')
def beggest():
    import random as r
    li = []
    for i in range(10):
        li.append(r.randint(0,100))
    
    # 注意传入模板需要的参数
    return  render_template("big.html", li=li)
5.3.3.1.5. 结果反馈

tmplate-filter

5.3.3.2. 使用装饰器template_filter

使用装饰器的效果跟手动注册一致,代码如下,效果可以自行验证。

@app.template_filter('biggest2')
def find_biggest2(li):

    assert  li
    rst  = li[0]
    for i in li:
        if i > rst:
            rst = i
    return rst

@app.route('/biggest2')
def beggest2():
    import random as r
    li = []
    for i in range(10):
        li.append(r.randint(0,100))
    return  render_template("big.html", li=li)

5.4. 控制结构

控制结构的语句要求写在{% %}当中。

作为一个可以使用的语言系统,控制结构是必不可少的,模板中常用的控制结构有:

  • if/else if/else/endif
  • for/endfor

需要注意点的是, 对于控制结构,都要有一个结束标志,例如 if 语句的 endif 或者for 语句的endfor

相对别的语言来讲,控制结构比较简单,对于有python基础的同学来讲, 都是小儿科啦,这里就两个结构,一个选择if结构,一个循环for结构。

5.4.1. if

Jinja2 中的 if 语句跟Python中的if极其相似,同样需要对一个变量或者 表达式进行判断,如果有必要会使用else语句 ,也可以使用嵌套的else if

常见的if语句结构为:

{% if xxx %}

如果表达式为真需要显示的内容 

{% endif %}

或者带有else:

{% if xxx %}
    
    如果表达式为真需要显示的内容
    
{% else %}

    如果表达式不为真需要显示的内容

{% endif %}

或者更复杂的嵌套:

{% if xxx %}

    如果表达式xxx为真需要显示的内容
    
{% else if YYY %}

    如果表达式yyy为真但xxx不为真需要显示的内容
    
{% endif %}

5.4.2. for循环oo

Jinja2中的for循环跟python中的for也类似,可以用来对列表或者生成器函数进行迭代。

{% for i in li %}

     需要循环出现的内容

{% endfor %}

5.4.3. 控制结构小案例

for循环或者if语句的案例参看下面内容:

<h1>全列表内容为: </h1><br>
    {% for i in li %}
        <li>{{i}}</li>
    {% endfor %}

    <h1>其中偶数为: </h1><br>
    {% for i in li %}
    {% if i %2 == 0 %}
    <li>{{i}}</li>
    {% endif %}

    {% endfor %}

相应的python代码如下:

    @app.route('/contr')
    def contr():
        import random as r
        li = []
        for i in range(10):
            li.append(r.randint(0,100))

        return  render_template("contr.html", li=li)

上面代码运行后的结果如下:

控制结构

5.4.4. for内置变量

for循环有一些内置变量用来增强模板的能力, 这些变量能直接使用:

  • loop.index: 当前循环迭代的次数(从 1 开始)
  • loop.index0: 当前循环迭代的次数(从 0 开始)
  • loop.revindex: 到循环结束需要迭代的次数(从 1 开始)
  • loop.revindex0: 到循环结束需要迭代的次数(从 0 开始)
  • loop.first: 如果是第一次迭代,为 True
  • loop.last: 如果是最后一次迭代,为 True
  • loop.length: 序列中的项目数
  • loop.cycle: 在一串序列间期取值的辅助函数

此类变量可以直接拿来使用,具体使用参看案例代码:

Python代码:

    my_list = [
        {
            "id": 1,
            "value": "我来北京图灵学院的专业:"
        },
        {
            "id": 2,
            "value": "Python全栈"
        },
        {
            "id": 3,
            "value": "人工智能"
        },
        {
            "id": 4,
            "value": "数据分析"
        },
        {
            "id": 5,
            "value": "爬虫大全"
        }
    ]

    @app.route('/listfor')
    def list_for():
        return render_template(r"forvari.html", li=my_list)

Html代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

{# 下面代码显示其实有一个bug, index=1的内容会显示两次, why? #}
{% for item in li  %}
{% if loop.first %}
<li style="background-color: red">{{ loop.index}}  -  {{item.value }}</li>
{% endif %}


{% if loop.index == 2 %}
<li style="background-color: blue">{{ loop.index}}  -  {{ item.value }}</li>
{% elif loop.index == 3 %}
<li style="background-color: green">{{ loop.index}}  -  {{ item.value }}</li>
{% elif loop.index == 4 %}
<li style="background-color: gray">{{ loop.index}}  -  {{ item.value }}</li>
{% else %}
<li style="background-color: palegreen">{{ loop.index}}  -  {{ item.value }}</li>
{% endif %}
{% endfor %}

</body>
</html>

5.5. 模板复用

在整个网站的前端代码模板的处理中,我们很自然的需要用到 一些常见的软件工程内容,比如代码复用等,我们可以通过模板中的宏,继承和包含 来实现代码复用。

5.5.1. macro

直接把宏理解成函数比较合适,如果是函数,我们需要宏:

  • 编写好可被调用
  • 调用的时候执行内容
  • 为了方便管理,宏可以单独放入一个文件,然后倒入

5.5.1.1. 宏定义

基本跟函数定义类似,不同的是需要有一个结束符号:

{% macro input(name,value='',type='text') %}
{# 把这里的代码理解成函数体 #}
{# 可以包含我们的模板标签,前端内容 #}
<input type="{{type}}" name="{{name}}"
       value="{{value}}" class="form-control">
{% endmacro %}

5.5.1.2. 宏的调用

调用宏只需要把他当成一个普通函数使用即可,如果是把宏单独出 一个文件保存,则需要导入:

  • 可以直接调用:

      {{ input('北京图灵学院' value='tuling')}} 
    

    显示的结果就是把宏定义体内的内容利用传入的参数进行替换后的内容:

     <input type="text" name="北京图灵学院"
      value="tuling" class="form-control">
    
  • 宏单独存入一个文件, 则需要导入, 我们把宏存入文件macro.html:

    在使用的时候,需要在其它模板文件中先导入,再调用

      {% import 'macro.html' as func %}
    
      {{ func.input("北京图灵学院", value="tuling") }}
    

5.5.1.3. 宏的小案例

参看文件:

  • macro.html
  • macrotest.html
  • tpl.py

5.6. 模板的继承

正常一个页面我们需要拆解成页头,页脚等结构,奔着不重复造轮子的精神, 我们的模板也用到了继承,一些公共部分,我们采用继承就可以。

模板继承的步骤是:

  • 准备被继承的模板base.html, 在父模板中,我们需要继承的代码块 用代码块语法留出位置

      {% block  one%} 
      {% endblock  one%}
    
  • 子模块扩展父模块,然后直接实现父模块中的预留的内容

      {% extends 'base.html' %}
    
      {% block one %} 
      {% endblock one %}
    

模块的继承是一个十分简单的内容,但是需要注意:

  • 不支持多重继承,准确的说模块连继承都算不上,只是复用
  • block标签名称需要唯一
  • extends尽量写在第一行,便于阅读

5.6.1. 案例

  • 父模板是tuling_base.html

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>这是爸爸模板</title>
      </head>
      <body>
    
      <h1>在爸爸模板中出现这个标题就可以了:</h1>
      {% block one %}
      {% endblock one %}
    
      </body>
      </html>
    
  • 子模块tuling_son.html

      {% extends "tuling_base.html" %}
    
      {% block one %}
      <h3>在儿子模板中显示这个也是可以滴</h3>
      哈哈哈哈哈,我得儿意得笑呀,我笑看红尘天不老呀~~~~
      {% endblock one %}
    
  • py代码为/myson的url

      @app.route('/myson')
      def myson():
          return render_template(r'tuling_son.html')
    

5.7. 包含

代码重用可以用继承,也可以用包含,这样就把别的文件的内容 直接加载到当前模板中,可以把继承理解成完整的整个模板拿过来了, 包含是一块一块的代码分别复制粘贴过来。

包含的语法:

 {% include "one.html"  %}

使用包含的时候需要注意:

  • 如果模板不存在会出现TemplateNotFound异常

  • 如果需要忽略异常,使用ignore missing

      {% include "one.html" ignore missing %}
    

5.7.1. 案例

  • 被包含代码one.html, 代码只是一段,并不是可运行的完整代码

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>Title</title>
      </head>
      <body>
    
  • 模板two.html

      {% include "one.html" %}
    
      <h1>这个是包含的代码</h1>
    
      </body>
      </html>
    
  • py代码为/inc的url

      @app.route('/incl')
      def incl():
          return render_template(r'two.html')
    

5.8. 模板特有的变量和函数

程序需要往模板传入数据,一般我们可以通过参数传入,但有些数据是系统 默认内置的对象或者函数,我们本节学习使用那些内置的数据。

常用的内置信息有:

  • config:系统的配置信息
  • request:当前的请求对象
  • session:当前session对象
  • g:当前的g变量内容
  • url_for: 显示对应的URL
  • get_flashed_messages: 得到当前环境中的消息

5.8.1. 代码案例

  • 模板代码:

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>Title</title>
      </head>
      <body>
    
      <li><h1>{{config.MYNAME}}</h1></li>
      <li><h1>{{request.url}}</h1></li>
      <li><h1>{{session}}</h1></li>
      <li><h1>{{g.name}}</h1></li>
      <li><h1>{{url_for("incl")}}</h1></li>
    
      </body>
      </html>
    
  • python代码:)

      def buildin():
          return render_template(r'buildin.html')
    
  • 最后结果:

    buildin

5.9. Flask-WTF表单

Form是是HTML页面中负责数据采集的部件。 表单有三个部分组成:

  • 表单标签
  • 表单域
  • 表单按钮

在Flask中,我们可以使用 Flask-WTF 扩展来处理表单, 它封装了 WTForms,并且它有验证表单数据的功能

5.9.1. WTForms的标准字段

  • StringField: 文本字段
  • TextAreaField: 多行文本字段
  • PasswordField: 密码文本字段
  • HiddenField: 隐藏文件字段
  • DateField: 文本字段,值为 datetime.date 文本格式
  • DateTimeField: 文本字段,值为 datetime.datetime 文本格式
  • IntegerField: 文本字段,值为整数
  • DecimalField: 文本字段,值为decimal.Decimal
  • FloatField: 文本字段,值为浮点数
  • BooleanField: 复选框,值为True 和 False
  • RadioField: 一组单选框
  • SelectField: 下拉列表
  • SelectMutipleField: 下拉列表,可选择多个值
  • FileField: 文件上传字段
  • SubmitField: 表单提交按钮
  • FormField: 把表单作为字段嵌入另一个表单
  • FieldList: 一组指定类型的字段

5.9.2. WTForms常用验证函数

使用 Flask-WTF 需要配置参数 SECRET_KEY。

  • DataRequired: 确保字段中有数据
  • EqualTo: 比较两个字段的值,常用于比较两次密码输入
  • Length: 验证输入的字符串长度
  • NumberRange: 验证输入的值在数字范围内
  • URL: 验证URL
  • AnyOf: 验证输入值在可选列表中
  • NoneOf: 验证输入值不在可选列表中

5.9.3. 代码示例

如果使用第三方的WTF,需要进行安装,代码如下:

#  需要注意点 是一定要在当前虚拟环境下安装
pip install flask_wtf

安装完毕后就需要对系统参数进行设置,主要是配置信息,这里有 两个配置必须设置:

#  关闭CSRF保护
app.config['WTF_CSRF_ENABLED'] = False
#  设置秘钥,关于安全的一般都需要设置秘钥 
app.config['SECRET_KEY'] = "kdkakikeiqkl;hjkiauhfipouhqkjklkll;kjkj1kl231"

5.9.3.1. python代码

代码都加了注释,并不复杂,也就不过多的介绍。

from flask_wtf import FlaskForm
from wtforms import SubmitField, StringField, PasswordField
from wtforms.validators import DataRequired, EqualTo

#  本案例需要的其他内容,请求体和消息插件
from flask import request, flash


# 定义一个Form,使用WTF定义
class MakeForm(FlaskForm):
    username = StringField("用户名", validators=[DataRequired()])
    password = StringField("密码", validators=[DataRequired()])
    address = StringField("地址")
    tel = StringField("电话")
    submit = SubmitField("提交啦")

@app.route('/wtf', methods=['get', 'post'])
def wtf():
    # 生成表单
    f = MakeForm()

    #  对应POST请求后的表单验证
    if f.validate_on_submit():
        un = request.form.get("username")
        pwd = request.form.get("password")
        address = request.form.get("address")
        tel = request.form.get("tel")

        return "加油,你是最胖的~~~"
    #  form数据验证没有通过
    else:
        if request.method == "POST":
            print("hahah")
            flash("有错误的呀,加油呀,年轻银~~~")

    #  如果访问不是通过submit提交上来的,即是第一次get访问,则返回模板
    return render_template('wtf.html', form=f)

5.9.3.2. HTMl代码

在HTML中只是把form传入的各个内容进行设置就可以,需要注意的是我们使用了 messages, 此时在python代码中需要用到flask

<form method="post">
    {{form.username.label}}{{form.username}}<br/>
    {{form.password.label}}{{form.password}}<br/>
    {{form.address.label}}{{form.address}}<br/>
    {{form.tel.label}}{{form.tel}}<br/>
    {{form.submit}}<br/>

</form>

{% for message in get_flashed_messages() %}
    {{ message }}
{% endfor %}

5.9.3.3. 结果演示

  • 第一次输入到时候,会显示表单,此时填入相应内容。

  • 如果内容正确,会显示返回内容:

    结果正确

  • 如果输入有问题,触发了验证器,则显示一下内容: 结果不正确