0%

Flask入门(一)

本篇博客主要参考图灵程序设计丛书《Flask Web开发》(第二版) 作者:【美】Miguel Grinberg

总览

Flask 是一种小型框架,可以成为“微框架”

Flask 开发伊始就被设计为可扩展的框架,具有一个包含基本服务的核心,其他功能可通过扩展实现

其主要有三个依赖

  • 路由
  • 调试
  • Web 服务器网关接口(WSGI,Web server gateway interface)

子系统由 Werkzeug 提供
模板系统由 Jinja2 提供
命令行集成由 Click 提供

在Python3中创建虚拟环境

新建一个文件夹,打开命令行,切换到当前目录,执行:
python -m venv virtual-environment-name

通常虚拟环境名称为venv,例如:
python -m venv venv

然后就能看到在当前目录下出现了一个名为venv的子目录

接着我们需要激活它:
venv\Scripts\activate
可以看到就下来的命令格式:
(venv) F:\flasky>

虚拟环境被激活后,里面的Python解释器的路径会添加到当前命令会话的PATH环境变量中
在命令提示符中输入 python,将调用虚拟环境中的解释器,如果打开了多个命令提示符窗口,每个窗口中都要激活虚拟环境

虚拟环境的工作结束后,在命令提示符中输入deactivate,还原当前中断会话的PATH环境变量

应用的基本结构

初始化

所有Flask应用都必需创建一个应用实例

1
2
from flask import Flask
app = Flask(__name__)

Flask 类的构造函数只有一个必须指定的参数,即应用主模块或包的名称

路由和视图函数

客户端(如 Web 浏览器)把请求发送给服务器,Web 服务器再把请求发送给 Flask 应用,应用需要知道每个 URL 请求需要运行什么代码,所以保存了一个处理 URL 到 Python 函数的青蛇关系,这个程序叫做路由

最渐变的方式就是使用应用实例提供的app.route装饰器,如

1
2
3
@app.route('/')
def index():
return '<h1>Hello Flask!</h1>'

index()这样处理入站请求的函数,就叫做视图函数

其实,很多服务的 URL 都包含可变部分,例如 http://www.example/user-name
Flask 支持这种形式的 URL,可以在app.route装饰器中使用特殊的句法实现

1
2
3
@app.route('/user/<name>')
def user(name):
return '<h1>Hello {}!</h1>'.format(name)

完整应用

一个完整的应用应该具有类似于如下的结构:

1
2
3
4
5
6
7
8
9
10
11
from flask import Flask
app = Flask(__name__)


@app.route('/')
def index():
return '<h1>Hello Flask!</h1>'


if __name__ == '__main__':
app.run(debug=True)

运行程序后会出现如下提示信息(这里还打开了调试模式):

1
2
3
4
5
6
7
* Serving Flask app "hello" (lazy loading)
* Environment: production
* Debug mode: on
* Restarting with stat
* Debugger is active!
* Debugger PIN: 299-102-867
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

接着就可以在本地浏览器输入127.0.0.1:5000进行访问了

模板

jinja2 模板

hello.py的同级目录下新建一个文件夹templates,添加两个html文件

  • templates/index.html

<h1>Hello Flask!</h1>

  • templates/user.html

<h1>Hello, !</h1>

修改一下应用中的视图函数,以渲染模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask import Flask,render_template
app = Flask(__name__)


@app.route('/')
def index():
return render_template('index.html')


@app.route('/user/<name>')
def user(name):
return render_template('user.html', name=name)


if __name__ == '__main__':
app.run(debug=True)

打开浏览器,依次访问

127.0.0.1:5000

127.0.0.1:5000/user/Nopech

变量

模板中的表示一个变量,告诉模板引擎这个位置的值从渲染模板时使用的数据获取

列举一些变量过滤器

过滤器名 说明
safe 渲染时不转义
capitalize 值的首字母转为大写,其他小写
lower 值转为小写形式
upper 值转为大写
titile 每个单词首字母都转为大写
trim 删去值的首位空格
striptags 渲染之前删去值中所有HTML标签

控制结构

控制结构可以改变模板的渲染流程

条件判断语句

1
2
3
4
5
{% if name %}
Hello, {{ name }}!
{% else %}
Hello, Stranger!
{% endif %}

重复使用宏,并渲染一组元素

先把宏保存在单独的文件中,然后再需要使用的模板中导入

1
2
3
4
5
6
{% import 'macros.html' as macros %}
<ul>
{% for comment in comments %}
{{ macros.render_comment(comment) }}
{% endfor %}
</ul>

模板继承

模板继承类似于 python 的类继承

首先创建一个名为base.html的基模板

