作者:chen_h
微信号 & QQ:862251340
微信公众号:coderpai
快速入门
路由:URL规则与视图函数
请求、应答与会话 :Request/Response/Session
模板:分离数据与视图
访问数据库 :SQLAlchemy简介
Flask框架的基本定位是开发服务端的动态网页应用。因此,模板是必不可少的环节。 Flask使用的是Jinja2模板引擎,因此本节课程中的模板语法,基本遵从Jinja的文档。
在本节课程中,主要从以下几个方面讲解Flask框架中的模板:
Flask的模板引擎
两种模板渲染的方法
在模板中使用变量和表达式
在模板中使用全局对象
自定义模板全局对象
在模板中使用过滤器
自定义模板过滤器
模板变量的转义问题
在模板中使用循环结构
在模板中使用递归循环结构
在模板中使用条件结构
在Flask中,视图函数的返回值为响应的正文被送往前端浏览器。毫无疑问,一个实用 的视图函数需要在服务端根据请求的不同动态构造这个内容。然而手工拼接一段冗长 的HTML串是乏味而且相当容易出错。
这正是模板引擎发挥威力的地方,只需要将模板和数据送入模板引擎,我们就告 别了那些那些拼接、转义之类的琐碎之事,轻松得到一个渲染后的字符串:
在Flask中,使用模板引擎基本就是分这三个步骤:
第一、声明数据
uname = 'WHOAMI'
````
第二、编写模板
<div class="se-preview-section-delimiter">div>
tpl = ‘
第三、提交模板引擎渲染
<div class="se-preview-section-delimiter">div>
render_template_string(tpl,username=uname)
实验代码如下:
<div class="se-preview-section-delimiter">div>
from flask import Flask,render_template_string
app = Flask(name)
@app.route(‘/’)
def v_index():
uname = ‘WHOAMI’
tpl = ‘
app.run(host=’0.0.0.0’,port=8080)
实验页面如下:
![](http://upload-images.jianshu.io/upload_images/1155267-1d7d3436ebd3e84e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
<div class="se-preview-section-delimiter">div>
## 3. 模板渲染
Flask基于Jinja2模板引擎,提供了两个渲染函数,分别使用字符串或单独的文件保存模板内容:
* render_template_string(sourcestr,**context) - 使用sourcestr作为模板字符串
* render_template(filename,**context) - 使用filename指定的文件内容作为模板字符串
**render_template_string** :下面的示例使用一个相当简单的模板,向不同的用户回 送个性化的欢迎信息:
<div class="se-preview-section-delimiter">div>
@app.route(‘/user/’)
def show_user_profile(uname):
return render_template_string(‘
在Jinja2的语法中,*{{varibale}}*表示一个输出命令,每当渲染引擎发现一个输出命令,它就 在渲染结果中,使用模板数据上下文中变量*variable*的值替换原始的输出命令。
**render_template** :在生产环境中,在代码里写模板字符串不是什么好主意。正经的方法是将模板写在 单独的模板文件里,使用render_template()函数进行渲染:
<div class="se-preview-section-delimiter">div>
@app.route(‘/user/’)
def v_user(username):
return render_template(‘user.html’,username=username)
默认情况下,Flask使用当前模块文件夹下的*templates*子目录作为模板目录,user.html文件 应当放置在这个文件夹下:
<div class="se-preview-section-delimiter">div>
/app
/web.py
/templates
/user.html
user.html的内容如下:
<div class="se-preview-section-delimiter">div>
<div class="se-preview-section-delimiter">div>
## 4. 变量与表达式
模板变量在模板被渲染时通过*上下文字典*传递给模板。下面的示例中,在模板中使用了 变量*name*和*age*,当调用*render_template_string()*渲染模板时,通过关键字参数 将两个变量的值传递进来:
<div class="se-preview-section-delimiter">div>
tpl = ‘name : {{ name }} age : {{age}} ’
print render_template_string(tpl,name=’Marion5’,age=12)
**变量成员** :如果传入模板的变量是不是Python简单类型,而是比如字典或对象类型, 那么在模板中可以向Python中一样的方式访问其成员属性或方法。
稍有不同的是,对于字典变量,除了可以使用*[]*方式访问其成员,还可以使用*.*:
<div class="se-preview-section-delimiter">div>
tpl = ‘name: {{u[“name”]}} name again:{{u.name}}’
print render_template_string(tpl,u={‘name’:’Marion5’,’age’:12})
同样的,对于对象变量,除了使用*.*访问属性值,还可以使用*[]*:
<div class="se-preview-section-delimiter">div>
class User:
def init(self,name,age):
self.name = name
self.age = age
tpl = ‘name : {{u.name}} name again:{{u[“name”]}}’
print render_template_string(tpl,u=User(‘Mary’,20))
**表达式** :变量还可以应用表达式,比如进行数学运算,那些常用的数学 操作符( + - * / // % ** )都是有效的:
<div class="se-preview-section-delimiter">div>
data = {‘x’:12,’y’:13}
tpl = ‘{{x}} + {{y}} = {{ x+y }}’
print render_template_string(tpl,**data)
或者进行比较或逻辑运算( == != > >= < <= and or not):
<div class="se-preview-section-delimiter">div>
data = {‘x’:12,’y’:13,’z’:11}
tpl = ‘{{x}} > {{y}} : {{ x>y }}’
print render_template_string(tpl,**data)
**函数调用** :在输出命令中,可以对变量或常量进行函数调用:
<div class="se-preview-section-delimiter">div>
tpl = ‘{{ range(10) }} ’
print render_template_string(tpl)
需要注意的是,模板有自己的*全局域/globals*,因此这里的*range()*函数并不是Python 应用中的函数。
## 5. 全局对象
Jinja2内置的全局对象包括:
* range([start, ]stop[, step])
* lipsum(n=5, html=True, min=20, max=100)
* dict(**items)
* class cycler(*items)
* class joiner(sep=', ')
Flask向Jinja2模板注入了以下全局对象,可以在模板中直接访问:
* config - 当前Flask应用实例的配置对象
* request - 当前HTTP请求对象
* session - 当前HTTP请求的会话对象
* g - 当前HTTP请求周期内的全局对象
* url_for() - URL生成函数
* get_flashed_messages() - 闪信函数
下面的示例中,从session中提取当前用户名:
@app.route(‘/’)
def v_index():
tpl = ‘welcome back, {{ session.username }}’
return render_template_string(tpl)
<div class="se-preview-section-delimiter">div>
## 6. 自定义全局对象
可以使用应用对象的context_processor装饰器向引擎注入额外的全局对象。 下面的示例向模板全局域中注入*vendor*变量,其值为*hubwiz*:
<div class="se-preview-section-delimiter">div>
@app.context_processor
def vendor_processor():
return dict(vendor=’hubwiz’)
这时我们可以在模板中直接使用*vendor*变量了:
<div class="se-preview-section-delimiter">div>
@app.route(‘/’)
def v_index():
tpl = ‘powered by {{vendor}}’
return render_template_string(tpl)
当然,同样的方法可以用于注入全局函数。下面的示例向模板全局域中注入*format_price* 函数:
<div class="se-preview-section-delimiter">div>
@app.context_processor
def utility_processor():
def format_price(amount, currency=u’€’):
return u’{0:.2f}{1}’.format(amount, currency)
return dict(format_price=format_price)
<div class="se-preview-section-delimiter">div>
## 7. 过滤器
模板中可以使用过滤器*|*来修改变量的值。下面的示例使用内置的*title*过滤器 将*name*变量中每个单词的首字母转换为大写:
<div class="se-preview-section-delimiter">div>
tpl = ‘{{ name|title }}’
print render_template_string(tpl,name=’jimi hendrix’) #Jimi Hendrix
**过滤器级联** :可以将多个过滤器串联起来,构成过滤流水线。下面的示例对name 变量依次使用了两个过滤器,*scriptags*过滤器用来除去name变量中的HTML标签, *title*过滤器将传入的字符串中每个单词的首字母转换为大写:
<div class="se-preview-section-delimiter">div>
tpl = ‘{{ name|striptags|title }}’
print render_template_string(tpl,name=’
**过滤器参数** :可以使用小括号为过滤器传入额外的参数。下面的示例将列表型变量 的多个成员使用*join*过滤器连接起来:
<div class="se-preview-section-delimiter">div>
tpl = ‘{{ seq | join(“-“) }}’
print render_template_string(tpl,seq=[1,2,3]) # 1-2-3
在Jinja2中,一个过滤器其实就是一个函数,第一个参数用来接收前序环节传入的值,而 返回值则作为后续环节过滤器函数的第一个参数:
![](http://upload-images.jianshu.io/upload_images/1155267-7d9c560a0e0a41ea.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
Jinja2内置了很多过滤器,在其官网[文档页](http://jinja.pocoo.org/docs/dev/templates/#builtin-filters) 可以了解详细情况。
<div class="se-preview-section-delimiter">div>
## 8. 定制过滤器
我们已经知道,过滤器其实就是一个函数。在Flask中,可以使用*Flask.template_filter* 装饰器创建自己的过滤器。下面的示例创建了一个名为*reverse*的串反转过滤器,它总是 将输入的字符串逆向重排:
<div class="se-preview-section-delimiter">div>
@app.template_filter(‘reverse’)
def reverse_filter(s):
return s[::-1]
下面的示例演示了如何调用我们自制的过滤器:
<div class="se-preview-section-delimiter">div>
@app.route(‘/’)
def index():
return render_template_string(‘{{ greeting | reverse }}’,greeting=’Hello, Jinja2’ )
另一种等价地创建定制过滤器的方法是将过滤器函数添加到Flask应用实例的*jinja_env*字典中:
<div class="se-preview-section-delimiter">div>
def reverse_filter(s):
return s[::-1]
app.jinja_env.filters[‘reverse’] = reverse_filter
<div class="se-preview-section-delimiter">div>
## 9. 块过滤器
块过滤器可以在整块内容上应用指定的过滤器:
<div class="se-preview-section-delimiter">div>
{% filter [filtername] %}
…
{% endfilter %}
下面的示例将filter块内的模板内容使用*upper*过滤器全部转换成大写:
<div class="se-preview-section-delimiter">div>
tpl = ”’
{% filter upper %}
Filter sections allow you to apply regular Jinja2 filters on a block
of template data. Just wrap the code in the special filter section
在块过滤器上也可以使用多个过滤器进行*级联*。下面的示例首先对filter块内容使用*escape* 过滤器进行转义,然后使用*upper*过滤器将其转换为大写:
<div class="se-preview-section-delimiter">div>
tpl = ”’
{% filter escape | upper %}
Filter sections allow you to apply regular Jinja2 filters on a block
of template data. Just wrap the code in the special filter section
<div class="se-preview-section-delimiter">div>
## 10. 模板中的变量风险
模板引擎的基本工作是依据给出的*未知的*数据上下文,结合模板生成HTML串。 考虑到结果HTML串将要在客户端的浏览器中运行,这里面存在着诸多的隐患。
**XSS** :如果模板变量来自于用户输入,那么存在被恶意访问者注入用于跨站攻击脚本的风险。
在下面的示例中,数据上下文*user*的内容来自于数据库中,而昵称/nickname 的值是允许用户自己修改的。一个恶意的访问者在自己的昵称中掺杂了脚本,当 任意用户访问该用户的个人页面时,都将被弹窗:
<div class="se-preview-section-delimiter">div>
user = {‘id’:123,’nickname’:’haha‘}
tpl = ‘
**页面变形** : 另一种轻微一些但更常见的问题是用户提交的数据中包含具有 特殊意义的HTML字符,比如*<*、*>*、*&*等。下面的示例中,用户 的昵称恰好看起来恰好是一个HTML标签,导致其个人页面中不能显示昵称:
<div class="se-preview-section-delimiter">div>
user = {‘id’:123,’nickname’:’< IAMKING>’}
tpl = ‘
<div class="se-preview-section-delimiter">div>
## 11. 变量转义
解决这些问题的办法就是对变量执行*转义*操作,将变量中的具有特殊含义的HTML字符 使用HTML实体码表示。例如:**将被转换为**。
**自动转义** : 在模板中使用*autoescape*标签可以开启或关闭模板引擎的自动转义 功能。在开启自动转义功能时,模板引擎将对转义块内的所有变量自动执行转义操作。
下面的示例中,使用*autoescape*标签开启了自动转义:
<div class="se-preview-section-delimiter">div>
user = {‘id’:123,’nickname’:’< IAMKING>’}
tpl = ”’
{% autoescape true %}
但是自动转义开启的时候,会对转义块内所有的变量执行转义操作,即是这些变量压根 不可能包含HTML字符,或者其内容可控。当变量数量很多时,这将造成不必要的性能损失。
我们可以使用*safe*过滤器将这些可控的变量标记为安全的,渲染引擎将不再对其进行转义。 下面的示例中,使用*safe*标签取消*id*变量的转义操作:
<div class="se-preview-section-delimiter">div>
user = {‘id’:123,’nickname’:’< IAMKING>’}
tpl = ”’
{% autoescape true %}
**手动转义** :和自动转义对应的就是手动的对变量执行转义操作。方法是使用*escape* 过滤器,可以简写为*e*。
下面的示例中,对模板中的*nickname*变量执行手动转义:
<div class="se-preview-section-delimiter">div>
user = {‘id’:123,’nickname’:’< IAMKING>’}
tpl = ‘
<div class="se-preview-section-delimiter">div>
## 12. 循环结构
假设我们有一组用户数据如下:
<div class="se-preview-section-delimiter">div>
data = [
{‘name’ : ‘John’,’age’ : 20,},
{‘name’ : ‘Linda’,’age’ : 21},
{‘name’ : ‘Mary’,’age’ : 30},
{‘name’ : ‘Cook’,’age’ : 40}
]
可以使用循环结构,对一组数据使用单一模板进行渲染:
<div class="se-preview-section-delimiter">div>
{% for [loop condition] %}
…
{% endfor%}
下面的示例对列表中的每一个对象生成一个*<li>*标签:
<div class="se-preview-section-delimiter">div>
tpl = ”’
**迭代过滤** :Jinja2的for循环不能像Python一样中途*退出/break*或*跳过/continue*, 但是它支持在迭代时进行*条件过滤*。下面的示例模板只为年龄大于25的用户生成列表项:
<div class="se-preview-section-delimiter">div>
tpl = ”’
**默认输出块** :如果没有执行至少一次循环(比如列表为空,或者被过滤了), 可以使用*else*块生成默认的输出。下面的示例模板将在没有用户匹配时输出*not found*:
<div class="se-preview-section-delimiter">div>
tpl = ”’
<div class="se-preview-section-delimiter">div>
## 13. 递归循环
有些数据是具有不确定层次的递归数据,比如文件系统,目录里还有目录:
<div class="se-preview-section-delimiter">div>
/application —— 目录
/app.py —— 文件
/static —— 目录
/main.css —— 文件
/jquery.min.css —— 文件
/templates —— 目录
/user.html —— 文件
其对应的数据表达参见示例中的*tree*对象。Jinja2的循环结构支持*递归*调用。使用方法如下:
<div class="se-preview-section-delimiter">div>
#### 13.1 使用*recursive*关键字声明循环为*递归*循环
<div class="se-preview-section-delimiter">div>
{% for item in data recursive}
…
{% endfor %}
<div class="se-preview-section-delimiter">div>
#### 13.2 在循环内部,使用*loop()*函数调用子节点
<div class="se-preview-section-delimiter">div>
{{ loop(item.children) }}
<div class="se-preview-section-delimiter">div>
## 14. 循环块中的特殊变量
在for循环块中,Jinja2提供了关于循环的一些特殊变量:
**loop.index** :当次执行的循环序号,从1开始。下面的示例将输出1至10:
<div class="se-preview-section-delimiter">div>
{% for i in range(10) %}
{{ loop.index }}
{% endfor %}
**loop.index0** :当前执行的循环序号,从0开始。
**loop.revindex** :当前执行的循环反序序号,从1开始。下面的示例将输出10至1:
class="se-preview-section-delimiter">
{% for i in range(10) %}
{{ loop.revindex }}
{% endfor %}
**loop.revindex0** :当前执行的循环反序序号,从0开始
**loop.first** :如果当次执行是循环中的首次,则值为True。下面的示例将输出True、False、False....
class="se-preview-section-delimiter">
{% for i in range(10) %}
{{ loop.index }}
{% endfor %}
**loop.last** :如果档次执行时循环中的最后一次,则值为True
**loop.length** :列表中的元素数量
**loop.cycle(*args)** :从一个列表中循环取值。下面的示例将循环输出c1、c2、c3、c1、c2、c3...
class="se-preview-section-delimiter">
{% for i in range(10) %}
{{ loop.cycle(‘c1’,’c2’,’c3’) }}
{% endfor %}
**loop.depth** :递归循环的层深,从1开始
**loop.depth0** :递归循环的层深,从0开始
<div class="se-preview-section-delimiter">div>
## 15. 条件结构
在Jinja2中,可以使用*条件块*设置模板内容的输出条件。只有当指定的条件 满足时,条件块内的模板内容才会被渲染输出:
<div class="se-preview-section-delimiter">div>
{% if [condition] %}
…
{% endif %}
下面的示例中,只有当用户的年龄不小于18岁时,才输出适合成人观看的内容:
<div class="se-preview-section-delimiter">div>
data = {‘name’:’Obama’,age:62}
tpl = ”’
{% if user.age >= 18 %}
**elif** :可以为条件块添加使用*elif*添加多重条件判断分支:
class="se-preview-section-delimiter">
{% if [condition] %}
…
{% elif [condition2] %}
…
{% elif [condition3] %}
…
{% endif%}
下面的示例中,当用户的年龄大于60岁时,输出养生节目,大于18岁而小于60岁时,输出成人节目:
<div class="se-preview-section-delimiter">div>
data = {‘name’:’Obama’,age:62}
tpl = ”’
{% if user.age >= 60%}
else :当条件块中的所有条件都不满足时,可以使用else添加默认输出块: