FlaskDay05

模板相关

目录

回顾

在这之前的示例里面,视图函数的主要作用是生成请求的响应,这是最简单的请求。实际上我们也涉及到了视图函数的两个作用:处理业务逻辑和返回响应内容。但是在大型应用中,把业务逻辑和表现内容放在一起,会增加代码的复杂度和维护成本。而模板就能承担视图函数中返回响应内容这一作用,从而整个系统耦合低,整个代码结构更加清晰。

实际上我们已经使用过 render_template() 方法来渲染模板,下面是更加系统化的学习模板这一概念。

模板初识

  • 模板其实是一个包含响应文本的文件,其中用占位符(变量)表示动态部分,告诉模板引擎其具体的值需要从使用的数据中获取
  • 使用真实值替换变量,再返回最终得到的字符串,这个过程称为“渲染”
  • Flask 是使用 Jinja2 这个模板引擎来渲染模板
  1. Jinjia2 是 Flask 框架内置的模板语言,设计思想来源于 Django 的模板引起,在其基础上扩展了语法和其他更加强大的功能
  2. 所谓模板语言就是一种可以自动生成文档的简单文本格式,在模板语言中,一般会把一些变量传给模板,替换模板的特定位置上预先定义好的占位变量名。
  3. Flask 提供的 render_template() 函数封装了该模板引擎
  4. render_template() 函数的第一个参数是模板的文件名,后面的参数都是键值对,表示模板中变量对应的真实值,即:
render_template(模板名,key = value,key = value)

变量代码块

先传一个名字?

import settings
from flask import Flask,render_template
app = Flask(__name__)
app.config.from_object(settings)
@app.route('/')
def index():
username = '小明'
return render_template('test0223.html')
if __name__ == '__main__':
app.run()

那么我们如何把视图函数中的 username 变量传入 test0223.html 文件中呢?即像上面语法所说,传入参数即可

# 省略部分代码
@app.route('/')
def index():
username = '小明'
return render_template('test0223.html', name = username)

这里注意一下:

  1. 由于 render_template() 默认是从 templates 文件夹中获取模板文件(在初始化类的时候已经默认),所以我们一般都是把模板文件放在templates 文件夹中,当然你也可以在 templates 文件夹中再新建一个文件夹,但此时 render_template() 函数的第一个变量就要将路径稍微修改一下,这一点后面会讲到
  2. 所谓:render_template(模板名,key = value,key = value) 中,key 的值是任意的,而 value 的值则是所定义的变量名,上面的示例中即为:username 。也就是说你可以:username = username ,这也是更加普遍的写法,但是对于新手来说理解不太友好,总之 key 可为任意值。

接下来就是如何在模板文件里面(这里指 test0223.html )中接收这个传入的变量呢?这里就涉及到模板语法问题,这里先介绍最基础的: {{}} 来表示变量名,这种 {{}} 语法叫做变量代码块即:

{{ 变量名key }}

所以应用在 test0223.html 中即可为,

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>小明在哪里</title>
</head>
<body>
<div>小明的展示区</div>
<p>用户名是:{{ name }}</p>
</body>
</html>

本地调试结果如下:

进阶

上面成功传入小明的名字,即为字符串。而 Jinja2 模版中的变量代码块可以是任意 Python 类型或者对象,只要它能够被 Python 的 str() 方法转换为一个字符串就可以,下面就演示一下列表、字典。

import settings
from flask import Flask, render_template
app = Flask(__name__)
app.config.from_object(settings)
@app.route('/')
def index():
username = '小明'
userplace = ['北京', '上海', '广州', '深圳'] # 列表
userwork = {'work1': 'python工程师', 'work2': 'java开发工程师', 'work3': '数据分析师'} # 字典
return render_template('test0223.html', name=username, place=userplace, work=userwork)
if __name__ == '__main__':
app.run()

对应模板文件:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>小明在哪里</title>
</head>
<body>
<div>小明的展示区</div>
<p>用户名是:{{ name }}</p>
<br>
<p>{{ name }}的工作地点有如下选择:{{ place }},<br>最后{{ name }}选择的是 {{ place[0] }}</p>
<hr>
<p> {{ name }}可以选的工作有:{{ work }},<br>最后{{ name }}选择的是 {{ work['work1'] }}</p>
</body>
</html>

