Flask 入门 - app 的基本结构

初始化

所有 Flask 程序都必须创建一个 app 实例。Web 服务器使用 Web 服务器网关接口协议(Web Server Gateway Interface, WSGI)把接收自客户端的所有请求都转交给这个对象处理。 app 实例是 Flask 类的对象:

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

Flask 类的构造函数自有一个必须指定的参数, 即 app 主模块或包的名字。

Flask 用 name 这个参数决定程序的根目录, 以便稍后能够找到相对于程序根目录的资源文件位置。

路由和视图函数

客户端(例如 Web 浏览器)把请求发送给 Web 服务器, Web 服务器再把请求发送给 Flask app 实例。 app 实例需要知道对每个 URL 请求运行哪些代码, 所以 app 实例保存了一个 URL 到 Python 函数的映射关系。处理 URL 和函数之间关系的程序称为路由。

在 Flask 中使用 app 实例提供的 app.route 装饰器把所装饰的函数注册为路由:

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

装饰器是可以把函数注册为事件的处理程序。

前例是把 index() 函数注册为 app 根地址的处理程序。如果部署的程序的服务器域名为 www.example.com, 在浏览器中访问 http://www.example.com 后会触发服务器执行 index() 函数。这个函数的返回值称为 响应, 它是客户端接收到的内容。如果客户端是 Web 浏览器, 响应就是显示给用户看的文档。

像 index() 这样的函数称之为 视图函数(view function)。视图函数返回的响应可以是包含 HTML 的简单字符串,也可以是复杂的表单。

可变 URL:

1
2
3
@app.route('/user/<name>')
def user(name):
return '<h1>Hello, %s!</h1>' % name

路由中的动态部分默认使用字符串, 也可以使用 int/float/path 类型, path 类型也是字符串, 但不把斜线作为分割符, 而将其当作动态片段的一部分。

1
@app.route('/user/<int:id>')

启动服务器

app 实例使用 run 方法启动 Flask 集成的 Web 服务器:

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

__name__ == '__main__' 确保了只有直接 执行这个脚本时才启动 Web 服务器。如果这个脚本由其它脚本引入, 程序假定父级脚本会启动不同的服务器, 因此不会执行 app.run()

服务器启动后会进入轮询, 等待并处理请求, 轮询会一直运行,直到程序停止,例如按 Ctrl-C 键。

一个完整的 app

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

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

@app.route('user/<name>')
def user(name):
return '<h1>hello, %s!</h1>' % name

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

启动这个 app:

1
2
3
(venv) $ python hello.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader

在浏览器中键入:

1
http://localhost:5000/user/Dave

会显示:

Hello, Dave!


请求/响应循环

app 和请求上下文

Flask 从客户端收到请求时, 要让视图函数能访问一些对象, 这样才能处理请求。请求对象封装了客户端(例如浏览器)发送的 HTTP 请求。

要让视图函数能访问请求对象, 一个显而易见的方式是把请求对象作为参数传递给视图函数, 不过这会导致程序中每个视图函数都增加一个参数。如果视图函数还要访问其它对象, 那么视图函数会变得越来越臃肿和难以维护。

为此, Flask 使用 上下文 临时把某些对象变为全局可访问:

1
2
3
4
5
from flask import request
@app.route('/')
def index():
user_agent = request.headers.get('User-Agent')
return '<p>你的浏览器是 %s</p>' % user_agent

在这个例子中我们把 request 当作全局变量来使用。事实上, request 不可能是全局变量, 你想想, 在多个线程同时处理不同客户端发送的不同请求时, 每个线程看到的 request 对象必然不同。 Flask 使用上下文让特定的变量在每一个线程中全局可访问, 与此同时却不会干扰其它线程。

多线程 Web 服务器会创建一个线程池, 再从线程池中选择一个线程用于处理接收到的请求。

在 Flask 中有两种上下文: app 上下文请求上下文。下表列出了这两种上下文提供的全局变量:

