Flask

Flask 官网定义是一个“微” python web 开发框架

Flask 官方定义的 “微”
“微”不代表 Flask 功能不强。 微框架中的“微”字表示 Flask 的目标是保持核心简单而又可扩展。 Flask 不会替你做出许多决定,比如选用何种数据库。 类似的决定,如使用何种模板引擎,是非常容易改变的。 Flask 可以变成你任何想要的东西,一切恰到好处,由你做主。

缺省情况下, Flask 不包含数据库抽象层、表单验证或者其他已有的库可以处理的东西。 然而, Flask 通过扩展为你的应用添加这些功能,就如同这些功能是 Flask 原生的一样。 大量的扩展用以支持数据库整合、表单验证、上传处理和各种开放验证等等。Flask 可能是 “微小”的,但它已经为满足您的各种生产需要做出了充足的准备。

Flask 具有一个包含基本服务的强健核心,其他功能则可通过扩展实现。Flask 主要依赖 Werkzeug WSGI 和 Jinja 模板引擎套件

Werkzeug

Werkzeug 是一个 WSGI 工具包,可以作为一个Web框架的底层库。
Werkzeug 包括:

  • 交互式调试器,允许使用交互式解释器检查浏览器中的堆栈跟踪和源代码,以了解堆栈中的任何帧。
  • 功能齐全的请求对象,具有可与header,查询参数,表单数据,文件和cookie交互的对象。
  • 可以包装其他WSGI应用程序并处理流数据的响应对象。
  • 一个路由系统,用于将URL匹配到端点并为端点生成URL,并具有可扩展的系统来捕获URL中的变量。
  • HTTP实用程序,用于处理实体标签,缓存控制,日期,user agents,Cookie,文件等。
  • 可在本地开发应用程序时使用的线程化WSGI服务器
  • 一个测试客户端,用于在测试过程中模拟HTTP请求,而无需运行服务器。

    Jinja2

    Jinja2 是一个功能齐全的 Python 模板引擎。
    Jinja2 是 Python 中使用最多的模板引擎之一。它受到 Django 模板系统的启发,但是使用了一种表达性语言来扩展它,这种语言为模板作者提供了一套更强大的工具。除此之外,它还为重要的安全性应用程序增加了沙箱执行和可选的自动转义。

Jinja2 示例:

  1. <title>{% block title %}{% endblock %}</title>
  2. <ul>
  3. {% for user in users %}
  4. <li><a href="{{ user.url }}">{{ user.username }}</a></li>
  5. {% endfor %}
  6. </ul>

特性:

  • 沙箱中执行
  • 强大的 HTML 自动转义系统保护系统免受 XSS
  • 模板继承
  • 及时编译最优的 python 代码
  • 可选提前编译模板的时间
  • 易于调试。异常的行数直接指向模板中的对应行。
  • 可配置的语法

    WSGI

    HTTP 基础

    一个Web应用的本质就是:
  1. 浏览器发送一个HTTP请求;
  2. 服务器收到请求,生成一个HTML文档;
  3. 服务器把HTML文档作为HTTP响应的Body发送给浏览器;
  4. 浏览器收到HTTP响应,从HTTP Body取出HTML文档并显示。

静态 HTML

最简单的Web应用就是先把HTML用文件保存好,用一个现成的HTTP服务器软件,接收用户请求从文件中读取HTML并返回
Apache、Nginx、Lighttpd等这些常见的静态服务器就是干这件事情的。

动态HTML

如果要动态生成HTML,就需要把上述步骤自己来实现。不过,接受HTTP请求解析HTTP请求发送HTTP响应都是苦力活,如果我们自己来写这些底层代码,还没开始写动态HTML呢,就得花个把月去读HTTP规范。

正确的做法是底层代码由专门的服务器软件实现,我们用Python专注于生成HTML文档。因为我们不希望接触到TCP连接、HTTP原始请求和响应格式,所以,需要一个统一的接口,让我们专心用Python编写Web业务。

WSGI 基础

WSGI 是 Python 语言中所定义的 Web 服务器和 Web 应用程序的通用接口标准

