Flask框架——模板:分离数据与视图

作者:chen_h
微信号 & QQ:862251340
微信公众号:coderpai


Flask 框架学习目录


1. 概述

Flask框架的基本定位是开发服务端的动态网页应用。因此,模板是必不可少的环节。 Flask使用的是Jinja2模板引擎,因此本节课程中的模板语法,基本遵从Jinja的文档。

在本节课程中,主要从以下几个方面讲解Flask框架中的模板:

  • Flask的模板引擎

  • 两种模板渲染的方法

  • 在模板中使用变量和表达式

  • 在模板中使用全局对象

  • 自定义模板全局对象

  • 在模板中使用过滤器

  • 自定义模板过滤器

  • 模板变量的转义问题

  • 在模板中使用循环结构

  • 在模板中使用递归循环结构

  • 在模板中使用条件结构

2. 模板引擎

在Flask中,视图函数的返回值为响应的正文被送往前端浏览器。毫无疑问,一个实用 的视图函数需要在服务端根据请求的不同动态构造这个内容。然而手工拼接一段冗长 的HTML串是乏味而且相当容易出错。

这正是模板引擎发挥威力的地方,只需要将模板数据送入模板引擎,我们就告 别了那些那些拼接、转义之类的琐碎之事,轻松得到一个渲染后的字符串:

在Flask中,使用模板引擎基本就是分这三个步骤:

第一、声明数据

uname = 'WHOAMI'
````

第二、编写模板





<div class="se-preview-section-delimiter"></div>

tpl = ‘

Welcome,{{username}}


第三、提交模板引擎渲染





<div class="se-preview-section-delimiter"></div>

render_template_string(tpl,username=uname)


实验代码如下:





<div class="se-preview-section-delimiter"></div>

-- coding:utf-8 --

from flask import Flask,render_template_string
app = Flask(name)
@app.route(‘/’)
def v_index():
uname = ‘WHOAMI’
tpl = ‘

Welcome,{{username}}


return render_template_string(tpl,username=uname)

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(‘

Welcome,{{ uname }}

‘,uname=uname)


在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>


Welcome, {{ username }}







<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 应用中的函数。





<div class="se-preview-section-delimiter"></div>

## 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中提取当前用户名:





<div class="se-preview-section-delimiter"></div>

@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=’

jimi hendrix

‘) #Jimi Hendrix


**过滤器参数** :可以使用小括号为过滤器传入额外的参数。下面的示例将列表型变量 的多个成员使用*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


{% endfilter %}
”’
render_template_string(tpl)

在块过滤器上也可以使用多个过滤器进行*级联*。下面的示例首先对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


{% endfilter %}
”’
render_template_string(tpl)





<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 = ‘

homepage of {{nickname}}


render_template_string(tpl,**user)


**页面变形** : 另一种轻微一些但更常见的问题是用户提交的数据中包含具有 特殊意义的HTML字符,比如*<*、*>*、*&*等。下面的示例中,用户 的昵称恰好看起来恰好是一个HTML标签,导致其个人页面中不能显示昵称:





<div class="se-preview-section-delimiter"></div>

user = {‘id’:123,’nickname’:’< IAMKING>’}
tpl = ‘

homepage of {{nickname}}


render_template_string(tpl,**user)






<div class="se-preview-section-delimiter"></div>

## 11. 变量转义

解决这些问题的办法就是对变量执行*转义*操作,将变量中的具有特殊含义的HTML字符 使用HTML实体码表示。例如:*<IAMKING>*将被转换为*<IAMKING>*。

**自动转义** : 在模板中使用*autoescape*标签可以开启或关闭模板引擎的自动转义 功能。在开启自动转义功能时,模板引擎将对转义块内的所有变量自动执行转义操作。

下面的示例中,使用*autoescape*标签开启了自动转义:





<div class="se-preview-section-delimiter"></div>

