Flask 是一个使用 Python 编写的微型 Web 框架。依赖 Jinja2 模板引擎和 Werkzeug WSGI 套件。这两个库的文档请移步:
默认情况下, Flask 不包含数据库抽象层、表单验证以及其他可以用已有库处理 的东西。Flask 通过大量的扩展(第三方包)支持各种各样的功能,以满足各种生产需要。
Flask 支持 Python 3.6 以上版本。如果要支持 async
,因为会用到 contextvars.ContextVar
,所以需要 Python 3.7 以上版本。
当安装 Flask 时,以下依赖库会被自动安装:
flask
命令,并允许添加自定义 管理命令。以下库不会被自动安装。如果我们手动安装了,那么 Flask 会检测到这些库:
flask
命令时为通过 dotenv 设置环境变量提供支持。使用 pip 安装:
pip install Flask
创建一个hello.py文件(名称随意,但不要叫flask),写入以下内容:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "Hello, World!
"
代码解析:
__name__
,因为,我们的所有东西都在这一个文件中。route()
装饰器来告诉 Flask 触该函数的 URL 。使用 flask 命令运行:
export FLASK_APP=hello
flask run
# Running on http://127.0.0.1:5000/
第一条命令设置了一个环境变量,以便 flask 找到我们的 flask 应用。但如果文件名为 app.py
或者 wsgi.py
,那么就不需要设置 FLASK_APP
环境变量。
运行第二条命令后打开http://127.0.0.1:5000/
,就可以看到“Hello World!”字样。
flask run
命令默认以调试模式运行,在该模式下,服务器会在修改应用代码之后自动重启,并在页面上展示错误信息。不仅如此,在页面上,还提供了交互调试器,输入应用启动时终端中提供的Debugger PIN,就能在页面中进行调试。
如果需要打开所有开发功能,那么需要在运行 flask run
之前设置 FLASK_ENV
环境变量为 development
:
export FLASK_ENV=development
flask run
当返回 HTML ( Flask 中的默认响应类型)时,为了防止注入攻击,所有用户提供的值在输出渲染前必须被转义。
手动转义方法:
from markupsafe import escape
@app.route("/" )
def hello(name):
return f"Hello, {escape(name)}!"
路由中的
从 URL 中捕获值并将其传递给视图函数,假如用户提交的name
为,那么这段代码会被转义为字符串直接输出,不会被渲染执行。
使用 Jinja2引擎(这个稍后会介绍)渲染的 HTML 模板会自动执行此操作。
使用 route()
装饰器将视图函数绑定到 URL:
@app.route('/')
def index():
return 'Index Page'
@app.route('/hello')
def hello():
return 'Hello, World'
通过把 URL 的一部分标记为 <变量名称>
就可以在 URL 中捕获变量。捕获的部分会作为关键字参数传递给函数。通过使用 <转换器:变量名>
,可以选择性的加上一个转换器,用以指定参数的类型。
转换器类型:
转换器 | 说明 |
---|---|
string |
(默认值) 接受任何不包含斜杠的文本 |
int |
接受正整数 |
float |
接受正浮点数 |
path |
类似string ,但可以包含斜杠 |
uuid |
接受 UUID 字符串 |
关于 URL 的制定规则,推荐在末尾带一个斜杠,如/index/
。如果用户在请求该 URL 时,写成了0/index
,那么 Flask 会自动进行重定向到/index/
。
如果开发者在制定 URL 时,没有带斜杠,如index
。那么,用户在访问index/
时,就会得到一个“404 not found”错误。这有助于保持这些资源的 url 唯一,还有助于搜索引擎避免对同一个页面进行两次索引。
url_for()
函数用于构建指定函数的 URL。第一个参数是函数名,它还可以接收任意个关键字参数,对应 URL 中的变量。其他参数会被当做 URL 的查询参数。
相对于硬编码到模板中的 URL,使用反向解析函数url_for()
来构建URL的好处如下:
url_for()
会帮我们转义特殊字符;/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/' )
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 的默认名称就是视图函数名称。
默认情况下,一个路由只回应 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
方法。
Flask 为我们做好了在开发过程中的静态文件服务,我们只需在项目中创建一个名为“static”的文件夹,将静态文件放入里面就行了。
静态文件位于应用的/static
中,使用特定的 'static'
端点(end point,就是 URL 的名称)就可以生成相应的 URL :
url_for('static', filename='style.css') # url_for 可以通过 URL 的名称,反向解析出 URL
这个静态文件在文件系统中的位置应该是 static/style.css
。
在 python 内部处理 HTML 不仅繁琐、笨拙还不安全。因此,flask 已经为我们配置好了jinja2 模板引擎。
使用render_template()
方法可以渲染模板,我们只要提供模板名称和需要作为参数传递给模板的变量就行了:
from flask import render_template
@app.route('/hello/')
@app.route('/hello/' )
def hello(name=None):
return render_template('hello.html', name=name)
Flask 会在 templates
文件夹内寻找模板,该文件应该和上面的代码处于同一目录下。
自动转义默认开启。如果您可以信任某个变量,且知道它是安全的 HTML,那么可以使用Markup
类把 它标记为安全的,或者在模板中使用 |safe
过滤器:
from markupsafe import Markup
Markup('Hello %s!') % ''
关于 jinja2 模板引擎的使用,请参考官方文档:传送门
在 Flask 中由全局对象request
来提供请求信息。如果你有一些 Python 的经验,你可能会想知道:既然这个对象是全局的,怎么还能保持线程安全?答案是本地环境。
如果您想了解工作原理和如何使用本地环境进行测试,那么请阅读本节, 否则可以跳过本节。
request 对象在 Flask 中是全局对象,但不是通常意义下的全局对象。这些对象实际上是特定环境下本地对象的代理,看上去很晦涩,但其实含好理解。
设想现在处于处理线程的环境中。一个请求进来了,服务器决定生成一个新线程-。当 Flask 开始处理该请求时,会把当前线程作为活动环境,并把当前应用和 WSGI 环境绑定到这个环境(线程)。它以一种巧妙的方式使得一个应用可以在不中断的情况下调用另一个应用。
这对我们有什么用?基本上可以完全不理会。这个只有在做单元测试时才有用。在测试时会遇到由于没有请求对象而导致依赖于请求的代码会突然崩溃的情况。对策是自己创建 一个请求对象并绑定到环境。最简单的单元测试解决方案是使用 test_request_context()
环境管理器。通过使用 with
语句可以绑定一个测试请求,以便于交互。例如:
from flask import request
with app.test_request_context('/hello', method='POST'):
# now you can do something with the request until the
# end of the with block, such as basic assertions:
assert request.path == '/hello'
assert request.method == 'POST'
另一种方式是把整个 WSGI 环境传递给 request_context()
方法:
with app.request_context(environ):
assert request.method == 'POST'
通过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'
# 如果请求方法是GET或凭据无效,则执行下面的代码
return render_template('login.html', error=error)
当要获取的 key 不在form
属性中时,会抛出一个KeyError
异常。如果没有处理该异常,那么会显示一个“HTTP 400 Bad Request”错误页面。
要获取 URL 中的参数(如?key=value
),可以使用args
属性:
search_word = request.args.get('key', '')
用 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)}")
...
获取 cookies,可以使用cookies
属性。设置 cookies,可以使用set_cookie
方法。请求对象的 cookies
属性是一个包含了客户端传输的所有 cookies 的字典。
from flask import request
@app.route('/')
def index():
# 获取 cookies
username = request.cookies.get('username')
# 设置 cookie
resp = make_response(render_template(...)) # 生成响应对象
resp.set_cookie('username', 'the username')
……
在 Flask 中,如果使用会话(session),那么就不要直接使用 cookies ,因为会话比较安全一些。
使用 redirect()
函数可以重定向。使用abort()
可以更早退出请求,并返回错误代码:
from flask import abort, redirect, url_for
@app.route('/')
def index():
return redirect(url_for('login')) # 这里使用了url_for 进行反向解析 URL
@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。
视图函数的返回值会自动转换为一个响应对象:
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
如果视图返回一个 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])
当然,我们也可以通过第三方扩展来解决更复杂的情况。
会话对象相当于用密钥加密的 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 浏览器支持的大小是否符合。
有时候可能会遇到数据出错需要纠正的情况,这时候我们就需要查看错误信息,而日志的作用就是记录应用程序运行过程中的信息,包括错误信息。
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_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-SQLAlchemy 为我们在 Flask 中轻松使用 SQLAlchemy 提供了支持。
Flask 的扩展的扩展通常命名为“Flask-xxx”或者“xxx-Flask”。可以在 PyPI 中搜索标记为 Framework :: Flask 的扩展包。
请参阅每个扩展的文档以了解其安装、配置和使用说明。
一般来说,扩展将自身的配置在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)
虽然 PyPI 已经包含许多 Flask 扩展,但是如果找不到合适的, 那么可以创建自己的扩展。
扩展的创建方法可以参考官方文档中《Flask Extension Development》一章,也可以看我后续的文章。