本地调试结果如下:

通过上面的例子我们可以发现,不仅可以传入字典、列表,还能通过上面的方式显示一个字典或者列表中的某个元素。

控制代码块

如果模板中只能传入视图函数中预先定好的变量,是不是功能有些单一了,如果我想在模板中循环遍历变量或者对变量进行判断的时候,即实现一些语言层次的功能,就可以使用控制代码块。控制代码块用 {% %} 定义,具体语法如下:

#循环遍历
{% for 变量 in 可迭代对象 %}
{{ 变量 }}
{% endfor %}
# 判断语句
{% if 条件 %}
{{ 变量 }}
{% else %}
{{ 变量 }}
{% elif %}
{{ 变量 }}
{% endif %}

这里需要注意的是:

  1. for 和 endfor 要成对出现
  2. if 和 endif 也要成对出现

示例 1

我们继续拿上面的代码进行控制代码块的演示,视图函数不变,我们所需要的修改模板文件:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>小明在哪里</title>
</head>
<body>
<div>小明的展示区</div>
<p>用户名是:{{ name }}</p>
<br>
<p>{{ name }}的工作地点有如下选择:{{ place }},<br>最后{{ name }}选择的是 {{ place[0] }}</p>
<hr>
<p> {{ name }}可以选的工作有:{{ work }},<br>最后{{ name }}选择的是 {{ work['work1'] }}</p>
<hr>
<p>{{ name }} 的工作地点如下,其中最中意的工作为显示为超链接的那个</p>
{% for places in place %}
{% if places=='广州' %}
<li><a href="">{{ places }}</a></li>
{% else %}
<li>{{ places }}</li>
{% endif %}
{% endfor %}
</body>
</html>

本地调试结果如下:

示例 2

其实控制代码块更普遍的用法是根据变量来创建表格,我们来写一个新的视图函数与模板文件,从而来创建一个个人信息表:

### 省略部分代码
@app.route('/show')
def show():
private_data=[
{'name': '小明', 'sex': '男', 'phone': '13478952325'},
{'name': '小芳', 'sex': '女', 'phone': '13922234242'},
{'name': '小洪', 'sex': '女', 'phone': '13831132321'},
{'name': '小张', 'sex': '男', 'phone': '13513123133'},
]
return render_template('datashow.html',userdata = private_data)

