Flask
Flask 是一个微型的 Python 开发的 Web 框架,基于Werkzeug WSGI工具箱和 Jinja2 模板引擎。
一. 初识Flask
最小的Flask程序
1
2
3
4
5
6
7
8
9from flask import Flask #从flask包中导入Flask类
app = Flask(__name__)
#注册路由
def index(): #视图函数
return 'hello world!'
app.run(debug=True) #启动flask,通过参数debug启动调试模式,便于将修改的代码即时更新到web中注册路由:建立处理请求的函数,并为其定义对应的URL规则。只需为函数添加
app.route()
装饰器,并传入URL规则作为参数,我们就可以让URL与函数建立关联。1
2
3
4
5
6
7
8
9#app.route(路径)
def index1():
代码
#app.add_url_rule(路径,view_func=视图函数名)
def index2():
代码
app.add_url_rule('/',view_func=index2)启动Flask程序:
app.run()
方法,flask run
1
2
3
4
5
6
7app.run(host='0.0.0.0',debug=True,port=80)
# 0.0.0.0表示接受任意ip访问
#在Terminal中用flask命令启动
flask run 启动
flask run --host=0.0.0.0
flask run --port=80为一个视图绑定多个URL :
1
2
3
4
5
6
def hello():
name = request.args.get('name','Flask')
return 'Hello, %s !' % name动态URL实现: 在URL规则中添加变量部分,使用
<变量名>
的形式表示。1
2
3
4
5
6
7
8
9
10
11#可在app.route()装饰器里使用defaults参数设置URL变量的默认值,该参数接收字典作为输入,存储URL变量和默认值的映射
def greet(name):
return 'hello, %s!' % name
#当然,也可以用更简便的方法写
def greet(name='brother'):
return 'hello, %s' % name项目配置:即配置一些大写形式的Python变量的值。flask中提供了很多方式来加载配置
1
2
3
4
5
6
7
8#使用app.config方法
app.config['ADMIN_NAME']='Brother'
#使用update()方法一次加载多个值
app.config.update(
TESTING =TRUE
SECRET_KEY='_5#yF4Q8z\n\xec]/'
)Flask命令: 可以通过
flask --help
查看flask当前所有命令1
2
3
4
5
6
7
8#启动flask程序
flask run
#查看当前的路由列表
flask routes
#启动Python Shell(Python交互式解释器)
flask shell可以通过创建任意一个函数,并为其添加
app.cli.command()
装饰器,就可以注册一个新的自定义flask命令。1
2
3
4
5
6
7from flask import Flask
import click
def hello():
"""just say hello""" #注释说明信息
click.echo('Hello, Human!')注册的自定义命令也可以在
flask --help
的命令列表中看到,且列表中的命令说明即为函数中的注释说明信息。
二. Flask与HTTP
请求响应循环:即客户端发出请求,服务端接收请求并返回响应,客户端接收响应的循环。这是每一个web程序的基本工作模式。
具体一点:
- 即用户访问一个URL,浏览器生成对应的http请求,经由互联网发送请求报文到对应的Web服务器;
- Web服务器接收请求报文,通过WSGI将HTTP格式的请求数据转换成Flask程序能够使用的Python数据;
- Flask程序根据请求的URL执行对应的视图函数,获取函数返回值并生成响应。
- 响应经过WSGI转换生成HTTP响应,再经由Web服务器把响应报文传递给客户端。
- 客户端浏览器渲染响应报文中包含的HTML和CSS代码,并执行Javascript代码,最终把解析后的页面呈现在用户浏览器的界面中。
请求报文:包括请求方法、URL、协议版本、首部字段(header)以及内容实体。
HTTP常见的几种请求类型:
方法 说明 GET 获取资源 POST 创建或更新资源 PUT 创建或替换资源 DELETE 删除资源 HEAD 获得报文首部 OPTIONS 询问支持的方法 Request对象:Flask的请求对象request中封装了从客户端发来的请求报文,我们能从request对象中获取请求报文中的所有数据。
常见的request的属性和方法:
属性/方法 值/说明 url 完整URL path 请求资源路径 host 域名或主机ip args 解析后的查询字符串,可通过字典方式获取键值 cookies 包含所有随请求提交的cookies字典 data 包含字符串形式的请求数据 files 包含所有上传文件,可通过字典的形式获取文件 form 包含解析后的表单数据,通过其中input标签的name属性值作为键获取 values 结合了args和form属性的值 get_data() 获取请求中的数据,默认读取为字节字符串,将其中as_text设为True则返回解码后的unicode字符串 get_json() 作为json解析并返回数据 json 包含解析后的json数据,其内部调用get_json() headers 包含首部字段的对象,可以以字典形式操作 method 请求的HTTP方法 referer 请求的发起源URL,即Referer scheme 请求的URL模式(http或https) user_agent 用户代理,包含用户的客户端类型,操作系统类型等信息 实例—获取请求URL中的查询字符串:
1
2
3
4
5
6
7
8
9
10
11from flask import Flask, request
app = Flask(__name__)
def hello():
#name = request.args['name']
#如果在request对象中直接使用键作为索引获取数据时(如上),如果没有对应的键,则会报错。
#因此我们应使用get()方法来获取对应数据(如下),get()方法的第二个参数可以设置默认值
name = request.args.get('name','brother')
return 'hello, %s!' % name在Flask中处理请求:当收到请求后,Flask会根据请求报文中的URL从路由表中寻找相应的视图函数。当请求的URL与某个视图函数的URL规则匹配成功时,就调用对应的视图函数。用
flask routes
可查看当前程序中的路由表。设置监听的HTTP方法:可以在
app.route()
装饰器中用methods参数传入一个包含HTTP请求方法的可迭代对象。1
2
3
4#下面的视图函数同时监听GET请求和POST请求
def hello():
return 'hello'URL处理:Flask提供了一些转换器可以转换URL中传递的变量类型
转换器 说明 int 整型 float 浮点型 string 不包含斜线的字符串 path 包含斜线的字符串 any 匹配一系列给定值中的一个元素 uuid UUID字符串 1
2
3
4
5
6
7
8
9
10#下面的例子中即使用了 int 转换器,返回一个经过计算后的整型
def go_back(year):
return 'welcome to %d' % (2020-year)
#any转换器在用法上唯一特别,需要在转换器后添加括号给出可选值,若传入的字符不在any后面的可选值中,均会返回404错误响应
def color():
pass
#即当且仅当传入'red','blue','white'时,才会成功调用下面的视图函数请求钩子:有时需要对请求进行预处理和后处理,此时可利用Flask提供的的一些请求钩子,用来注册一个函数,使得该函数在请求处理的不同阶段执行。
钩子 说明 before_first_request 在处理第一个请求前运行 before_request 在处理每个请求前运行 after_request 在没有抛出处理异常的每个请求结束后运行 teardown_request 无论有没有抛出异常,在每个请求结束后运行 after_this_request 在视图函数内注册一个函数,会在这个请求结束后运行 例如:
1
2
3
def do_preprocess():
pass #这里的代码会在每个请求处理前执行下面是请求钩子的一些常见应用场景:
before_first_request:在运行程序前进行一些程序的初始化操作,如创建数据库表,添加管理员用户等。
before_request:比如网站上要记录用户最后在线的时间,可以记录用户最后发送请求的时间。
after_request:比如更新、插入数据库等操作,请求结束后需要将更改提交到数据库中。
响应报文:主要由协议版本、状态码(status code)、原因短语(reason phrase)、响应首部和响应主体组成。
HTTP状态码用来表示处理的结果。
在Flask中生成响应:响应在Flask中使用Response对象表示,响应报文中的大部分内容由服务器处理,多数情况下我们只负责返回主体内容。
视图函数最多可以返回三个元素:响应主体、状态码、首部字段。
比如,普通的响应可以只包含主体内容:
1
return 'Hello!'
默认的状态码为200,下面指定了不同的状态码:
1
return 'Hello!', 201
还可以附加或修改某个首部字段。如要生成状态码为302的重定向响应,就需要修改首部中Location字段的值,并将其设为要重定向的目标URL:
1
return '', 302,{'Location':'http://www.baidu.com'}
重定向:除了像以上例子手动生成302响应外,Flask还提供了一些辅助函数,如
redirect()
,重定向的目标URL作为第一个参数,如:1
2
3from flask import Flask,redirect
...
return redirect('http://www.baidu.com')错误响应:多数情况下,Flask会自动处理并返回常见的错误响应。当然也可以手动返回错误响应,可以使用Flask提供的
abort()
函数,在该函数中传入状态码即可返回对应的错误响应:1
2
3
4from flask import Flask,abort
...
abort(404)
#注意,abort()前不用写return,且一旦abort()函数被调用,其之后的代码将不会被执行响应格式:在HTTP响应中,数据可以通过多种格式传输。不同的响应数据格式需要设置不同的MIME类型,MIME的类型在首部Content-Type字段中定义。
以默认的HTML类型为例:
1
Content-Type: text/html; charset=utf-8
如果想使用其他的MIME类型,可以使用Flask提供的
make_response()
方法来生成响应对象,传入响应主体内容作为参数,然后使用响应对象的mimetype属性设置MIME类型。如:1
2
3
4
5
6
7from flask import make_response
def index():
response = make_response('hello,world!')
response.mimetype='text/plain'
return responseResponse类的常用属性和方法:
方法/属性 说明 headers 表示响应首部,可以像字典一样操作 status 状态码,文本类型 status_code 状态码,整型 mimetype MIME类型 set_cookie() 用来设置一个cookie 常用的数据格式:纯文本、HTML、XML、JSON。下列为了对比不同的数据类型,将会用不同的数据类型来表示同一个便签内容:Brother写给Ronie的一个提醒。
纯文本:text/plain
1
2
3
4
5Note
to: Ronie
from: Brother
heading: Reminder
body: Don't forget the party!HTML:text/html
1
2
3
4
5
6
7
8
9
10
11
<html>
<head></head>
<body>
<h1>Note</h1>
<p>to: Ronie</p>
<p>from: Brother</p>
<p>heading: Reminder</p>
<p>body:<strong>Don't forget the party!</strong></p>
</body>
</html>XML:application/xml
1
2
3
4
5
6
7
<note>
<to>Ronie</to>
<from>Brother</from>
<heading>Reminder</heading>
<body>Don't forget the party!</body>
</note>JSON:application/json
1
2
3
4
5
6
7
8{
"note":{
"to":"Ronie",
"from":"Brother",
"heading":"Reminder",
"body":"Don't forget the party!"
}
}Flask提供了方便的
jsonify()
函数,可以对我们传入的参数进行序列化,转换成JSON字符串作为响应主体,然后生成响应对象,并且设置正确的MIME类型:1
2
3
4
5from flask import jsonify
...
return jsonify(name='Brother',gender='male')
#jsonify()函数默认生成200响应,也可以附加状态码自定义相应类型,如下:
return jsonify(message='Error'), 500Cookie:HTTP是无状态协议,但某些Web程序必须记住客户端的某些信息,于是有了Cookie技术。Cookie技术通过在请求和响应报文中添加Cookie数据来保存客户端的状态信息。
添加Cookie信息:在Flask中,可以使用Response类提供的
set_cookie()
方法。需要先用make_response()
方法手动生成响应对象,传入响应主体作为参数。1
2
3
4
5
def setCookie(cookie):
response = make_response('You got a cookie!!!')
response.set_cookie('name',cookie)
return responseset_cookie()
方法支持多个参数来设置Cookie 的选项:属性 说明 key cookie 的键 value cookie的值 max_age cookie被保存的秒数;默认在用户会话结束时(即关闭浏览器)过期 expires 具体的过期时间,一个datetime对象或UNIX时间戳 path 限制cookie只在给定的路径可用,默认为整个域名 domain 设置cookie可用的域名 secure 如果设为True,只有通过https才能使用 httponly 如果设为True,禁止客户端用JavaScript获取cookie 当浏览器保存了服务端设置的Cookie后,浏览器再次发送到该服务器的请求会自动携带设置的Cookie信息,并存储在请求首部的Cookie字段中。
获取cookie信息:在Flask中,Cookie可以通过请求对象的cookies属性读取。
1
2name = request.cookies.get('name','Human')
#这里的Human是默认值,即没有cookie时name的值安全的Cookie:Session:浏览器中的cookie是明文存储且可以被修改的,把用户认证信息以明文方式存储在cookie里是非常危险的。方便的是,Flask提供了session对象来将敏感的Cookie数据进行加密存储。
session通过密钥对数据进行签名以加密数据,因此,得先设置一个密钥,这里的密钥就是一个具有一定复杂度和随机性的字符串。
程序的密钥可以通过
Flask.secret_key
属性或配置变量SECRET_KEY
来设置,如:1
2
3
4
5
6
7
8
9
10#1.通过Flask提供的属性写入
app.secret_key = 'secret string'
#2.把密钥写进系统环境变量然后用os模块提供的getenv()方法获取
SECRET_KEY=secret string #这里是系统环境变量
import os
...
app.secret_key=os.getenv('SECRET_KEY','默认密钥')
#可以在getenv()中添加第二个参数,作为没有获取到环境变量时使用的默认密钥模拟用户认证:下面使用session模拟用户的认证功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29#登录视图
from flask import redirect,session,url_for
def login():
session['logged_in'] = True #写入session
return redirect(url_for('hello'))
#hello视图
def hello(name):
name = request.args.get('name')
if name is None:
name = request.cookies.get('name','Human')
response = 'Hello, %s !' % name
#根据用户认证状态的不同返回不同内容
if 'logged_in' in session:
response += '[Authenticated]'
else:
response += '[Not Authenticated]'
return response
#只提供给认证用户的后台
def admin():
if 'logged_in' not in session:
abort(403)
return 'Welcome to admin page!'删除cookie方法:使用session对象的pop()方法
1
2
3
4
5
6#与登录视图对应,登出视图即删除cookie
def logout():
if 'logged_in' in session:
session.pop('logged_in')
return redirect(url_for('hello'))提示:默认情况下,session cookie会在用户关闭浏览器时删除。可通过将
session.permanent
属性设为True,可将session有效期延长为Flask.permanent_session_lifetime
属性对应的datetime.timedelta
对象;也可以通过配置变量PERMANENT_SESSION_LIFETIME
设置,默认为31天。注意:尽管session对象会对Cookie进行签名并加密,但这种方式仅能保证session的内容不被篡改。加密后的数据借助一些工具仍然可以轻易读取内容(即使不知道密钥),因此,绝对不能在session中存储敏感信息,如用户密码等。
Flask上下文:程序上下文、请求上下文。
程序上下文中存储了程序运行所必须的信息;
请求上下文包含了请求的各种信息,如请求URL、HTTP方法等。
Flask中的四个上下文全局变量:
变量名 上下文类别 说明 current_app 程序上下文 指向处理请求的当前程序实例 g 程序上下文 替代Python的全局变量,存储全局数据;仅在当前请求中可用,每次请求都会重设 request 请求上下文 封装客户端发出的请求报文数据 session 请求上下文 用于记住请求之间的数据,通过签名的Cookie实现 例如,如果每个视图都需要查询字符串获取变量值,那么可以借助g将这个操作移动到
before_request
处理函数中执行,并保存到g的任意属性上:1
2
3
4
5from flask import g
def get_name():
g.name = request.args.get('name')上下文钩子:Flask为上下文提供了一个
teardown_appcontext
钩子,使用它注册的回调函数会在程序上下文和请求上下文被销毁时调用。1
2
3
4
5#例如在每个请求处理结束后销毁数据库连接
def teardown_db(exception):
...
db.close()HTTP进阶实践:重定向回上一个页面、使用Ajax技术发送异步请求、HTTP服务端推送。
- 重定向回上一个页面:定义A、B、do-something 三个视图函数,其中A、B中有跳转向do-something的链接。要求若从A点链接,则触发do-something中的重定向回到A;若从B点链接,则重定向回B;若未成功检测到next参数和referer字段或URL不安全,则回到hello页面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40from urllib.parse import urlparse,urljoin
from flask import Flask,request,redirect,url_for
app = Flask(__name__)
#A页面,其中有转向do_something页面的链接
def A():
return '<h1>A page!</h1><a href="%s">Do something and redirect</a>' % url_for('do_something',next=request.full_path)
#B页面,其中有转向do_something页面的链接
def B():
return '<h1>B page!</h1><a href="%s">Do something and redirect</a>' % url_for('do_something',next=request.full_path)
#检验重定向目标url的安全性
def is_safe_url(target):
#用request.host.url获取程序内的主机URL
ref_url = urlparse(request.host_url)
#用urljoin函数将目标URL转为绝对URL,接着用urlparse函数解析两个URL
test_url = urlparse(urljoin(request.host_url,target))
#对目标URL的URL模式和主机地址进行验证,确保只有内部URL才会被返回
return test_url.scheme in ('http','https') and ref_url.netloc == test_url.netloc
def redirect_back(default='hello',**kwargs):
#递归next参数和referer字段中的url值
for target in request.args.get('next'), request.referrer:
if not target:
continue
#用is_safe_url函数判断目标URL的安全性,安全则返回重定向
if is_safe_url(target):
return redirect(target)
#不安全则回到'hello'页面
return redirect(url_for(default,**kwargs))
def do_something():
#do something
return redirect_back()- 使用Ajax技术发送异步请求:在下面的示例程序中,我们将使用全局jQuery函数
ajax()
发送Ajax请求。ajax()
函数是底层函数,有丰富的自定义配置。
参数 参数值类型及默认值 说明 url 字符串;默认为当前页地址 请求的地址 type 字符串;默认为GET 请求的方式,即HTTP方法 data 字符串;无默认值 发送到服务器的数据。会被jQuery自动转换为查询字符串 dataType 字符串;默认由jQuery自动判断 期待服务器返回的数据类型,可用: xml、html、script、json、jsonp、text contentType 字符串,默认为 application/x-www-form-urlencoded;charset=UTF-8 发送请求时使用的内容类型,即请求首部的Content-Type字段内容 complete 函数;无默认值 请求完成后调用的回调函数 success 函数;无默认值 请求成功后调用的回调函数 error 函数;无默认值 请求失败后调用的回调函数 对于处理Ajax请求的视图函数,不会返回完整的HTML响应,这时一般会返回局部数据,常见的有三种类型:纯文本或局部HTML模板、JSON数据、空值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#1.纯文本可以在JavaScript用来直接替换页面中的文本值,而局部HTML可以直接插入到页面中,如返回评论列表:
def get_comments(post_id):
...
return render_template('comments.html')
#2.JSON数据可以在JavaScript中直接操作:
def get_profile(user_id):
...
return jsonify(username=username,bio=bio)
#在ajax()方法的success回调中,响应主体的JSON字符串会被解析为JSON对象,可以直接获取并进行操作。
#3.空值,有些时候程序中一些接收Ajax请求的视图并不需要返回数据给客户端,比如用来删除文章的视图,此时可以直接返回空值,并将状态码指定为204(表示无内容):
def delete_post(post_id):
...
return '', 204异步加载长文章示例:在示例中,将显示一篇很长的虚拟文章,文章下方有一个”Load More”的按钮,单击按钮会发送一个Ajax请求获取文章的更多内容并直接动态插入到文章下方。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29#用来显示虚拟文章的 show_article视图:
from jinja2.utils import generate_lorem_ipsum
def show_article():
article_body = generate_lorem_ipsum(n=2) #生成两段随机文本
return '''
<h1>A very long article</h1>
<div class="body">%s</div>
<button id="load">Load More</button>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script type="text"/javascript">
$(function() {
$('#load').click(function() {
$.ajax({
url:'/more', #目标URL
type:'get', #请求方法
success:function(data) { #返回2XX响应后触发的回调函数
$('.body').append(data); #将返回的响应插入到页面中
}
})
})
})
</script>''' % article_body
#处理/more的视图函数会随机返回文章正文,如下:
def load_article():
return generate_lorem_ipsum(n=1)- HTTP服务端推送:不论是传统的HTTP请求-响应的通信模式,还是异步的Ajax式请求,都是客户端主动发出请求,服务端才返回响应,这种通信模式称为客户端拉取。然而某些情况下,需要服务端主动推送。
实现服务器端推送的一系列技术合称为
HTTP Server Push
,目前常用的推送技术有:传统轮询、长轮询、Server-Sent Events(SSE),以及HTML5的API中包含的WebSocket协议。名称 说明 传统轮询 在特定时间间隔内,客户端使用Ajax技术不断向服务器发HTTP请求,然后获取新数据并更新到页面 长轮询 和传统轮询类似,但服务器端若没有返回数据,则保持连接一直开启直到有数据时才返回响应。取回数据后再次发送另一个请求 Server-Sent Event(SSE) 通过HTML5中的EventSource API实现。SSE会在客户端和服务端建立一个单向通道,客户端监听来自服务端在任意时间发送的数据。 WebSocket协议 WebSocket是一种基于TCP协议的全双工通信协议。其实时性更强,而且可以实现双向通信,其浏览器兼容也强于SSE Web安全防范:此处提到Sql注入、Xss、CSRF攻击的原理和防范措施。
三. 模板
模板引擎:读取并执行模板中的特殊语法标记,并根据传入的数据将变量替换为实际值,输出最终的HTML页面,这个过程被称为渲染。Flask默认使用的模板引擎是Jinja2。
在模板中添加Python语句和表达式时,需要使用特定的定界符把它们标示出来。在Jinja2里常见的三种定界符:
1
2
3
4
5
6
7
8#1.语句,如if判断、for循环等:
{% ... %}
#2.表达式,如字符串、变量、函数调用等:
{{ ... }}
#3.注释:
{# ... #}使用if和for语句时,在语句结束的地方,必须添加结束标签:
1
2
3
4
5
6
7
8
9{% if user.bio %}
<i>{{ user.bio }}</i>
{% else %}
<i>This user has not bio!</>
{% endif %} #if语句结束标签
{% for i in str %}
<li> {{ i }}</li>
{% endfor %} #for语句结束标签在视图函数中渲染模板时,使用Flask提供的渲染函数
render_template()
,在该函数中,需要传入的第一个参数为模板的文件名,除此之外,可以以关键字参数的形式传入模板中使用的变量值。如下:1
2
3
4
5from flask inport Flask, render_template
def watchlist():
return render_template('userlist.html',user1=‘admin’,user2=‘brother’)此外,如果想传入函数在模板中调用,那么需要传入函数对象本身,而不是函数调用(会传入函数返回值),所以传入时仅写函数名称即可。
上下文:模板上下文包含很多变量,其中包括调用
render_template()
函数时手动传入的变量以及Flask默认传入的变量。除此之外,可以使用
set
标签在模板中定义变量或将一部分模板数据定义为变量:1
2
3
4
5
6
7
8#在模板中定义变量
{% set navigation = [('/','Home'),('/about','About')] %}
#将一部分模板数据定义为变量
{% set navigation %}
<li><a href = "/">Home</a>
<li><a href = "/about">About</a>
{% endset %}内置上下文变量:
config
: 当前的配置对象request
: 当前的请求对象,在已激活的请求环境下可用session
: 当前的会话对象,在已激活的请求环境下可用g
: 与请求绑定的全局变量,在已激活的请求环境下可用自定义上下文:Flask提供了一个
app.context_processor
装饰器,用来注册模板上下文处理函数,可以帮我们完成统一传入变量的工作。其需要返回一个包含变量键值对的字典,如下:1
2
3
4
5
6
7
8
9
10#1.使用装饰器
def inject_user():
user = 'brother'
return dict(user=user) #等同于 return {'user':user}
#2.将其作为方法直接调用
app.context_processor(inject_user)
#用lambda表达式简化后
app.context_processor(lambda:dict(user='brother'))全局对象:指在所有模板中都可以直接使用的对象,包括在模板中导入的模块。
内置全局对象:
Jinja2在模板中默认提供的常用的三个函数:
1
2
3
4
5
6range([start,]stop[,step]) #与range()用法相同
lipsum(n=5,html=True,min=20,max=100) #生成随机文本,可以在测试时用来填充页面,默认5段html文本,每段20-100个单词
dict(**items) #与dict()用法相同除了Jinja2内置的全局函数,Flask也在模板中内置了两个全局函数:
1
2
3
4url_for() #用于生成URL的函数,用法示例:
<a href="{{ url_for('index') }}">Return Index Page</a>
get_flashed_messages() #用于获取flash消息的函数自定义全局函数:除了使用
app.context_processor
注册模板上下文处理函数来传入函数,也可以使用app.template_global
装饰器直接将函数注册为模拟全局函数,如下1
2
3
4
5
6
def brother():
return 'I am brother.'
#或使用app.add_template_global()方法注册自定义全局函数
app.add_template_globat(brother)过滤器:在Jinja2中,过滤器(filter)是一些可以用来修改和过滤变量值的特殊函数,过滤器和变量用
|
隔开,需要参数的过滤器可以像函数一样使用括号传递。例如:
{{ name|title }}
会将name变量的值进行标题化,相当于在python中调用name.title()
。还可以将过滤器作用于一部分模板数据,使用标签声明开始和结束。
1
2
3
4#upper 将一段文字转换为大写
{% filter upper %}
This text becomes uppercase.
{% endfilter %}
1 |
|
为防范XSS攻击,Flask设置了Jinja2会自动对模板中的变量进行 escape
过滤器或escape()
函数对变量进行转义。
在确保变量值安全的情况下,如果想避免转移,将变量作为HTML解析,有两种方法:
1 | #safe过滤器 |
自定义过滤器:使用app.template_filter()
装饰器可以注册自定义过滤器
1 | #添加自定义过滤器,在变量值后面加入一个音符图标(♫) |
测试器:测试器是一些用来测试变量或表达式,返回布尔值的特殊函数。使用
is
连接变量和测试器。例如:1
2
3
4
5
6#number测试器用来判断一个变量或表达式是否为数字
{% if age is number %}
{{ age*365 }}
{% else %}
不是数字!
{% endif %}Jinja2中内置了许多测试器,常见的测试器如下:
测试器 说明 callable(object) 判断对象是否可被调用 defined(value) 判断变量是否已定义 undefined(value) 判断变量是否未定义 none(value) 判断变量是否为Noone number(value) 判断变量是否是数字 string(value) 判断变量是否是字符串 sequence(value) 判断变量是否是序列,如字符串列表元组 iterable(value) 判断变量是否可迭代 mapping(value) 判断变量是否是匹配对象,如字典 sameas(value,other) 判断变量与other是否指向相同的内存地址 自定义测试器:和过滤器类似,可以使用
app.template_text()
装饰器来注册一个自定义测试器1
2
3
4
5
6#判断变量是否为admin
def isAdmin(s):
if s == 'admin':
return True
return False模板环境对象:在Jinja2中,渲染行为由jinja2.Environment类控制,所有的配置选项、上下文变量、全局函数、过滤器和测试器都存储在Environment实例上。我们可以使用Flask创建的Environment对象,它存储在
app.jinja_env
属性上。
我们可以通过app.jinja_env
改变jinja2的设置,如自定义所有的定界符:1
2
3app = Flask(__name__)
app.jinja_env.variable_start_string = '[['
app.jinja_env.variable_end_string = ']]'模板环境中的全局函数、过滤器和测试其分别存储在Environment对象的globals、filters、tests属性中,这三个属性都是字典对象。我们可以直接操作这三个字典来添加响应的函数或变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22#1.添加自定义全局对象user1和user2
#和app.template_global()装饰器不同,直接操作globals字典允许我们传入任意Python对象,而不仅仅是函数
def user1():
return 'Brother'
user2 = 'Ronie'
app.jinja_env.globals['user1'] = user1
app.jinja_env.globals['user2'] = user2
#2.添加自定义过滤器smile
def addSmile(str):
return str + ':)'
app.jinja_env.filters['smile'] = addSmile
#3.添加自定义测试器Admin
def isAdmin(str):
if str == 'admin':
return True
return Falske
app.jinja_env.tests['Admin'] = isAdmin模板结构组织:除了使用函数、过滤器等工具控制模板的输出外,Jinja2还提供了一些工具来在宏观上组织模板内容。
局部模板:当多个独立模板中都会使用同一块HTML代码时,我们可以将这部分代码抽离出来存储到一个局部模板中,可以在其他独立模板中使用include标签来插入一个局部模板,这会把局部模板的内容插在使用include标签的位置。
1
2#例如,在其他模板中,可以在任意位置使用下面的代码插入 _banner.html 的内容
{% include '_banner.html' %}宏:Jinja2提供的一个特性,类似Python中的函数。可以把一部分模板代码封装到宏里,使用传递的参数来构建内容,最后返回构建完成的内容。宏在功能上和局部模板类似,都是为了方便代码块的重用。
为方便管理,我们把宏存储在单独文件中,这个文件通常命名为 macros.html。在创建宏时需要用
macro
和endmacro
标签声明宏的开始和结束,在开始标签中定义宏的名称和接收的参数。1
2
3
4
5
6
7{% marco people(num=1) %}
{% if num == 1 %}
Person.
{% elif num > 1 %}
People.
{% endif %}
{% endmarco %}在使用时,需要像从模块中导入函数一样使用import语句导入它,然后作为函数调用:
1
2
3{% from 'macros.html' import people %}
...
{{ people(num=1) }}注意,导入宏时,默认并不会传递当前上下文到该宏中,因此如果想在导入的宏中使用当前上下文中的变量和函数,就要在导入时显示地使用
with context
声明传入当前模板地上下文:1
{% from 'macros.html' import people with context %}
模板继承:Jinja2地模板继承允许定义一个基模板,把网页上的导航栏、页脚等通用内容放在基模板中,而每一个继承基模板的子模板在被渲染时都会自动包含这些部分。
编写基模板:基模板存储了程序页面的固定部分,通常被命名为base.html。
为了能够让子模板方便地覆盖或插入内容到基模板中,我们需要在基模板中定义**块(block)**,然后在子模板中定义同名块来执行继承操作。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#基模板的编写 base.html
<html>
<head>
{% block head %}
<meta charset="utf-8">
<title>{% block title %} Title {% endblock title %}</title>
...
{% endblock head %}
</head>
<body>
{% block body %}
...
{% endblock body %}
</body>
</html>编写子模板:子模板中,我们只需要对基模板中的特定块进行修改(覆盖、追加):
1
2
3
4
5
6
7
8
9
10
11
12#1.覆盖操作
{% extends 'base.html' %} #声明基模板
{% block title %}
New title
{% endblock title %}
...
#2.追加操作
{% block body %}
{{ super() }}
追加内容
{% endblock body %}模板进阶实践:空白控制、加载静态文件、消息闪现、自定义错误页面、Javascript和CSS中的Jinja2
空白控制:在实际输出的HTML文件中,模板中的Jinja2语句、表达式、注释的位置会保留空格。
可以通过在定界符内侧添加减号来移除该语句中的空白:
1
2
3{% if True -%}
<p>Hello!</p>
{%- endif %}加载静态文件:在Flask中,我们默认将静态文件(CSS、JS、图片)存储在与主脚本同级目录的static文件夹中。Flask中内置了用于获取静态文件的视图函数
static
:1
2
3
4
5#如在模板中添加"brother.jpg"图片
<img src="{{ url_for('static',filename='brother.jpg') }}" width="50">
#如在模板中加载"styles.css"文件
<link rel="stylesheet" type="text/css" href="{{ url_for('static',filename='style.css') }}">消息闪现:Flask提供了一个非常有用的
flash()
函数,它可以用来闪出需要显示给用户的消息。但
flash()
函数并不会直接呈现出信息,而是会将信息存储进session,我们可以用get_flashed_messages()
函数在模板中获取闪现的信息。1
2
3
4
5
6
7
8
9
10#先用flash()将闪现信息存储进session
def just_flash():
flash('I am flash!')
return redirect(url_for('index'))
#然后可以在index对应的模板中用get_flashed_messages()函数来获取信息
{% for msg in get_flashed_messages() %}
<p>{{ msg }}</p>
{% endfor %}自定义错误页面:我们可以使用
app.errorhandler()
装饰器来注册错误处理函数,从而自定义错误页面。1
2
3
4#先准备好自定义的页面,这里假定有404.html
def page_not_found(e): #错误处理函数接收异常对象e作为参数
return render_template('errors/404.html'),404 #需要在返回值中注明对应的状态码错误处理函数接收的Werkzeug内置的HTTP异常类有下列常用属性:
属性 说明 code 状态码 name 原因短语 description 错误描述,另外使用get_description()方法还可以获取HTML格式的错误描述代码 Javascript和CSS中的Jinja2:当我们需要在Javascript和Css中使用Jinja2的语句时,用HTML引入相应的JS和Css文件中的Jinja2代码将不会被渲染,因为只有使用
render_template()
传入的模板文件才会被渲染。对于这种情况,一下面有一些Tips:将使用Jinja2语句和变量的js和css直接用<style>和<script>标签写在HTML中,这样其中的Jinja2语句可以被渲染,但不推荐这种方法
将在js和css中要用到的jinja2的值直接定义为js和css中的变量,如下:
1
2
3
4
5
6
7
8
9#使用HTML元素的 data-* 属性存储,然后在Js中,使用DOM元素的dataset属性来获取 data-*的属性值,这里的 * 可以自己命名
<span data-id="{{ user.id }}" data-username="{{ user.name }}"></span>
#对于需要全局使用的数据,可以直接使用嵌入式JS代码定义变量
<script type="text/javascript">
var name = '{{ user.name }}'
</script>
#CSS同理
四. 表单
在Web程序中,表单的处理并不简单,不仅要创建表单、验证用户输入的内容、向用户显示错误提示,还要获取并保存数据。
WTForms:是一个使用Python编写的表单库,它使得表单的定义、验证(服务器端)和处理变得非常轻松。并且支持在Python中使用类定义表单,故一般不会在模板中直接用HTML编写表单。使用WTForms前,需要安装Flask-WTF扩展。
定义WTForms表单类:
1
2
3
4
5
6
7
8
9from WTForms import Form,StringField,PasswordField,BooleanField,SubmitField
from WTForms.validators import DataRequired,Length
#from flask_wtf import FlaskForm
class LoginForm(Form):
#使用Flask-WTF定义表单时,要继承FlaskForm类,如class LoginForm(FlaskForm)
username = StringField('Username',validators=[DataRequired()])
password = PasswordField('Password',validators=[DataRequired(),Length(8,128)])
remember = BooleanField('Remember me')
submit = SubmitField('Log in')每个字段属性通过实例化WTForms提供的字段类表示。字段属性的名称将作为对应HTML<input>元素中的name属性及id属性值。
常用的WTForms字段如表:
字段类 说明 对应的HTML表示 BooleanField 复选框,值会被处理为布尔结果 <input type=”checkbox”> DateField 文本字段,值被处理为datetime.date对象 <input type=”text”> DateTimeField 文本字段,值被处理为datetime.datetime对象 <input type=”text”> FileField 文件上传字段 <input type=”file”> FloatField 浮点数字段,值被处理为浮点型 <input type=”text”> IntegerField 整数字段,值被处理为整数 <input type=”text”> RadioField 一组单选按钮 <input type=”radio”> SelectField 下拉列表 <select><option></option></select> SelectMultipleField 多选下拉列表 <select multiple><option></option></select> SubmitField 提交按钮 <input type=”submit”> StringField 文本字段 <input type=”text”> HiddenField 隐藏文本字段 <input type=”hidden”> PasswordField 密码文本字段 <input type=”password”> TextAreaField 多行文本字段 <textarea></textarea> 在实例化字段类时可传入的参数:
- label:字段标签label的值
- render_kw:用来设置对应input标签的属性值的字典
- validators:包含一系列验证器的列表
- default:用于设置默认值
常用的验证器(validator):
- DataRequired:验证数据是否有效
- Email:验证Email地址
- EqualTo:验证两个字段值是否相同
- InputRequired:验证是否有数据
- Length(min= ,max= ):验证输入长度是否在指定范围内
除此之外还有NumberRange、Optional、Regexp、URL、AnyOf、NoneOf等验证器。
获取表单字段对应的HTML代码:实例化创建好的表单类,然后直接调用实例的属性即可获取表单字段对应的HTML代码
1
2
3
4
5
6
7#输出HTML代码
form = LoginForm()
form.username()
#将输出表单实例中属性为username的input标签代码
form.username.label()
#将输出label元素的HTML代码默认情况下,WTForms输出的字段HTML代码只会包含id和name属性。如果要添加额外的属性(如添加placeholder属性占位文本和添加class属性设置对应CSS类样式),通常有两种方法:使用
render_kw
属性 和 调用字段时传入。1
2
3
4
5
6#1.使用render_kw属性
username = StringField('Username',render_kw={'placeholder':'Your Username'})
#2.在调用字段时传入
form.username(style='width:200px;',class_='bar')
#class是Python的保留关键字,这里用class_来代替class,渲染后的input标签会获得正确的class属性,在模板中调用时则可以直接使用class在模板中渲染表单:
1
2
3
4
5
6
7#传入表单类实例
from forms import LoginForm
def basic():
form = LoginForm()
return render_template('basic.html',form=form)在模板中,只需要调用表单类的属性即可获得对应的HTML代码:
1
2
3
4
5
6
7<from method="post">
{{ form.csrf_token }}
{{ form.username.label }}<br>{{ form.username }}<br>
{{ form.password.label }}<br>{{ form.password }}<br>
{{ form.remember }}{{ form.remember.label }}<br>
{{ form.submit }}<br>
</form>处理表单数据的步骤:
从获取数据到保存数据大致会经历以下步骤:
- 解析请求,获取表单数据
- 对数据进行必要的转换,比如将勾选框的值转换成Python的布尔值
- 验证数据是否符合要求,同时验证CSRF令牌
- 如果验证未通过则需要生成错误消息,并在模板中显示错误消息
- 如果通过验证,就把数据保存到数据库或做进一步处理
提交表单:表单的提交行为主要由三个属性控制,即action(当前URL)、method(提交的HTTP请求方法)、enctype(表单数据的编码类型)。
GET方式:表单数据会以查询字符串的形式附加在请求的URL里,且仅适于长度不超过2000字符,且不包含敏感信息。
POST方式:出于安全的考虑,一般使用POST方法提交表单。按照默认的编码类型,表单数据会被存储在请求主体中。
获取和验证表单数据:客户端验证、服务器端验证。
客户端验证:指在客户端(如浏览器)对用户的输入值进行验证,如HTML5内置的验证属性(type、required、min、max、accept),除此之外,通常会使用Javascript实现完善的验证机制。客户端方式可以实时动态提示用户输入是否正确,只有正确才会将表单数据发送到服务端。
服务端验证:指用户把输入的数据提交到服务端,在服务端对数据进行验证,若错误,则在响应中加入错误信息,用户修改后再次验证直到通过验证。我们在Flask程序中使用WTForms实现的就是服务端验证。
WTForms验证机制:在实例化表单类时传入表单数据,对表单实例调用
validate()
方法,逐个对字段调用定义的验证器之后返回结果布尔值,如果验证错误,就把错误信息存储到表单实例的errors属性对应的字典中。在视图函数中验证表单:由于视图函数同时接收GET和POST请求,所以要根据请求方法的不同来执行不同的代码:
1
2
3
4
5
6
7
8
9from flask import request
...
def basic():
form = LoginForm()
if request.method == 'POST' and form.validate(): #检测到POST请求,同时通过验证器
username = form.username.date #获取username字段的数据
... #处理POST请求的其他代码
return render_template('basic.html',form=form) #处理GET请求,渲染模板Flask-WTF提供的validate_on_submit()方法合并了检测请求和validate()方法,故上面的代码可以进一步简化为:
1
if form.validate_on_submit():
除POST外,validate_on_submit()方法还会验证PUT、PATCH、DELETE方法的表单数据。
在模板中渲染错误信息:如果
form.validate_on_submit()
方法返回False,即说明验证没有通过,WTForms会把错误消息添加到表单类的errors属性中,我们可以在模板里使用for循环迭代错误消息列表:1
2
3
4
5
6
7{% for msg in form.username.errors %}
<small class="error"> {{ msg }}</small><br>
{% endfor %}
{% for msg in form.password.errors %}
<small class="error"> {{ msg }}</small><br>
{% endfor %}表单进阶实践:介绍一些表单处理的相关技巧,这些技巧可简化表单的处理过程。并且介绍一些表单的非常规应用。
- 设置错误消息语言
- 使用宏渲染表单
- 自定义验证器(行内验证器、全局验证器)
- 文件上传(定义上传表单,渲染上传表单,处理上传文件,多文件上传)
- 使用Flask-CKEditor集成富文本编辑器(WYSIWYG编辑器)
- 单个表单的多个提交按钮
- 单个页面多个表单
上述拓展技巧这里不再赘述,感兴趣可自行搜索具体的实现方法。
关于第五章数据库和第六章电子邮件部分的内容学习由于学习重点不在开发所以暂停,以后若有需求则会继续填坑——2020.11.04
- 本文作者: Squidward
- 本文链接: http://www.squidward.xyz/2020/10/28/Flask学习笔记/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!