变量名 上下文 说明
current_app app上下文 当前所激活app的app实例
g app上下文 处理请求时用作临时存储的对象。每次请求都会重设这个变量
request 请求上下文 请求对象, 封装了客户端发出的 HTTP 请求中的内容
session 请求上下文 用户会话, 用于存储请求之间需要”记住”的值的字典

Flask 在分发请求之前激活(或推送)app上下文和请求上下文, 请求处理完成后再将其删除。 app 上下文在被推送后, 就可以在线程中使用 current_appg 变量。类似地, 在请求上下文被推送后, 就可以使用 requestsession 变量。如果我们使用这些变量时没有激活 app 上下文或请求上下文, 那么程序就会出错。

激活虚拟环境后进入 Python shell, 下面演示app上下文的使用方法:

1
2
3
4
5
6
7
8
9
10
>>> from hello import app
>>> from flask import current_app
>>> current_app.name
...
RuntimeError: Working outside of application context.
>>> app_ctx = app.app_context()
>>> app_ctx.push() # 推送 app 上下文
>>> current_app.name
'hello'
>>> app_ctx.pop() # 弹出 app 上下文

在这个例子中, 没有激活 app 上下文之前就调用 current_app.name 就会导致错误, 但是推送完上下文之后就可以调用了。

注意, 在 app 实例上调用 .app_context() 方法便获得了一个程序上下文。

请求调度

程序收到客户端发来的请求时, 要找到处理该请求的视图函数。Flask 通过在 app 的 URL 映射中查找请求的 URL 来完成这个任务。 URL 映射是 URL 和视图函数之间的对应关系。 Flask 使用 app.route 装饰器/非装饰器形式的 app.add_url_rule() 生成映射。

我们在 Python shell 中查看 Flask 的 app 中映射是什么样子的:(所有操作请确保你已经激活了虚拟环境)

1
2
3
4
5
6
7
(venv) $ python
>>> from hello import app
>>> app.url_map
>>> app.url_map
Map([<Rule '/' (HEAD, OPTIONS, GET) -> index>,
<Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
<Rule '/user/<name>' (HEAD, OPTIONS, GET) -> user>])

//usr/<name> 路由在 app 中使用 app.route 装饰器定义。 /static/<filename> 路由是 Flask 添加的特殊路由, 用于访问静态文件。

URL 映射中的 HEAD、OPTIONS、GET 是请求方法。Flask 为每个路由都指定了请求方法, 这样不同的请求发送到相同的 URL 上时, 会使用不同的视图函数进行处理。 HEAD 和 OPTIONS 方法由 Flask 自动处理, 因此可以说上面的 app 中 URL 映射中的 3 个路由都使用 GET 方法。

请求钩子

有时需要在请求之前或之后执行代码会很有用。例如, 在请求开始时我们可能需要创建数据库连接/认证发起请求的用户。为了避免在每个视图函数中都使用重复的代码, Flask 提供了注册通用函数的功能, 注册的函数可在请求被分发到视图函数之前/之后被调用。

请求钩子 使用装饰器实现。 Flask 支持以下 4 种钩子:

  • before_first_request: 注册一个函数, 在处理第一个请求之前运行。
  • before_request: 注册一个函数, 在每次请求之前运行。
  • after_request: 注册一个函数, 如果没有未处理的异常抛出, 则在每次请求之后运行。
  • teardown_request: 注册一个函数, 即使有未处理的异常抛出, 也在每次请求之后运行。

在请求钩子和视图函数之间共享数据一般使用上下文全局变量 g。 例如 before_request 处理程序可以从数据库中加载已登录用户, 并将其保存到 g.user 中。随后调用视图函数时, 视图函数再使用 g.user 获取用户。

响应

Flask 调用视图函数后, 会将其返回值作为响应的内容。大多数情况下, 响应就是一个简单的字符串, 作为 HTML 页面回送客户端。

在响应的文本之后添加一个状态码:

1
2
3
@app.route('/')
def index():
return '<h1>Bad Request</h1>', 400