客户端和服务器端进行沟通遵循了 HTTP 协议,可以说HTTP就是它们之间沟通的语言。从HTTP请求到我们的Web程序之间,还有另外一个转换过程——从HTTP报文到 WSGI 规定的数据格式。WSGI 可以视为 WSGI服务器和我们的 Web 应用程序进行沟通的语言。

WSGI 是一种协议,这里,需要注意两个相近的概念:

  • uwsgi 同 WSGI一样是一种协议
  • 而 uWSGI 是实现了 uwsgi 和 WSGI 两种协议的web服务器

WSGI 接口定义非常简单,它只要求 Web 开发者实现一个可调用对象,就可以响应HTTP请求。我们来看一个最简单的Web版本的“Hello, web!”:

  1. def application(environ, start_response):
  2. start_response('200 OK', [('Content-Type', 'text/html')])
  3. return [b'<h1>Hello, web!</h1>']

上面的 application() 函数就是符合WSGI标准的一个HTTP处理函数,它接收两个参数:

  • environ:一个包含所有HTTP 请求信息的 dict 对象,HTTP 请求的所有输入信息都可以通过 environ 获取
  • start_response:一个发送 HTTP 响应 Header的函数

函数的返回值将作为HTTP响应的Body发送给浏览器

application() 函数中,调用如下函数就发送了HTTP响应的Header:

  1. start_response('200 OK', [('Content-Type', 'text/html')])

注意Header只能发送一次,也就是只能调用一次start_response()函数。start_response()函数接收两个参数,一个是HTTP响应码,一个是一组 list 表示的 HTTP Header,每个Header用一个包含两个strtuple表示

函数的返回值 b'<h1>Hello, web!</h1>' 将作为HTTP响应的Body发送给浏览器。

根据WSGI的定义,请求和响应的主体应该为字节串(bytestrings),即Python 2中的str类型。在Python 3中字符串默认为unicode类型,因此需要在字符串前添加b前缀,将字符串声明为bytes类型。这里为了兼容两者,统一添加了b前缀

有了WSGI,我们关心的就是如何从environdict 对象拿到 HTTP 请求信息,然后构造HTML,通过start_response()发送Header,最后返回Body。
整个 application() 函数本身没有涉及到任何解析HTTP的部分,也就是说,底层代码不需要我们自己编写,我们只负责在更高层次上考虑如何响应请求就可以了。

不过,等等,这个application()函数怎么调用?如果我们自己调用,两个参数 environstart_response 我们没法提供,返回的 bytes 也没法发给浏览器。
所以 application() 函数必须由WSGI服务器来调用,并提供 environstart_response 这两个参数

Python内置了一个 WSGI 服务器(wsgiref),它是用纯Python编写的WSGI服务器的参考实现。所谓“参考实现”是指该实现完全符合WSGI标准,但是不考虑任何运行效率,仅供开发和测试使用。

运行 WSGI 服务

我们先编写hello.py,实现Web应用程序的WSGI处理函数:

  1. # hello.py
  2. def application(environ, start_response):
  3. start_response('200 OK', [('Content-Type', 'text/html')])
  4. return [b'<h1>Hello, web!</h1>']

然后,再编写一个server.py,负责启动WSGI服务器,加载 application() 函数:

  1. # server.py
  2. # 从wsgiref模块导入:
  3. from wsgiref.simple_server import make_server
  4. # 导入我们自己编写的application函数:
  5. from hello import application
  6. # 创建一个服务器,IP地址为空,端口是8000,处理函数是application:
  7. httpd = make_server('', 8000, application)
  8. print('Serving HTTP on port 8000...')
  9. # 开始监听HTTP请求:
  10. httpd.serve_forever()

确保以上两个文件在同一个目录下,然后在命令行输入 python server.py 来启动WSGI服务器:
Flask  源码解析 00 —— 预备知识 - 图1

注意:如果 8000 端口已被其他程序占用,启动将失败,请修改成其他端口。
启动成功后,打开浏览器,输入 http://localhost:8000/,就可以看到结果了:
Flask  源码解析 00 —— 预备知识 - 图2
在命令行可以看到wsgiref打印的log信息:
Flask  源码解析 00 —— 预备知识 - 图3