模板文件:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>个人信息展示页</title>
</head>
<body>
<table border="1" cellpadding="5" cellspacing="0">
{% for users in userdata %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ users.name }}</td>
<td>{{ users.sex }}</td>
<td>{{ users.phone }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>

结果如下:

上面代码中出现了 loop,实际上在一个循环代码块内部调用 loop 的属性可以获得循环中的状态数据,上面的用法:loop.index ,就可以让表格按顺序排序,另还有其他的用法:

loop.index0 #序号从0开始;
loop.revindex #倒序排序;
loop.revindex0 #从0开始倒序排序;
loop.first #布尔类型,是否是第一行;
loop.last #布尔类型,是否是第二行;

题外话

上面控制块的示例代码是通过 Jinjia2 模板库进行渲染的,如果我们只是运行 html 文件,则只会显示合理的 html 标签所渲染出来的内容,也就是说单独运行 html 文件,变量代码块和控制代码块是无法正常渲染的,在浏览器看来只是一堆字符串,因为未被 Jinjia2 渲染,如将上面的 datashow.html 文件单独渲染,则是这样的情况:

另外如果你在 Pycharm 对 templates 文件夹中的 html 文件进行快捷键的注释时( CTRL + /),你会发现并不是常规的 html 注释语法,而是:{# 注释的内容 #}

过滤器

所谓过滤器,其实本质上是函数。

过滤器的出现是因为我们对传入变量有所 “求”。我们希望修改变量的显示,格式化或者运算等,但是在模板中是无法调用 Python 中的某些方法的,所以就用到了过滤器。过滤器语法如下:

{{  变量名 | 过滤器  }}
{{  变量名 | 过滤器(参数)}}

示例

我们用 safe 过滤器展示过滤器的使用,其作用是禁用转译。新建一个视图函数与对应的模板文件:

# 省略部分代码
@app.route('/safe')
def safe():
usermsg = '<h1> 这是个标题 </h1>'
return render_template('safetest.html',msg=usermsg)

模板文件:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>safe 过滤器测试页面</title>
</head>
<body>
{{ msg }}
</body>
</html>

我的初衷就是能够显示 h1 标题的形式,就是“又粗又大”。然而如果按照上面的代码进行本地调试,结果则是如左下图;原因也很明显:变量是字符串,传到模板时也是字符串,不使用过滤器 safe,即将内容原样输出;所以如果使用过滤器 safe,是将内容当做 html 进行解析。

如果“禁用转义”这四个字能将你绕晕,我更推荐你去理解 safe 这个单词的意思:safe 的本意是安全,在这里指的是我清楚并明白我传入的参数是安全的,不要对它做任何其他处理

那么只需在根据语法使用过滤器 safe,即可得到我们想要的效果:{{ msg | safe }} ,结果如右下图

常见过滤器

上面的例子很经典的表现了过滤器的使用与意义,也就是像之前所说就是个函数就是个方法。而 flask 中自带了很多简单的过滤形式的过滤器:

过滤器 作用 例子 效果
safe 禁用转义

{{ '<em>hello</em>' | safe }}

hello
capitalize 把变量值的首字母转成大写,其余字母转小写

{{ 'hello' | capitalize }}

Capitalize
lower 把值转成小写

{{ 'HELLO' | lower }}

hello
upper 把值转成大写

{{ 'hello' | upper }}

HELLO
title 把值中的每个单词的首字母都转成大写

{{ 'hello world' | title }}

Hello World
reverse 字符串反转

{{ 'olleh' | reverse }}

hello
format 格式化输出

{{ '%s is %d' | format('name',17) }}

name is 17
striptags 渲染之前把值中所有的HTML标签都删掉

{{ 'hello' | striptags }}

hello
truncate 字符串截断

{{ 'hello every one' | truncate(9)}}

hello...

而上面的仅仅为字符串的过滤器,对于列表也有相对应的过滤器:

过滤器 作用 例子 效果
first 取第一个元素

{{ [1,2,3,4,5,6] | first }}

1
last 取最后一个元素

{{ [1,2,3,4,5,6] | last }}

6
length 获取列表长度

{{ [1,2,3,4,5,6] | length }}

6
sum 列表求和

{{ [1,2,3,4,5,6] | sum }}

21
sort 列表排序

{{ [6,2,3,1,5,4] | sort }}

[1,2,3,4,5,6]

自定义过滤器

自定义过滤器的出现也很简单,就像系统内置的函数一样,虽然有现成的,但是终归不能符合现实需求,所以我们可以自定义过滤器,在这里你可以理解为自定义函数。

而自定义过滤器的方法也是和添加路由的方法基本类似有两种:

语法大概如下:

# 使用第一种方法
def 函数名():
方法
app.add_template_filter(函数名,'自定义过滤器方法名')
# 使用第二种方法
@app.template_filter('自定义过滤器名')
def 函数名(参数):
参数=参数.replace('hello','thanks')
return 参数

下面还是通过例子来把这两种方法都实现一下,明确需求,我们是要新建一个过滤器,而不是一个 路由/ 视图函数,我们可以继续使用刚才的视图函数与路由,把传的变量稍微改一下,实现一个可以将 'hello' 替换为 '我把 hello 换成了这个' 作用的过滤器。

# 省略部分代码
@app.route('/safe')
def safe():
usermsg = '<h1> 这是个标题 </h1>'
msg = 'hello'
return render_template('safetest.html', msg=msg)
# 第一种方法 使用 add.template_filter() 方法
def replace_hello(value):
value = value.replace('hello', '我把 hello 换成了这个')
return value
app.add_template_filter(replace_hello, 'replace1')
# 第二种方法 使用
@app.template_filter('replace2')
def reverse_list(value):
value=value.replace('hello','我把 hello 换成了这个')
return value

模板文件:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>safe 过滤器测试页面</title>
</head>
<body>
<h2>这是第一种方法:使用 add_template_filter() 方法</h2>
{{ msg | safe }}
<br>
{{ msg | replace1 }}
<hr>
<h2>这是第二种方法:使用装饰器自定义过滤器</h2>
{{ msg | safe }}
<br>
{{ msg | replace2 }}
</body>
</html>
image.png

我个人还是习惯使用第二种方法自定义过滤器,即通过装饰器来实现自定义过滤器,不会有一种多此一举的感觉。

模板复用

有了上面模板的基础知识与使用,我们已经能够做出基本的系统,但是你会发现当你做一个系统的时候,很多页面(模板)有类似的情况:

别被绕晕了,其实就是这样一个问题:模板内容重复,那么是不是每个模板都要写这种内容相同的 HTML 代码? 所以就引出了模板复用这一个概念,像遇到这种情况,可以使用 JinJa2 模板中的 继承(extends)、包含(include)、宏(macro)来进行实现,有一定编程基础的(指使用面向对象编程语言的)一看这几个名词就非常熟悉,甚至已经知道是什么作用了。

继承(extends)

继承,字面意思,你可以理解为儿子继承父亲家产的意思,更巧的是,实际上在一般 Web 开发中,需要继承的部分定义在模板中,模板继承,而不需要重复书写,恰好是这个父子概念,语法如下:

#父模板
{% block 名字 %} #名字是任意的
{% endblock %}
#子模板
{% extends '父模板的名字' %}

继承并非 “一时兴起”,选择继承之前你要有全盘计划,具体继承可以分为两步:

  1. 定义父模板:

    • 定义一个 base.html 模板
    • 分析模板中哪些是变化,对变化部分用标签语法 block 进行挖坑处理(预留位置)
    • 一般情况下,样式 CSS 和脚本 JS 是需要提前预留的地方,按照正常开发也是这样的道理
  2. 子模板继承父模板

    • 继承父模板内容
    • 通过 block 内容 找到对应的标签 block 来填充内容

下面来一个示例。先创建父模板 base.html 文件:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
# 预留编写网页名的block
<title>{% block title %}父模板的title{% endblock %}</title>
<style>
#head{
height: 50px;
background-color: bisque;
}
#head ul{
list-style: none;
}
#head ul li{
float: left;
width: 100px;
text-align: center;
font-size: 18px;
height: 50px;
line-height: 50px;
}
#middle{
height: 500px;
}
#foot{
height: 50px;
line-height: 50px;
background-color: aqua;
}
</style>
# 预留编写样式css的block
{% block mycss%}{% endblock %}
</head>
<body>
<div id="head">
<ul>
<li>首页</li>
<li>秒杀</li>
<li>超市</li>
<li>图书</li>
<li>会员</li>
</ul>
</div>
<div id="middle">
# 预留编写中间内容的block
{% block middle %}<button id="btn">我是中间部分</button>{% endblock %}
</div>
<div id="foot">我是底部内容</div>
# 预留编写脚本的block
{% block myjs %}{% endblock %}
</body>
</html>

