《Flask Web开发:基于Python的Web应用开发实战》笔记二、
第三章、模板
视图函数作用即生成请求的响应,如果把业务逻辑和表现逻辑混在一起会导致代码难以理解和维护。吧表现逻辑转移到模板中能够提升程序的可维护性。
模板是一个响应文本的文件,其中包含用占位变量表示的动态部分,其具体值只在请求的上下文才能知道。
使用真实值替换变量,在返回最终得到的响应字符串,这一过程称为渲染。
3.1、Jinja2模板引擎
3.1.1、渲染模板
在默认情况下,Flask程序会在templates子文件夹中寻找模板。在下一个hello.py版本中,要把前面定义的模板保存在templates文件夹中,并分别命名为index.html和user.html。
from flask import Flask,render_templatefrom flask_script import Managerapp = Flask(__name__)manager = Manager( app )@app.route('/')def index(): return render_template('index.html')@app.route('/user/')def user(name): return render_template('user.html',name=name)if __name__ == "__main__": manager.run()
- 代码详解:Flask提供的render_template函数吧Jinja2模板引擎集成到程序中。render_template函数的第一个参数是模板的文件名,随后的参数都是键值对,表示模板中变量对应的真实值。
3.1.2、变量
Jinja2能识别所有类型的变量,甚至是一些复杂的类型,例如列表、字典和对象。在模板中使用变量的一些示例如下:
DICT {{ mydict['key'] }}
LIST {{ mylist[3] }}
list with a variable index: {{ mylist[myintvar]}}
object's method: {{ myobj.somemethod() }}
- 常用Jinja2 变量过滤器
- safe 渲染值时不转义
- capitalize 把值的首字母转换成大写,其他字母转换成小写
- lower 把值转换成小写形式
- upper 把值转换成大写形式
- title 把值中每个单词的首字母都转换成大写
- trim 把值的首尾空格去掉
- striptags 渲染之前把值中所有的HTML标签都删掉
3.1.3、控制结构
条件控制语句
{% if user %}
{% else %}
{% endif %}
for循环语句
{% for comment in comments %}
{% endfor %}
支持宏
{% marco render_comment(comment) %}
多处重复使用的模板代码片段可以写入单独的文件,再包含在所有的模板中,以避免重复:
{ % include 'comment.html' %}
- 另外一中重复使用代码的强大方式是模板继承,他类似于Python代码中的类继承。继承方式如下:首先创建一个名为base.html的基础模板:
{% block head %}
{% block title %}{% endblock %}- My Application {% endblock %}{% block body %}{% endblock %} - block标签定义的元素可在衍生的模板中年修改。在本例中,我们定义了名为head,title,body的块元素。注意,title包含在head中。下面就是基于基础模板的衍生模板:
{% extends bash.html %}{% block title %}Index{% endblock%}{% block head%}{{ super() }}{% endblock %}{% block body %}
hello,world
{% endblock %} - extends指令声明这个模板衍生自base.htmk,在extends指令之后,基础模板中的3个板块重新定义,模板引擎会贾汪其插入适当的位置。注意新定义head块,在基础模板中内容是空的,所以使用super()获取原来的内容。
3.2、使用Flask-Bootstrap集成Twitter Bootstrap
Bootstrap是Twitter开发的一个开源框架,它提供的用户界面组件可用于创建整洁且具有吸引力的网页,并且这些网页还能兼容所有现代的Web浏览器。
Bootstrap是客户端框架,不会直接涉及服务器。要下在程序中继承Bootstrap,显然需要对模板做所有必要的改动,更简单的方法就是安装Flask-Bootstrap的Flask扩展,简化集成的过程。
- Flask-Bootstrap使用pip方式安装
pip install flask-bootstrap
Flask扩展一般在创建程序实例时初始化。
from flask.ext.bootstrap import Bootstrapbootstrap = Bootstrap(app)
- templates/user.html,使用的就是Flask-Bootstrap的模板
{% extends "bootstrap/base.html" %}{% block title %}Flasky{% endblock %}{% block navbar %}
{% endblock %}{% block content %}{% endblock %}Hello, {{ name }}!
代码详解:
- Jinja2中的extends指令从Flask-Bootstrap中导入bootstrap/base.html,从而实现模板继承。Flask-Bootstrap中的基础模板提供了一个网页框架,引入了Bootstrap中的所有CSS和JavaScript文件。
- 基础模板中定义了可在衍生模板中重新定义的块。block和endblock指令定义块中的内容可添加到基模板中。
Flaks-Bootstrap基模板中定义的块:
块名 说明
- doc 整个HTML文档
- html_attribs 标签的属性
- html 标签的内容
- head 标签中的内容
- title
标签中的内容 - metas 一组标签
- styles 层叠样式表定义
- body_attribs 标签的属性
- body 标签中的内容
- navbar 用户定义的导航条
- content 用户定义的页面内容
- scripts 文档底部的JavaScript声明
3.3、自定义错误页面
Flask允许程序使用基于模板的自定义错误页面,最常见的错误代码有两个:
404,客户端请求未知页面或路由时显示。
500,有未处理的异常时显示。
- 自定义错误页面:
@app.errorhandler(404)def page_not_found(e): return render_template('404.html'),404@app.errorhandler(500)def internal_server_error(e): return render_templte('500.html'),500
- 和视图函数一样,错误处理程序也返回响应,它们还返回与该错误对应的数字状态码。
- 如果从Github上克隆了这个程序的Git仓库,执行git checkout 3c签出这个程序的这个版本。
3.4、链接
在模板中直接编写简单路由的URL连接不难,但对于包含可变部分的动态路由,在模板中构建正确的URL就很困难;并且直接编写URL会对代码中定义的路由产生不必要的依赖。
Flask提供了url_for()辅助函数,可以使用程序URL映射中保存的信息生成URL。
url_for()函数最简单的用法是以视图函数名(后者app.add_url_route()定义路由时使用的端点名)作为参数,返回对应的URL。
使用url_for()生成动态地址时,将动态部分作为关键字参数传入。例如,url_for('user',name='john',_external=True)的返回结果是http://localhost:5000/user/john
传入url_for()的关键字参数不仅限于动态路由中的参数。函数能将任何额外参数添加到查询字符串中。例如,url_for('index',page=2)的返回结果是/?page=2
3.5、静态文件
默认设置下,Flask在程序根目录中名为static的子目录中寻找静态文件。如果需要,可在static文件夹中使用子文件夹存放文件。
3.6、使用Flask-Monment本地化日期和时间
问题背景:如果Web程序的用户来自世界各地,那么处理日期和时间就不是一个简单的任务。
解决方法:通过使用JavaScript开发的优秀客户端开源代码库,名为moment.js,可以在浏览器中渲染日期和时间。Flask-Monment是一个Flask程序扩展。能把moment.js集成到Jinja2模板中。
Flask-Moment可以通过pip安装:
pip install flask-moment
- 初始化Flask-Moment:
from flask.ext.moment import Momentmoment = Moment(app)
- 除了moment.js,Flask-Moment还依赖jquery.js.Bootstrap已经引入了jquery.js,因此只需引入moment.js即可。
{% block scripts %}{{ super() }}{{ moment.include_moment() }}{% endblcok %}
- 为了处理时间戳,Flask-Moment想模板开放了moment类,后台可以将utc时间传到前台进行渲染。
from datetime improt datetime@app.route('/')def index():return render_template('index.html',current_time=datetime.utcnow())
- 在模板中渲染current_time
The local date and time is {{ moment(current_time).format('LLL) }}
That was {{ moment(current_time).fromNow(refresh=True) }}
更多moment.js用法:http://momentjs.com/docs/#/displaying/
Flask-Moment假定服务器端程序处理的时间是"纯正的"datetime对象,且使用UTC表示。
第四章、Web表单
对于一些重复操作(生成表单的HTML代码和验证提交的表单数据),Flask-WTF扩展可以把处理Web表单的过程变成一种愉悦的体验。这个扩展对独立的WTForms包进行了包装,方便集成到Flask程序中。
WTForms官网:http://wtforms.simplecodes.com
- Flask-WTF及其依赖可使用pip安装:
pip install flaks-wtf
4.1、跨站请求伪造保护
默认情况,Flask-WTF能保护所有表单面授跨站请求伪造(CSRF)的***。为了实现CSRF保护,Flask-WTF需要程序设置一个密钥。Flask-WTF使用这个密钥生成加密令牌,再用令牌验证请求中表单数据的真伪。
示例代码(设置Flask-WTF):
app = Flask(__name__)app.config['SECRET_KEY'] = 'hard to guess string'
- 代码详解:app.config字典可用来存储框架、扩展和程序本身的配置变量
- SECRET_KEY配置变量是通用密钥,可在Flask和多个第三方扩展中使用。
4.2、表单类
使用Flask-WTF时,每个Web表单都由一个继承自Form的类表示。
示例代码(一个简单的Web表单,包含一个文本字段和一个提交按钮):
from flaks.ext.wtf import Formfrom wtforms import StringField,SubmitFieldfrom wtforms.validators import Requiredclass NameForm(Form): name = StringField("What's your name?",validators=[Required()]) submit = SubmitField('Submit')
- 代码详解:StringField类表示属性为type="text"的元素;SubmitField类表示属性为type="submit"的元素。StringField构造函数中的可选参数validators指定一个由验证函数组成的列表,在接收用户提交的数据之前验证数据。验证函数Required()确保提交的字段不为空
- Form基类由Flask-WTF扩展定义,从falsk.ext.wtf中导入。字段和验证函数可以直接从WTForms包中导入。
WTForms支持的HTML标准字段
- StringField 文本字段
- TextAreaField 多行文本字段
- PasswordField 密码文本字段
- HiddenField 隐藏文本字段
- DateField 文本字段,值为 datetime.date 格式
- DateTimeField 文本字段,值为 datetime.datetime 格式
- IntegerField 文本字段,值为整数
- DecimalField 文本字段,值为 decimal.Decimal
- FloatField 文本字段,值为浮点数
- BooleanField 复选框,值为 True 和 False
- RadioField 一组单选框
- SelectField 下拉列表
- SelectMultipleField 下拉列表,可选择多个值
- FileField 文件上传字段
- SubmitField 表单提交按钮
- FormField 把表单作为字段嵌入另一个表单
- FieldList 一组指定类型的字段
WTForms验证函数
- Email 验证电子邮件地址
- EqualTo 比较两个字段的值;常用于要求输入两次密码进行确认的情况
- IPAddress 验证 IPv4 网络地址
- Length 验证输入字符串的长度
- NumberRange 验证输入的值在数字范围内
- Optional 无输入值时跳过其他验证函数
- Required 确保字段中有数据
- Regexp 使用正则表达式验证输入值
- URL 验证 URL
- AnyOf 确保输入值在可选值列表中
- NoneOf 确保输入值不在可选值列表中
4.3、把表单渲染成HTML
示例代码:(使用Flask-WTF和Flask-Bootstrap渲染表单)
{% extends 'base.html' %}{% import "bootstrap/wtf.html" as wtf %}{% block title %}Flasky{% endblock %}{% block page_content %}hello,{% if name %}{{ name }}{% else %}Stranger{% endif%}
{{ wtf.quick_form(form)}}{% endblock %}
- 代码详解:导入的bootstrap/wtf.html文件定义了一个使用Bootstrap渲染Flask-WTF表单对象的辅助函数。wtf.quick_form()函数的参数为Flask_WTF表单对象,使用Bootstrap的默认样式渲染传入表单。
4.4、在视图函数中处理表单
示例代码:(视图函数index()不仅要渲染表单,还要接受表单中的数据。)
@app.route('/',methods=['GET','POST'])def index(): name = None form = NameForm() if form.validate_on_submit(): name = form.name.data form.name.data = '' return render_template('index.html',form=form,name=name)
- 代码详解:app.route修饰器中添加的methods参数告诉Flask在URL映射中把这个视图函数注册为GET和POST请求的处理程序。如果没指定methods参数,则默认把视图函数注册为GET请求的处理程序。
- 用户提交表单后,服务器会收到一个POST请求。validate_on_submit()会调用name字段上附属的Required()验证函数。如果名字不为空,就能通过验证,validate_on_submit()返回True。
4.5、重定向和用户会话
问题背景:当用户输入名字后提交表单,再点击浏览器的刷新按钮,会看到一个警告,关于要求再次提交表单之前进行确认。之所以会出这种问题,是因为刷新页面时浏览器会重新发送之前已经发送过的最后一个请求。
解决方案:使用重定向作为POST请求的响应,而不是使用常规响应。重定向是一种特殊的响应,响应内容是URL,而不是包含HTML代码的字符串。浏览器收到这种响应,会向重定向的URL发起GET请求,显示页面的内容。
另一问题:如果使用上面的解决方案,程序在处理POST请求时,使用from.name.data获取用户输入的名字,一旦请求结束,数据也就丢失了。所以需要程序将数据存储到用户会话中,在请求之间"记住"数据。用户是一种私有存储,存在每个连接到服务器的客户端中。
示例代码:
from flask import Flask,render_template,session,redirect,url_for@app.route('/',methods=['GET','POST'])def index(): form = NameForm() if form.validate_on_submit(): session['name'] = form.name.data return redirect(url_for('index')) return render_template('index.html',form=form,name=session.get('name')))
- 代码详解:合法表单数据的请求最后会调用redirect()函数。redirect()是个辅助函数,用来生成HTTP重定向响应。redirect()函数参数是重定向的URL。url_for()生成URL,因为这个函数使用URL映射生成URL,从而保证URL和定义的路由兼容,并且修改路由名字后依然可用。
4.6、Flash消息
问题背景:用户提交了有一项错误的登录表单,服务器发回的响应重新渲染了登录表单,并在表单上面显示信息,提示用户名或密码错误。
示例代码:
from flask ipmort Flask,render_template,session,redirect,url_for,flash@app.route('/',methods=['GET','POST'])def index(): form = NameForm() if form.validate_on_submit(): old_name = session.get('name') if old_name is not None and old_name != form.name.data: flash("Looks like you have changed your name") session['name'] = olde_name return redirect(url_for('index')) return render_template('index.html',name=session.get('name'),form=form) ```* 代码详解:代码会将每一次提交的名字与上一次**存储在会话中的名字**进行比较,如果两者不一样则会发给客户端下一个响应中显示一个信息。* 仅调用flash()函数并不能把消息显示出来,程序使用的模板要渲染这些信息,最好在基础模板中渲染Flash消息,因为这样所有页面都能使用这些消息。Flask把**get_flashed_messages()函数**开放给模板,用来获取并渲染消息。
{% block content %}
{% for message in get_flashed_messages() %}
{{ message }}
{% endfor %}
{% block page_content %}
{% endblock %}
{% endblock %}