Ctrl+C终止服务器。

如果你觉得这个Web应用太简单了,可以稍微改造一下,从environ里读取PATH_INFO,这样可以显示更加动态的内容:

  1. # hello.py
  2. def application(environ, start_response):
  3. start_response('200 OK', [('Content-Type', 'text/html')])
  4. body = '<h1>Hello, %s!</h1>' % (environ['PATH_INFO'][1:] or 'web')
  5. return [body.encode('utf-8')]

你可以在地址栏输入用户名作为URL的一部分,将返回Hello, xxx!
Flask  源码解析 00 —— 预备知识 - 图4
是不是有点Web App的感觉了?

WSGI、Werkzeug、Flask之间的关系

image.png

  • Flask 是一个基于 Python 开发并且依赖 jinja2 模板和 Werkzeug WSGI 服务的一个微型框架
  • Werkzeug 是一个 WSGI 工具包,作为 Flask 框架的底层库

Flask 中实现类似的 WSGI 程序,只不过对请求和响应的处理要丰富完善得多。在Flask中,这个可调用对象就是Flask 类的实例。

  • 通过 __call__ 方法将 Flask 对象变为可调用,以便 WSGI 服务器调用

    1. def __call__(self, environ, start_response):
    2. """The WSGI server calls the Flask application object as the
    3. WSGI application. This calls :meth:`wsgi_app` which can be
    4. wrapped to applying middleware."""
    5. return self.wsgi_app(environ, start_response)
  • 实现 wsgi_app 函数,处理 web 服务器转发的请求

    1. def wsgi_app(self, environ, start_response):
    2. ctx = self.request_context(environ)
    3. error = None
    4. try:
    5. try:
    6. ctx.push()
    7. response = self.full_dispatch_request()
    8. except Exception as e:
    9. error = e
    10. response = self.handle_exception(e)
    11. except: # noqa: B001
    12. error = sys.exc_info()[1]
    13. raise
    14. return response(environ, start_response)
    15. finally:
    16. if self.should_ignore_error(error):
    17. error = None
    18. ctx.auto_pop(error)

full_dsipatch_request 进行请求转发并找到对应 url 的处理函数,并返回处理结果

中间件

WSGI 允许使用中间件(Middleware)包装(wrap)应用程序,在应用程序被调用前添加额外的设置和功能。当接收到请求后,会先调用可调用对象外层的中间件,然后才调用可调用对象

这个特性经常被用来解耦程序的功能,这样可以将不同功能分开维护,达到分层的目的,同时也根据需要嵌套。

为 WSGI 程序添加中间件

  1. from wsgiref.simple_server import make_server
  2. def hello(environ, start_response):
  3. start_response('200 OK', [('Content-Type', 'text/html')])
  4. return [b'<h1>Hello, web!</h1>']
  5. class MyMiddleware():
  6. def __init__(self, app):
  7. self.app = app
  8. def __call__(self, environ, start_response):
  9. def custom_start_response(status, headers, exc_info=None):
  10. headers.append(('A-CUSTON-HEADER', 'Nothing'))
  11. start_response(status, headers)
  12. return self.app(environ, custom_start_response)
  13. wrapper_app = MyMiddleware(hello)
  14. server = make_server('', 5000, wrapper_app)
  15. server.serve_forever()

中间件接收可调用对象作为参数。这个可调用对象也可以是被其他中间件包装的可调用对象。中间件可以层层叠加,形成一个“中间件堆栈”,最后才会调用到实际的可调用对象

使用类定义的中间件必须实现call方法,接收 environ 和 start_response 对象作为参数,最后调用传入的可调用对象,并传递这两个参数

因为 Flask 中实际的WSGI可调用对象是 Flask.wsgi_app()方法,因此,如果我们自己实现了中间件,那么最佳的方式是嵌套在这个wsgi_app对象上

  1. class MyMiddleware():
  2. pass
  3. app = Flask(__name__)
  4. app.wsgi_app = MyMiddleware(app.wsgi_app)

参考资料

WSGI接口
Flask Web 开发实战