1
2
3
4
5
6
7
8
9
10
11
<html>
<head>
{% block head %}
<title>{% block title %}{% endblock %} - My Application</title>
{% endblock %}
</head>
<body>
{% block body %}
{% endblock %}
</body>
</html>

基模板中定义的区块可在衍生模板中覆盖
衍生模板示例如下

1
2
3
4
5
6
7
8
9
10
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ super() }}
<style>
</style>
{% endblock %}
{% block body %}
<h1>Hello Flask!</h1>
{% endblock %}

如果基模板和衍生模板中的同名区块都有内容,那么显示的将是衍生模板中的内容

使用Flask-Bootstrap集成Bootstrap

Bootstrap 是 Twitter 开发的一个开源 Web 框架,兼容所有现代的桌面和移动平台 Web 浏览器

要使用的扩展是 Flask-Bootstrap,可以用 pip 安装:

1
(venv)>pip install flask-bootstrap

初始化 Flask-Bootstrap

1
2
3
from flask_bootstrap import Bootstrap
# ...
bootstrap = Bootstrap(app)

自定义错误页面

常见的错误代码有两个:

  • 404:客户端请求未知页面或路由
  • 500:应用有未处理的异常

可以使用app.errorhandler装饰器为错误提供自定义处理函数

1
2
3
4
5
6
7
8
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 400


@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500

错误处理函数中引用的模板需要手动编写,一般来说这些模板应该采用和常规页面相同的布局,直接复制templates/user.html会有很多重复劳动

这就可以用 Jinja2 的模板继承机制来解决

包含导航栏的应用基模板

位于 templates/base.html

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
{% extends "bootstrap/base.html" %}

{% block title %}Flasky{% endblock %}

{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}

{% block content %}
<div class="container">
{% block page_content %}{% endblock %}
</div>
{% endblock %}

模板中的content区块只有一个<div>容器,其中包含一个新的空区块,名为page_content,内容需要由衍生模板定义

使用模板继承机制自定义404页面

1
2
3
4
5
6
7
8
9
10
{% extends "base.html" %}

{% block title %}Flasky - Page Not Found{% endblock %}

{% block page_content %}
<div class="page-header">
<h1>Not Found</h1>
<p>Please check out the url!</p>
</div>
{% endblock %}

简单测试一下效果

使用模板继承机制简化页面模板

1
2
3
4
5
6
7
8
9
{% extends "base.html" %}

{% block title %}Flasky{% endblock %}

{% block page_content %}
<div class="page-header">
<h1>Hello, {{ name }}!</h1>
</div>
{% endblock %}

使用Flask-Moment本地化日期和时间

服务器需要统一时间单位,一般使用协调世界时(UTC, coordinated universal time)

不过用户更希望看到的是当地时间,可以这样解决:把时间单位发送给 Web 浏览器,转换成当地时间,然后用 JavaScript 渲染

JavaScript 中有一个开源库,名为 Moment.js,可以在浏览器中渲染日期和时间

Flask-Moment 是一个 Flask 扩展,能简化把 Moment.js 集成到 Jinja2 模板中的过程

安装

1
(venv)>pip install flask-moment

初始化

1
2
from flask_moment import Moment
moment = Moment(app)

引入 Moment.js 库

在 templates/base.html 中引入

1
2
3
4
5
# ...
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}

添加一个 datatime 变量

hello.py 文件中修改代码

1
2
3
4
5
6
from datetime import datetime

@app.route('/')
def index():
return render_template('index.html',
current_time=datetime.utcnow())

使用 Flask-Moment 渲染时间戳

在 templates/index.html 文件中

1
2
<p>The local date and time is {{ moment(current_time).format('LLL') }}.</p>
<p>That was {{ moment(current_time).fromNow(refresh=True) }}.</p>

可以看到效果图

表单

之前介绍的模板都是单向的,所有信息都从服务器流向用户
对于大多数应用来说,还需要沿相反的方向流动信息,把用户提交的数据交给服务器处理

这时表单可以帮我们完成这一点

安装

1
pip install flask-wtf

配置Flask-WTF

app.config 字典可用于存储Flask、扩展和应用自身的配置变量

Flask-WTF 要求应用配置一个密钥,是为了防止表单遭到跨站请求伪造(CSRF, cross-site request forgery)攻击

方法如下

1
2
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'

表单类

定义包含一个文本字段和一个提交按钮的表单

1
2
3
4
5
6
7
8
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired


class NameForm(FlaskForm):
name = StringField('What if your name>', validators=[DataRequired()])
submit = SubmitField('Submit')

其中验证函数DataRequired()确保提交的字段内容不为空

WTForms 支持的 HTML 标准字段及其内建的验证函数很多,这里列举一部分

