目录
回顾
在这之前的示例里面,视图函数的主要作用是生成请求的响应,这是最简单的请求。实际上我们也涉及到了视图函数的两个作用:处理业务逻辑和返回响应内容。但是在大型应用中,把业务逻辑和表现内容放在一起,会增加代码的复杂度和维护成本。而模板就能承担视图函数中返回响应内容这一作用,从而整个系统耦合低,整个代码结构更加清晰。
实际上我们已经使用过 render_template()
方法来渲染模板,下面是更加系统化的学习模板这一概念。
模板初识
- 模板其实是一个包含响应文本的文件,其中用占位符(变量)表示动态部分,告诉模板引擎其具体的值需要从使用的数据中获取
- 使用真实值替换变量,再返回最终得到的字符串,这个过程称为“渲染”
- Flask 是使用 Jinja2 这个模板引擎来渲染模板
- Jinjia2 是 Flask 框架内置的模板语言,设计思想来源于 Django 的模板引起,在其基础上扩展了语法和其他更加强大的功能
- 所谓模板语言就是一种可以自动生成文档的简单文本格式,在模板语言中,一般会把一些变量传给模板,替换模板的特定位置上预先定义好的占位变量名。
- Flask 提供的
render_template()
函数封装了该模板引擎 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)
这里注意一下:
- 由于
render_template()
默认是从 templates 文件夹中获取模板文件(在初始化类的时候已经默认),所以我们一般都是把模板文件放在templates 文件夹中,当然你也可以在 templates 文件夹中再新建一个文件夹,但此时render_template()
函数的第一个变量就要将路径稍微修改一下,这一点后面会讲到 - 所谓:
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 %}
这里需要注意的是:
- for 和 endfor 要成对出现
- 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] |
自定义过滤器
自定义过滤器的出现也很简单,就像系统内置的函数一样,虽然有现成的,但是终归不能符合现实需求,所以我们可以自定义过滤器,在这里你可以理解为自定义函数。
而自定义过滤器的方法也是和添加路由的方法基本类似有两种:
- 一种是通过 Flask 应用对象的
add_template_filter()
方法 - 通过装饰器来实现自定义过滤器
语法大概如下:
# 使用第一种方法
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>
我个人还是习惯使用第二种方法自定义过滤器,即通过装饰器来实现自定义过滤器,不会有一种多此一举的感觉。
模板复用
有了上面模板的基础知识与使用,我们已经能够做出基本的系统,但是你会发现当你做一个系统的时候,很多页面(模板)有类似的情况:
- 多个模板具有完全相同的顶部和底部内容
- 多个模板中具有相同的模板代码内容,但是内容中部分值不一样
- 多个模板中具有完全相同的 html 代码块内容
别被绕晕了,其实就是这样一个问题:模板内容重复,那么是不是每个模板都要写这种内容相同的 HTML 代码? 所以就引出了模板复用这一个概念,像遇到这种情况,可以使用 JinJa2 模板中的 继承(extends)、包含(include)、宏(macro)来进行实现,有一定编程基础的(指使用面向对象编程语言的)一看这几个名词就非常熟悉,甚至已经知道是什么作用了。
继承(extends)
继承,字面意思,你可以理解为儿子继承父亲家产的意思,更巧的是,实际上在一般 Web 开发中,需要继承的部分定义在父模板中,子模板继承,而不需要重复书写,恰好是这个父子概念,语法如下:
#父模板
{% block 名字 %} #名字是任意的
{% endblock %}
#子模板
{% extends '父模板的名字' %}
继承并非 “一时兴起”,选择继承之前你要有全盘计划,具体继承可以分为两步:
定义父模板:
- 定义一个 base.html 模板
- 分析模板中哪些是变化,对变化部分用标签语法 block 进行挖坑处理(预留位置)
- 一般情况下,样式 CSS 和脚本 JS 是需要提前预留的地方,按照正常开发也是这样的道理
子模板继承父模板
- 继承父模板内容
- 通过 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')
另外,模板继承使用时注意点:
- 不支持多继承
- 为了便于阅读,在子模板中使用 extends 时,尽量写在模板的第一行。
- 不能在一个模板文件中定义多个相同名字的 block 标签。
- 当在页面中使用多个 block 标签时,建议给结束标签起个名字,当多个 block 嵌套时,阅读性更好。
- 当我们需要导入脚本、样式或者图片等其他内容,而不是在父、子模板中编写脚本、样式呢。这时我们可以使用 link标签、img 标签通过 url_for 或者通过相对路径导入。
比如:
{# 相对路径 #}
<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 下,如下图所示:
包含(include)
在 A、B、C 页面都有共同的部分,而其他页面没有这个部分,这个时候就可以考虑使用 include 包含,它的功能是将另一个模板整个加载到当前模板中,并直接渲染。语法格式为:
{% include '文件夹/模板文件' %}
包含的步骤为:
- 定义一个公共部分文件夹,再在文件夹中创建公共部分的模板;
- 哪个页面有该公共部分的内容,就通过 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,即可得到如下结果:
是不是稍微对继承有一点点感觉的区分,也就是你上面被模板复用三种类型解释被绕晕的那里。
宏
对宏(macro)的理解:
- 把它看作 Jinja2 中的一个函数,它会返回一个模板或者 HTML 字符串
- 为了避免反复地编写同样的模板代码,出现代码冗余,可以把他们写成函数以进行重用
- 需要在多处重复使用的模板代码片段可以写入单独的文件,再包含在所有模板中,以避免重复
定义宏(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')
本地调试,结果如下:
把所有宏定义在一个宏文件中
首先我们创建一个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')
本地调试结果如下:
总结一下
这三个例子能不能从上面从代码复用的三种方法解释中绕出来,而实际上,有更好的总结:
- 宏 (Macro)、继承 (Block)、包含 (include) 均能实现代码的复用。
- 继承 (Block) 的本质是代码替换,一般用来实现多个页面中重复不变的区域。
- 宏 (Macro) 的功能类似函数,可以传入参数,需要定义、调用。
- 包含 (include) 是直接将目标模板文件整个渲染出来。
至此关于模板的事情先说到这里 👋 ↑Top