user = {‘id’:123,’nickname’:’< IAMKING>’}
tpl = ”’
{% autoescape true %}

homepage of {{nickname}}


{% endautoescape %}
”’
render_template_string(tpl,**user)


但是自动转义开启的时候,会对转义块内所有的变量执行转义操作,即是这些变量压根 不可能包含HTML字符,或者其内容可控。当变量数量很多时,这将造成不必要的性能损失。

我们可以使用*safe*过滤器将这些可控的变量标记为安全的,渲染引擎将不再对其进行转义。 下面的示例中,使用*safe*标签取消*id*变量的转义操作:





<div class="se-preview-section-delimiter"></div>

user = {‘id’:123,’nickname’:’< IAMKING>’}
tpl = ”’
{% autoescape true %}

homepage of {{nickname}}


{% endautoescape %}
”’
render_template_string(tpl,**user)


**手动转义** :和自动转义对应的就是手动的对变量执行转义操作。方法是使用*escape* 过滤器,可以简写为*e*。

下面的示例中,对模板中的*nickname*变量执行手动转义:





<div class="se-preview-section-delimiter"></div>

user = {‘id’:123,’nickname’:’< IAMKING>’}
tpl = ‘

homepage of {{nickname | e }}


render_template_string(tpl,**user)






<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 = ”’


    {{% for user in users %}}
  • {{ user.name }}
  • {{% endfor %}}

”’
render_template_string(tpl,users=data)


**迭代过滤** :Jinja2的for循环不能像Python一样中途*退出/break*或*跳过/continue*, 但是它支持在迭代时进行*条件过滤*。下面的示例模板只为年龄大于25的用户生成列表项:





<div class="se-preview-section-delimiter"></div>

tpl = ”’


    {{% for user in users if user.age > 25 %}}
  • {{ user.name }}
  • {{% endfor %}}

”’
render_template_string(tpl,users=data)


**默认输出块** :如果没有执行至少一次循环(比如列表为空,或者被过滤了), 可以使用*else*块生成默认的输出。下面的示例模板将在没有用户匹配时输出*not found*:





<div class="se-preview-section-delimiter"></div>

tpl = ”’


    {{% for user in users if user.age > 50 %}}
  • {{ user.name }}
  • {{% else %}}
  • not found!
  • {{% endfor %}}

”’
render_template_string(tpl,users=data)






<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开始。下面的示例将输出110:





<div class="se-preview-section-delimiter"></div>

{% for i in range(10) %}
{{ loop.index }}
{% endfor %}


**loop.index0** :当前执行的循环序号,从0开始。

**loop.revindex** :当前执行的循环反序序号,从1开始。下面的示例将输出101:





<div class="se-preview-section-delimiter"></div>

{% for i in range(10) %}
{{ loop.revindex }}
{% endfor %}


**loop.revindex0** :当前执行的循环反序序号,从0开始

**loop.first** :如果当次执行是循环中的首次,则值为True。下面的示例将输出TrueFalseFalse....





<div class="se-preview-section-delimiter"></div>

{% for i in range(10) %}
{{ loop.index }}
{% endfor %}


**loop.last** :如果档次执行时循环中的最后一次,则值为True

**loop.length** :列表中的元素数量

**loop.cycle(*args)** :从一个列表中循环取值。下面的示例将循环输出c1、c2、c3、c1、c2、c3...





<div class="se-preview-section-delimiter"></div>

{% 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 %}

some adult content…

{% endif %}
”’
render_template_string(tpl,user=data)


**elif** :可以为条件块添加使用*elif*添加多重条件判断分支:





<div class="se-preview-section-delimiter"></div>

{% 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%}

some health preserving content…

{% elif user.age >= 18 %}
some adult content…

{% endif %}
”’
render_template_string(tpl,user=data)
“`

else :当条件块中的所有条件都不满足时,可以使用else添加默认输出块:

猜你喜欢

转载自blog.csdn.net/CoderPai/article/details/80520960