字段类型 说明
StringField 文本字段
SubmitField 表单提交按钮
TextAreaField 多行文本字段
PasswordField 密码文本字段
FileField 文件上传字段
SelectField 下拉列表
验证函数 说明
Email 验证电子邮件地址
IPAddress 验证 IPv4 网络地址
NumberRange 验证输入的值在数字范围之内
URL 验证 URL
AnyOf 确保输入的值在一组可能的值中
NoneOf 确保输入值不在可选列表中

把表单渲染成 HTML

假设视图函数通过 form 参数把一个 NameForm 实例传入模板,在模板中可以生成一个简单的 HTML 表单,如下

1
2
3
4
5
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name(id='my-text-field') }}
{{ form.submit() }}
</form>

调用字段时传入的任何关键字参数都将转换成字段的 HTML 属性
就像上面的代码,指定了id属性,就可以为其定义 CSS 样式

不过这样的工作量还是很大,所以可以使用 Bootstrap 预定义的表单样式渲染整个 Flask-WTF 表单

1
2
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}

import指令的使用和普通 Python 代码一样
导入的 bootstrap/wtf.html 文件中定义了一个使用 Bootstrap 渲染 Flask-WTF 表单对象的辅助函数
wtf.quick_form(form)函数的参数为 Flask-WTF 表单对象,使用 Bootstrap 的默认样式渲染传入的表单

templates/index.html 示例

1
2
3
4
5
6
7
8
9
10
11
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky{% endblock %}

{% block page_content %}
<div class="page-header">
<h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>
</div>
{{ wtf.quick_form(form) }}
{% endblock %}

在视图函数中处理表单

index()视图函数

hello.py: 使用GETPOST

1
2
3
4
5
6
7
8
@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装饰器中的参数method参数告诉 Flask,在 URL 映射中把该视图注册为GETPOST请求的处理程序
如果没有指定mehtond参数,只把视图函数注册为GET请求的处理程序

表单可以通过GET请求提交,但是GET请求没有主体,提交的数据以查询字符串的形势附加到 URL 中,在地址栏中的地址可见。基于此以及其他原因,处理表单提交几乎都使用POST请求

提交表单后,显示如下图

重定向和用户会话

上一个版本的 hello.py 存在一个可用性问题,用户提交表单后刷新页面,会收到一个警告

1
2
3
您所查找的网页要使用已输入的信息。
返回此页可能需要重复已进行的所有操作。
是否要继续操作?

这是因为刷新页面时浏览器会重新发送之前发送过的请求,很显然,这不是理想情况

可以使用重定向作为 POST 请求的响应

重定向的相应内容是 URL,而不是 HTML 代码的字符串
浏览器收到这种响应时,会向重定向的 URL 发起 GET 请求,显示页面的内容
这个页面可能会加载一段时间,因为要先把第二个请求发送给服务器

前一个请求时GET,所以刷新命令可以想预期一样正常工作了

这称为 Post/重定向/Get 模式

不过,这种情况下,应用处理 POST 请求时,可以通过 form.name.data获取用户输入的名字,但请求结束后数据就消失了,所以应用需要保存输入的名字

应用可以把数据存储在用户会话
用户会话是一种私有存储,每个连接到服务器的客户端都可以访问

默认情况下,用户会话保存在客户端 cookie 中,使用设置的密钥加密签名
若篡改了 cookie 的内容,签名和会话都会消失\

hello.py

1
2
3
4
5
6
7
8
9
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'))

上一个版本在局部变量name中存储用户在表单中输入的名字
这个变量现在保存在用户会话中,即session['name']

现在,包含有效表单数据的请求最后会使视图函数调用redirect()函数,可以生成 HTTP 重定向响应,render_template()函数中,我们使用session.get('name')直接从会话中读取name参数值

redirect()的参数使重定向的 URL
url_for()函数的第一个且唯一必须指定的参数是端点名,即路由的内部名称

闪现消息

请求完成后,有时需要让用户知道状态发生了改变,可以是确认消息、警告或者错误提醒

Flask 内置该功能,用flash()函数实现

hello.py:闪现消息

1
2
3
4
5
6
7
8
9
10
11
12
from flask import 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'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'))

示例中,每次提交的名字都会和存储在用户会话中的名字比较,若名字不同便调用flash()函数,在发给客户端的下一个响应中显示一个消息

当然,功能的实现还需要应用的模板来渲染(最好在基模板中渲染闪现消息,这样所有的页面都能显示需要显示的消息)

Flask 把 get_flashed_message()函数开发给模板,用于获取并渲染闪现消息

templates/base.html: 渲染闪现消息

1
2
3
4
5
6
7
8
9
10
11
12
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">&times;</button>
{{ message }}
</div>
{% endfor %}

{% block page_content %}{% endblock %}
</div>
{% endblock %}