在上面的模板文件中通过 style 标签为网页添加了一些样式,并在网页名字(title)、样式、中间内容、脚本上“挖了坑”,表示这是可变化的。

那么在子模板中通过网页名、样式、中间内容、脚本的 block 标签来填充子模板页面的内容展示,这一步可称之为“填坑”。

{% extends 'base.html' %}
# 填充网页名的block
{% block title %}子模板网页名{% endblock %}
# 填充样式的block
{% block mycss %}
<style>
#middle{
background-color: chocolate;
}
.div1 {
width: 33%;
height: 50px;
float: left;
}
</style>
{% endblock %}
# 填充脚本的block
{% block myjs %}
<script>
btn=document.getElementById('btn')
btn.onclick=(function () {
alert('欢迎学习Flask框架')
})
</script>
{% endblock %}
# 填充中间内容的的block
{% block middle %}
<div class="div1" id="d1"></div>
<div class="div1"></div>
<div class="div1"></div>
{% endblock %}

那么只需要编写两个简单的路由及其视图函数即可得到结果:

# 省略部分代码
# 展示父模板页面
@app.route('/father')
def father():
return render_template('father.html')
# 展示子模板页面
@app.route('/son')
def son():
return render_template('son.html')

另外,模板继承使用时注意点:

比如:

