一、Flask简介
Flask 是一个使用 Python 编写的微型 Web 框架。依赖 Jinja2 模板引擎和 Werkzeug WSGI 套件。这两个库的文档请移步:
默认情况下, Flask 不包含数据库抽象层、表单验证以及其他可以用已有库处理 的东西。Flask 通过大量的扩展(第三方包)支持各种各样的功能,以满足各种生产需要。
1.1 安装
Flask 支持 Python 3.6 以上版本。如果要支持 async ,因为会用到 contextvars.ContextVar ,所以需要 Python 3.7 以上版本。
1.2 依赖
当安装 Flask 时,以下依赖库会被自动安装:
- Werkzeug 用于实现 WSGI ,应用和服务之间的标准 Python 接口。
- Jinja2 用于渲染页面的模板语言。
- MarkupSafe 与 Jinja2 共用,在渲染页面时用于避免不可信的输入,防止注入攻击。
- ItsDangerous 保证数据完整性的安全标志数据,用于保护 Flask 的 session cookie.
- Click 是一个命令行应用的框架。用于提供
flask 命令,并允许添加自定义 管理命令。
1.3 可选依赖
以下库不会被自动安装。如果我们手动安装了,那么 Flask 会检测到这些库:
1.4 安装 Flask
使用 pip 安装:
pip install Flask
二、快速入门
2.1 最简单的 Flask 应用
创建一个hello.py文件(名称随意,但不要叫flask),写入以下内容:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
代码解析:
- 首先,我们导入了 Flask 类,该类的实例将会称为我们的 WSGI 应用。
- 然后,我们创建了 Flask 类的实例,参数是应用模块或者包的名称。通过该参数,Flask 才能知道在哪里可以找到模板和静态文件等东西。这里,我们使用了
__name__ ,因为,我们的所有东西都在这一个文件中。 - 接着,我们使用
route() 装饰器来告诉 Flask 触该函数的 URL 。 - 最后,返回了需要在浏览器中展示的数据。默认的类型是 HTML,此字符串中的 HTML 会被浏览器渲染。
使用 flask 命令运行:
export FLASK_APP=hello
flask run
第一条命令设置了一个环境变量,以便 flask 找到我们的 flask 应用。但如果文件名为 app.py 或者 wsgi.py ,那么就不需要设置 FLASK_APP 环境变量。
运行第二条命令后打开http://127.0.0.1:5000/ ,就可以看到“Hello World!”字样。
2.2 调试模式
flask run 命令默认以调试模式运行,在该模式下,服务器会在修改应用代码之后自动重启,并在页面上展示错误信息。不仅如此,在页面上,还提供了交互调试器,输入应用启动时终端中提供的Debugger PIN,就能在页面中进行调试。
如果需要打开所有开发功能,那么需要在运行 flask run 之前设置 FLASK_ENV 环境变量为 development :
export FLASK_ENV=development
flask run
2.3 HTML 转义
当返回 HTML ( Flask 中的默认响应类型)时,为了防止注入攻击,所有用户提供的值在输出渲染前必须被转义。
手动转义方法:
from markupsafe import escape
@app.route("/<name>")
def hello(name):
return f"Hello, {escape(name)}!"
路由中的 <name> 从 URL 中捕获值并将其传递给视图函数,假如用户提交的name 为<script>alert("bad")</script> ,那么这段代码会被转义为字符串直接输出,不会被渲染执行。
使用 Jinja2引擎(这个稍后会介绍)渲染的 HTML 模板会自动执行此操作。
2.4 路由
使用 route() 装饰器将视图函数绑定到 URL:
@app.route('/')
def index():
return 'Index Page'
@app.route('/hello')
def hello():
return 'Hello, World'
2.4.1 变量规则
通过把 URL 的一部分标记为 <变量名称> 就可以在 URL 中捕获变量。捕获的部分会作为关键字参数传递给函数。通过使用 <转换器:变量名> ,可以选择性的加上一个转换器,用以指定参数的类型。
转换器类型:
转换器 | 说明 |
---|
string | (默认值) 接受任何不包含斜杠的文本 | int | 接受正整数 | float | 接受正浮点数 | path | 类似string ,但可以包含斜杠 | uuid | 接受 UUID 字符串 |
2.4.2 重定向行为
关于 URL 的制定规则,推荐在末尾带一个斜杠,如/index/ 。如果用户在请求该 URL 时,写成了0/index ,那么 Flask 会自动进行重定向到/index/ 。
如果开发者在制定 URL 时,没有带斜杠,如index 。那么,用户在访问index/ 时,就会得到一个“404 not found”错误。这有助于保持这些资源的 url 唯一,还有助于搜索引擎避免对同一个页面进行两次索引。
2.4.3 URL 构建
url_for() 函数用于构建指定函数的 URL。第一个参数是函数名,它还可以接收任意个关键字参数,对应 URL 中的变量。其他参数会被当做 URL 的查询参数。
相对于硬编码到模板中的 URL,使用反向解析函数url_for() 来构建URL的好处如下:
- 反向解析比硬编码的 URL 更加直观;
- 方便集中管理 URL,只需要修改一次,而不是修改所有用到该 URL 的地方;
url_for() 会帮我们转义特殊字符;- 生成的路径总是绝对的,避免了浏览器中相对路径的意外行为;
- 如果你的应用被放置在 URL 根目录之外,例如,在
/myapplication 而不是/ ,url_for() 会正确地为你处理它。
例如,这里我们使用 test_request_context() 方法来尝试使用 url_for() 。 test_request_context() 告诉 Flask 正在处理一个请求,但实际上我们只是在代码中测试,没有真正发出请求:
from flask import url_for
app = Flask(__name__)
@app.route('/')
def index():
return 'index'
@app.route('/login')
def login():
return 'login'
@app.route('/user/<username>')
def profile(username):
return f'{username}\'s profile'
with app.test_request_context():
print(url_for('index'))
print(url_for('login'))
print(url_for('login', next='/'))
print(url_for('profile', username='John Doe'))
"""
结果如下:
/
/login
/login?next=/
/user/John%20Doe
"""
注意:在 flask 中,URL 的默认名称就是视图函数名称。
2.4.4 HTTP 方法
默认情况下,一个路由只回应 GET 请求。 但我们可以使用 route() 装饰器的 methods 参数来处理不同的 HTTP 方法:
from flask import request
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
return do_the_login()
else:
return show_the_login_form()
如果当前函数使用了 GET 方法, Flask 会自动添加 HEAD 方法和OPTIONS 方法。
2.5 静态文件
Flask 为我们做好了在开发过程中的静态文件服务,我们只需在项目中创建一个名为“static”的文件夹,将静态文件放入里面就行了。
静态文件位于应用的/static 中,使用特定的 'static' 端点(end point,就是 URL 的名称)就可以生成相应的 URL :
url_for('static', filename='style.css')
这个静态文件在文件系统中的位置应该是 static/style.css 。
2.6 渲染模板
在 python 内部处理 HTML 不仅繁琐、笨拙还不安全。因此,flask 已经为我们配置好了jinja2 模板引擎。
使用render_template() 方法可以渲染模板,我们只要提供模板名称和需要作为参数传递给模板的变量就行了:
from flask import render_template
@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
return render_template('hello.html', name=name)
Flask 会在 templates 文件夹内寻找模板,该文件应该和上面的代码处于同一目录下。
自动转义默认开启。如果您可以信任某个变量,且知道它是安全的 HTML,那么可以使用Markup 类把 它标记为安全的,或者在模板中使用 |safe 过滤器:
from markupsafe import Markup
Markup('<strong>Hello %s!</strong>') % '<blink>hacker</blink>'
关于 jinja2 模板引擎的使用,请参考官方文档:传送门
2.7 访问请求数据
在 Flask 中由全局对象request 来提供请求信息。如果你有一些 Python 的经验,你可能会想知道:既然这个对象是全局的,怎么还能保持线程安全?答案是本地环境。
2.7.1 本地环境
如果您想了解工作原理和如何使用本地环境进行测试,那么请阅读本节, 否则可以跳过本节。
request 对象在 Flask 中是全局对象,但不是通常意义下的全局对象。这些对象实际上是特定环境下本地对象的代理,看上去很晦涩,但其实含好理解。
设想现在处于处理线程的环境中。一个请求进来了,服务器决定生成一个新线程-。当 Flask 开始处理该请求时,会把当前线程作为活动环境,并把当前应用和 WSGI 环境绑定到这个环境(线程)。它以一种巧妙的方式使得一个应用可以在不中断的情况下调用另一个应用。
这对我们有什么用?基本上可以完全不理会。这个只有在做单元测试时才有用。在测试时会遇到由于没有请求对象而导致依赖于请求的代码会突然崩溃的情况。对策是自己创建 一个请求对象并绑定到环境。最简单的单元测试解决方案是使用 test_request_context() 环境管理器。通过使用 with 语句可以绑定一个测试请求,以便于交互。例如:
from flask import request
with app.test_request_context('/hello', method='POST'):
assert request.path == '/hello'
assert request.method == 'POST'
另一种方式是把整个 WSGI 环境传递给 request_context() 方法:
with app.request_context(environ):
assert request.method == 'POST'
2.7.2 请求对象(request)
通过request.method 属性可以获取当前的请求方法,request.form 属性用于处理表单数据(在 POST 或者 PUT 请求 中传输的数据),比如:
from flask import request
@app.route('/login', methods=['POST', 'GET'])
def login():
error = None
if request.method == 'POST':
if valid_login(request.form['username'],
request.form['password']):
return log_the_user_in(request.form['username'])
else:
error = 'Invalid username/password'
return render_template('login.html', error=error)
当要获取的 key 不在form 属性中时,会抛出一个KeyError 异常。如果没有处理该异常,那么会显示一个“HTTP 400 Bad Request”错误页面。
要获取 URL 中的参数(如?key=value ),可以使用args 属性:
search_word = request.args.get('key', '')
2.7.3 文件上传
用 Flask 处理文件上传很容易,只要确保在 HTML 表单中设置 enctype="multipart/form-data" 属性就可以了,否则浏览器将不会传送用户提交的文件。
已上传的文件被储存在内存或文件系统的临时位置,通过**files 属性就可以访问上传的文件**,每个上传的文件都储存在这个属性中。这个属性和 python 的字典类型基本一致,只是多了一个用于把上传文件保存到服务器的文件系统中的 save() 方法。用法举例:
from flask import request
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['the_file']
f.save('/var/www/uploads/uploaded_file.txt')
...
如果想要知道文件上传之前其在客户端中的名称,可以使用 filename 属性,但是这个值是可以伪造的,永远不要信任这个值。如果想要完全真实的文件名,可以通过 Werkzeug 提供的 secure_filename() 函数:
from werkzeug.utils import secure_filename
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
file = request.files['the_file']
file.save(f"/var/www/uploads/{secure_filename(file.filename)}")
...
2.7.4 Cookies
获取 cookies,可以使用cookies 属性。设置 cookies,可以使用set_cookie 方法。请求对象的 cookies 属性是一个包含了客户端传输的所有 cookies 的字典。
from flask import request
@app.route('/')
def index():
username = request.cookies.get('username')
resp = make_response(render_template(...))
resp.set_cookie('username', 'the username')
……
在 Flask 中,如果使用会话(session),那么就不要直接使用 cookies ,因为会话比较安全一些。
2.8 重定向和错误
使用 redirect() 函数可以重定向。使用abort() 可以更早退出请求,并返回错误代码:
from flask import abort, redirect, url_for
@app.route('/')
def index():
return redirect(url_for('login'))
@app.route('/login')
def login():
abort(401)
默认情况下每种错误代码都会对应显示一个错误页面。使用 errorhandler() 装饰器可以定制出错页面:
@app.errorhandler(404)
def page_not_found(error):
return render_template('404.html'), 404
注意render_template() 后面的 404 ,这是发送给浏览器的本次响应的响应状态码,默认是200。
2.9 响应
视图函数的返回值会自动转换为一个响应对象:
- 如果返回值是一个字符串,那么字符串会被作为响应体,然后转换为一个
text/html 类型的响应对象,状态码为200 OK 。 - 如果返回值是一个字典,那么会调用
jsonify() 来产生一个响应。
以下是转换的规则:
-
如果视图返回的是一个响应对象,那么就直接返回它。 -
如果返回的是一个字符串,那么根据这个字符串和默认参数生成一个响应对象。 -
如果返回的是一个字典,那么调用 jsonify() 创建一个响应对象。 -
如果返回的是一个元组,那么元组中必须至少包含一个特定元素:(response, status) 、 (response, headers) 或 (response, status, headers) 。 status 的值会重载状态代码, headers 是一个由额外头部值组成的列表或字典。 -
如果以上都不是,那么 Flask 会假定返回值是一个有效的 WSGI 应用并把它转换为 一个响应对象。
如果想要在视图内部掌控响应对象的结果,那么可以使用 make_response() 函数。我们在设置 cookie 时就用到它。下面是另外一个例子:
@app.errorhandler(404)
def not_found(error):
resp = make_response(render_template('error.html'), 404)
resp.headers['X-Something'] = 'A value'
return resp
2.9.1 JSON 格式的 API
如果视图返回一个 dict ,那么它会被转换为一个 JSON 响应:
@app.route("/me")
def me_api():
user = get_current_user()
return {
"username": user.username,
"theme": user.theme,
"image": url_for("user_image", filename=user.image),
}
如果字典不能满足需求,可以使用jsonify() 函数,该函数会序列化任何可以被序列化为 JSON 类型的数据。
@app.route("/users")
def users_api():
users = get_all_users()
return jsonify([user.to_json() for user in users])
当然,我们也可以通过第三方扩展来解决更复杂的情况。
2.10 会话(session)
会话对象相当于用密钥加密的 cookie ,即用户可以查看 cookie ,但是如果没有密钥就无法修改它。
使用会话之前您必须设置一个密钥:
from flask import session
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
使用下面的命令可以快捷的为 Flask.secret_key 生成一个值:
$ python -c 'import os; print(os.urandom(16))'
b'_5#y2L"F4Q8z\n\xec]/'
使用会话:
@app.route('/')
def index():
if 'username' in session:
return f'用户{session["username"]}已经登陆'
return '你还没有登陆'
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
session['username'] = request.form['username']
return redirect(url_for('index'))
@app.route('/logout')
def logout():
session.pop('username', None)
return redirect(url_for('index'))
Flask 将会话对象中的值序列化到 cookie 中。如果你发现一些值不能在请求之间持久存在,而 cookie 确实被启用了,并且你没有得到一个明确的错误消息。那么,请检查你的页面响应中的 cookie 的大小与 web 浏览器支持的大小是否符合。
2.11 日志
有时候可能会遇到数据出错需要纠正的情况,这时候我们就需要查看错误信息,而日志的作用就是记录应用程序运行过程中的信息,包括错误信息。
Flask 已经为我们配置好了一个日志工具,下面是一个简单使用示例:
app.logger.debug('A value for debugging')
app.logger.warning('A warning occurred (%d apples)', 42)
app.logger.error('An error occurred')
三、集成 WSGI 中间件
如果想要在应用中添加一个 WSGI 中间件,那么可以用应用的 wsgi_app 属性来包装。例如,假设需要在 Nginx 后面使用 ProxyFix 中间件,那么可以这样做:
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app)
用 app.wsgi_app 来包装,而不用 app 包装,意味着 app 仍旧指向 Flask 应用,而不是指向中间件。这样可以继续直接使用和配置 app 。
四、使用 Flask 扩展
扩展是指为 Flask 应用增加功能的包。例如 Flask-SQLAlchemy 为我们在 Flask 中轻松使用 SQLAlchemy 提供了支持。
4.1 寻找扩展
Flask 的扩展的扩展通常命名为“Flask-xxx”或者“xxx-Flask”。可以在 PyPI 中搜索标记为 Framework :: Flask 的扩展包。
4.2 使用扩展
请参阅每个扩展的文档以了解其安装、配置和使用说明。
一般来说,扩展将自身的配置在app初始化时,通过app.config 传递给 Flask 应用,一个名为“ Flask-Foo ”的扩展使用如下:
from flask_foo import Foo
foo = Foo()
app = Flask(__name__)
app.config.update(
FOO_BAR='baz',
FOO_SPAM='eggs',
)
foo.init_app(app)
4.3 创建扩展
虽然 PyPI 已经包含许多 Flask 扩展,但是如果找不到合适的, 那么可以创建自己的扩展。
扩展的创建方法可以参考官方文档中《Flask Extension Development》一章,也可以看我后续的文章。
|