表示请求无效。

视图函数返回的响应还可以接收第三个参数, 这是一个由首部(header)组成的字典, 可以添加到 HTTP 响应中。

Flask 还可以返回 Response 对象。 make_response() 函数可接受 1/2/3 个参数(和视图函数的返回值一样), 并返回一个 Response 对象。

1
2
3
4
5
6
from flask import make_response
@app.route('/')
def index():
response = make_response('<h1>This document carries a cookie!</h1>')
response.set_cookie('answer', '42')
return response

重定向是一种特殊的响应类型。这种响应类型没有页面文档, 只告诉浏览器一个新的地址用以加载新页面。Flask 已经提供了 redirect() 函数:

1
2
3
4
from flask import redirect
@app.route('/')
def index():
return redirect('http://www.example.com')

还有一种特殊的 abort 响应, 用于处理错误:

1
2
3
4
5
6
7
from flask import abort
@app.route('/user/<id>')
def get_user(id):
user = load_user(id):
if not user:
abort(404)
return '<h1>Hello, %s!</h1>' % user.name

Flask 扩展

使用Flask-Script支持命令行选项

Flask 的开发 Web 服务器支持很多启动设置选项,但只能在脚本中作为参数传给 app.run()函数。这种方式并不十分方便,传递设置选项的理想方式是使用命令行参数。

Flask-Script 是一个 Flask 扩展,为 Flask 程序添加了一个命令行解析器。Flask-Script 自带了一组常用选项,而且还支持自定义命令。

1
2
# 安装
(venv) $ pip install flask-script

要使用 flask-script 需要在 hello.py 修改下程序:

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask
from flask.ext.script import Manager

app = Flask(__name__)
manager = Manager(app)

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

if __name__ == '__main__':
manager.run()

专为 Flask 开发的扩展都暴漏在 flask.ext 命名空间下。Flask-Script 输出了一个名为Manager 的类,可从 flask.ext.script 中引入。

这个扩展的初始化方法也适用于其他很多扩展:把 app 实例作为参数传给扩展的构造函数,初始化主类的实例。创建的对象可以在各个扩展中使用。在这里,服务器由 manager.run() 启动,启动后就能解析命令行了。

注意, 在 Python 3 中要这样导入 flask-script 扩展, from flask_script import Manager

现在运行 hello.py,会显示一个帮助消息:

1
2
3
4
5
6
7
8
9
10
$ python hello.py
usage: hello.py [-h] {shell,runserver} ...

positional arguments:
{shell,runserver}
shell 在 Flask 应用上下文中运行 Python shell
runserver 运行 Flask 开发服务器:app.run()

optional arguments:
-h, --help 显示帮助信息并退出

shell 命令用于在程序的上下文中启动 Python shell 会话。你可以使用这个会话中运行维护任务或测试,还可调试异常。顾名思义, runserver 命令用来启动 Web 服务器。运行 python hello.py runserver 将以调试模式启动 Web 服务器,但是我们还有很多选项可用:

1
2
3
4
$ python hello.py runserver --help
usage: hello.py runserver [-?] [-h HOST] [-p PORT] [--threaded]
[--processes PROCESSES] [--passthrough-errors] [-d]
[-D] [-r] [-R]

--host 参数是个很有用的选项,它告诉 Web 服务器在哪个网络接口上监听来自客户端的连接。默认情况下,Flask 开发 Web 服务器监听 localhost 上的连接,所以只接受来自服务器所在计算机发起的连接。下述命令让 Web 服务器监听公共网络接口上的连接,允许同网中的其他计算机连接服务器:

1
2
3
(venv) $ python hello.py runserver --host 0.0.0.0
* Running on http://0.0.0.0:5000/
* Restarting with reloader

现在,Web 服务器可使用 http://a.b.c.d:5000/ 网络中的任一台电脑进行访问,其中 “a.b.c.d” 是服务器所在计算机的外网 IP 地址。