{# 相对路径 #}
<link rel="stylesheet" href="../static/css/style.css">
{# url_for #}
<link rel="stylesheet" href="{{ url_for('static',filename='css/style.css') }}">
<img src="{{ url_for('static',filename='images/封面.png') }}">

一般情况下,我们都是使用url_for方法导入,而且把脚本、样式、图片等文件放在 Flask 项目中的静态文件夹 static 下,如下图所示:

image.png

包含(include)

在 A、B、C 页面都有共同的部分,而其他页面没有这个部分,这个时候就可以考虑使用 include 包含,它的功能是将另一个模板整个加载到当前模板中,并直接渲染。语法格式为:

{% include '文件夹/模板文件' %}

包含的步骤为:

  1. 定义一个公共部分文件夹,再在文件夹中创建公共部分的模板;
  2. 哪个页面有该公共部分的内容,就通过 include 语法使用该公共部分的模板。

举个例子:

首先创建一个公共部分文件夹(common)及模板,模板文件代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>头部</title>
</head>
<body>
<h1>我被包含过来了</h1>
</body>
</html>

公共模板有了,现在编写一个welcome.html来使用公共模板页面,其代码如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>欢迎页面</title>
</head>
<body>
{# 使用公共模板 #}
{% include 'common/header.html' %}
<div >欢迎学习Flask框架</div>
</body>
</html>

然后再编写一个简单的视图函数即可:

@app.route('/welcome')
def welcome():
return render_template('welcome.html')

那么此时本地调试,进入 URL: 127.0.0.1:5000/welcome,即可得到如下结果:

image.png

是不是稍微对继承有一点点感觉的区分,也就是你上面被模板复用三种类型解释被绕晕的那里。

对宏(macro)的理解:

定义宏(macro)有两种方式:直接在模板中定义宏和把所有用写在一个宏模板文件中。其语法格式如下:

#定义宏
{% macro 宏名(参数) %}
代码块
{% endmacro %}
#导入宏
{% import '宏文件' as 别名 %}
#使用宏
{{ 别名.宏名(参数) }}

在模板中直接定义宏

首先我们创建一个名为macro1.html模板文件,其文件内容如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>宏的定义</title>
</head>
<body>
{# 定义宏 #}
{% macro input(value) %}
<input type="submit" placeholder="{{ value }}">
{% endmacro %}
{# 调用宏 #}
{{ input('提交') }}
</body>
</html>

这里我们定义了一个名为 input 的宏,其作用是创建一个提交按钮,然后通过 {{ input('参数') }} 调用了宏。编写视图函数,主要代码如下所示:

@app.route('/macro')
def use_macro():
return render_template('macro1.html')

本地调试,结果如下:

image.png

把所有宏定义在一个宏文件中

首先我们创建一个macro.html宏文件,其内容如下所示:

{% macro input(value) %}
<input type="text" placeholder="{{ value }}" name="username">
{% endmacro %}

然后创建一个模板文件来使用 macro.html 宏文件,其内容如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>宏的使用</title>
</head>
<body>
{% import 'macro.html' as f %}
{{ f.input('用户名') }}
</body>
</html>

这里我们通过 import 导入了宏文件,通过 as 给宏起了别名 f,然后通过 f.input() 调用了宏的方法,视图函数代码如下所示:

@app.route('/macro1')
def use_macro1():
return render_template('macro2.html')

本地调试结果如下:

image.png

总结一下

这三个例子能不能从上面从代码复用的三种方法解释中绕出来,而实际上,有更好的总结:


至此关于模板的事情先说到这里 👋 ↑Top

Some rights reserved
Except where otherwise noted, content on this page is licensed under a Creative Commons Attribution-NonCommercial 4.0 International license.