器→工具, 开源项目

Flask学习之Werkzeug

钱魏Way · · 9 次浏览

Werkzeug简介

Flask 和 Werkzeug 之间的关系可以理解为 Flask 是建立在 Werkzeug 基础之上的一个 Web 框架。要了解它们之间的关系,我们需要先分别理解 Flask 和 Werkzeug 的角色和功能。

Werkzeug

Werkzeug 是一个 WSGI(Web Server Gateway Interface)工具库,它为 Python Web 应用提供了一系列实用的功能和组件。Werkzeug 的主要职责包括:

  • 处理 HTTP 请求和响应。
  • 提供路由系统。
  • 实现请求和响应对象。
  • 提供实用的调试和错误处理工具。

总的来说,Werkzeug 更像是一个底层的工具库,为构建 Web 应用提供了基础的构建块和工具。

Flask

Flask 是一个轻量级的 Web 框架,它使用 Werkzeug 作为其 WSGI 层和路由系统的基础。Flask 在 Werkzeug 提供的基础功能之上添加了:

  • 更简洁易用的 API。
  • 模板渲染机制(使用 Jinja2)。
  • 支持插件和扩展。
  • 更多的抽象层,使得开发 Web 应用更为方便。

可以这样理解,Werkzeug 是提供基础 Web 功能的工具集,而 Flask 则是在这个工具集的基础上构建的,提供了更完整的 Web 框架体验。在 Flask 应用中,当处理路由、请求和响应等核心 Web 功能时,实际上是在使用 Werkzeug 的功能。换句话说,Werkzeug 为 Flask 提供了底层的 Web 功能支持,而 Flask 则在此之上提供了更高层次的抽象和便利。

通过这种关系,Flask 成为了一个易于上手但功能强大的 Web 框架,适合于快速开发小到中型的 Web 应用,同时保持足够的灵活性和可扩展性。

为了更加深入的了解Flask和Werkzeug的关系,我们带来使用Werkzeug搭建一个应用来学习。

Hello World App

准备工作

创建一个新的项目

$ mkdir werkzeug_movie_app
$ cd werkzeug_movie_app
$ python3 -m venv venv
$ source venv/bin/activate
(venv)$

安装 Werkzeug, Jinja,和 redis-py:

(venv)$ pip install Werkzeug Jinja2 redis
(venv)$ pip freeze > requirements.txt

Redis 会被用来存储电影数据。

应用开发

Werkzeug 是用于构建 WSGI 兼容 Web 应用程序的库集合。它不提供高级类(如 Flask)来构建完整的 Web 应用程序。相反,您需要自己从 Werkzeug 的库创建应用程序。

在项目的顶级文件夹中创建一个新的 app.py 文件:

from werkzeug.wrappers import Request, Response


class MovieApp(object):
    """Implements a WSGI application for managing your favorite movies."""
    def __init__(self):
        pass

    def dispatch_request(self, request):
        """Dispatches the request."""
        return Response('Hello World!')

    def wsgi_app(self, environ, start_response):
        """WSGI application that processes requests and returns responses."""
        request = Request(environ)
        response = self.dispatch_request(request)
        return response(environ, start_response)

    def __call__(self, environ, start_response):
        """The WSGI server calls this method as the WSGI application."""
        return self.wsgi_app(environ, start_response)


def create_app():
    """Application factory function that returns an instance of MovieApp."""
    app = MovieApp()
    return app

MovieApp 类实现一个与 WSGI 兼容的 Web 应用程序,该应用程序处理来自不同用户的请求并生成返回给用户的响应。以下是此类与 WSGI 服务器交互的流程:

收到请求后,系统会在 wsgi_app(): 中对其进行处理:

def wsgi_app(self, environ, start_response):
    """WSGI application that processes requests and returns responses."""
    request = Request(environ)
    response = self.dispatch_request(request)
    return response(environ, start_response)

环境 (environ) 在 Request 类中自动处理,以创建 request 对象。然后在dispatch_request()中处理 request。对于此初始示例, dispatch_request()返回“Hello World!”响应。然后从wsgi_app()返回响应。

与Flask的比较:MovieApp是 Flask 类的简化版本。在Flask类中,wsgi_app()是与 WSGI 服务器交互的实际 WSGI 应用程序。此外,dispatch_request()和 full_dispatch_request()用于执行请求调度,该调度将 URL 与适用的视图函数匹配并处理异常。

部署服务

将以下代码添加到 app.py 底部以运行 Werkzeug 开发服务器:

if __name__ == '__main__':
    # Run the Werkzeug development server to serve the WSGI application (MovieApp)
    from werkzeug.serving import run_simple
    app = create_app()
    run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True)

运行程序:

(venv)$ python app.py

浏览 http://localhost:5000 可以查看到 “Hello World!”消息。

存储静态文件的中间件

在 Web 应用程序中,中间件是一个软件组件,可以添加到请求/响应处理管道中以执行特定功能。

Web 服务器/应用程序要执行的一项重要功能是提供静态文件(CSS、JavaScript 和图像文件)。Werkzeug 为此功能提供了一个称为 SharedDataMiddleware 的中间件。

SharedDataMiddleware非常适合使用 Werkzeug 开发服务器来提供静态文件。

要利用SharedDataMiddleware,新建一个“static”文件夹,并且在其下再创建 “css”和“img”文件夹:

├── app.py
├── requirements.txt
└── static
    ├── css
    └── img

在“static/img”文件夹中,添加 Flask的logo。将其另存为 flask.png。

接下里,我们应用工厂方法:

import os

from werkzeug.middleware.shared_data import SharedDataMiddleware
from werkzeug.wrappers import Request, Response

def create_app():
    """Application factory function that returns an instance of MovieApp."""
    app = MovieApp()
    app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
        '/static': os.path.join(os.path.dirname(__file__), 'static')
    })
    return app

现在,当 Werkzeug 应用程序处理请求时,它将首先被路由到appSharedDataMiddleware()以确定是否已请求静态文件:

如果请求静态文件,SharedDataMiddlewarewsgi_app()将使用静态文件生成响应。否则,请求将向下传递到 Werkzeug 应用程序进行处理。

若要查看SharedDataMiddlewarewsgi_app()实际操作,请运行服务器并打开 http://localhost:5000/static/img/flask.png 查看 Flask Logo。

有关 Werkzeug 提供的中间件解决方案的完整列表,请查看中间件文档。

与Flask的比较:Flask 不使用SharedDataMiddleware。它采用不同的方法来提供静态文件。默认情况下,如果存在静态文件夹,Flask 会自动添加新的 URL 规则来提供静态文件。

为了说明此概念,请在 Flask 应用程序的项目中运行flask routes:

(venv)$ flask routes

Endpoint     Methods  Rule
-----------  -------  -----------------------
index        GET      /
static       GET      /static/<path:filename>

模板系统

正如在 Flask 项目中通常所做的那样,我们将使用 Jinja 作为应用的模板引擎。

首先新建一个 “templates”文件夹:

├── app.py
├── requirements.txt
├── static
│   ├── css
│   └── img
│       └── flask.png
└── templates

为了利用Jinja,请扩展该MovieApp类的构造函数:

from jinja2 import Environment, FileSystemLoader

def __init__(self):
    """Initializes the Jinja templating engine to render from the 'templates' folder."""
    template_path = os.path.join(os.path.dirname(__file__), 'templates')
    self.jinja_env = Environment(loader=FileSystemLoader(template_path),
                                 autoescape=True)

与Flask的比较:Flask也是利用jinja的Environment 来创建模板引擎。

在MovieApp类中,添加一个新方法: render_template()

def render_template(self, template_name, **context):
    """Renders the specified template file using the Jinja templating engine."""
    template = self.jinja_env.get_template(template_name)
    return Response(template.render(context), mimetype='text/html')

此方法将template_name和变量传递给模板引擎(**context)。然后,它使用 Jinja 的render()方法生成一个Response。

与Flask比较:render_template()是不是非常的熟悉?他是Flask中最常用的功能之一。

若要查看render_template()实际操作,更新dispatch_request()来渲染模板:

def dispatch_request(self, request):
    """Dispatches the request."""
    return self.render_template('base.html')

针对此应用的所有请求将渲染templates/base.html模板:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Werkzeug Movie App</title>

    <!-- CSS file for styling the application -->
    <link rel="stylesheet" href="/static/css/style.css" type="text/css">
</head>
<body>
    <h1>Werkzeug Movie App</h1>
    {% block body %}
    {% endblock %}
</body>
</html>

URL路由

路由主要将将URL与视图函数做匹配,Werkzeug提供了一个Map类来处理。让我们一起来看下是怎么处理的:

from werkzeug.routing import Map, Rule

def __init__(self):
    """Initializes the Jinja templating engine to render from the 'templates' folder."""
    template_path = os.path.join(os.path.dirname(__file__), 'templates')
    self.jinja_env = Environment(loader=FileSystemLoader(template_path),
                                 autoescape=True)
    self.url_map = Map([
        Rule('/', endpoint='index'),
        Rule('/movies', endpoint='movies'),
    ])

每个Rule对象都定义了一个URL和视图函数,如果匹配到URL就调用此函数。例如访问主页(‘/’)时,index函数就会被调用。

与Flask比较:Flask使用的是@route装饰器,其原理就是使用装饰器更新url_map,与上面手动url_map的操作是一样的。

为了应用URL映射,需要更新dispatch_request():

from werkzeug.exceptions import HTTPException

def dispatch_request(self, request):
    """Dispatches the request."""
    adapter = self.url_map.bind_to_environ(request.environ)
    try:
        endpoint, values = adapter.match()
        return getattr(self, endpoint)(request, **values)
    except HTTPException as e:
        return e

现在,当请求进入dispatch_request()时,将进行匹配操作。如果请求的 URL 包含在url_map中,则将调用适用的视图函数。如果在url_map中找不到该 URL,则会引发异常。

我们在url_map中指定了两个视图函数,因此现在让我们在MovieApp类中创建它们:

def index(self, request):
    return self.render_template('base.html')

def movies(self, request):
    return self.render_template('movies.html')

templates/base.html 是在上一节中已经创建的,现在需要创建 templates/movies.html:

{% extends "base.html" %}

{% block body %}
<div class="table-container">
    <table>
        <!-- Table Header -->
        <thead>
            <tr>
                <th>Index</th>
                <th>Movie Title</th>
            </tr>
        </thead>

        <!-- Table Elements (Rows) -->
        <tbody>
            <tr>
                <td>1</td>
                <td>Knives Out</td>
            </tr>
            <tr>
                <td>2</td>
                <td>Pirates of the Caribbean</td>
            </tr>
            <tr>
                <td>3</td>
                <td>Inside Man</td>
            </tr>
        </tbody>
    </table>
</div>
{% endblock %}

此模板文件利用模板继承将 base.html 用作父模板。它生成一个包含三部电影的表。

完成后再打开浏览器查看最终的结果。

异常处理

尝试访问http://localhost:5000/movies2,你会看到一个默认错误页面,原因是无法在url_map中找到对应的视图。

我们来通过扩展dispatch_request()创建一个自定义的错误页面:

from werkzeug.exceptions import HTTPException, NotFound

def dispatch_request(self, request):
    """Dispatches the request."""
    adapter = self.url_map.bind_to_environ(request.environ)
    try:
        endpoint, values = adapter.match()
        return getattr(self, endpoint)(request, **values)
    except NotFound:
        return self.error_404()
    except HTTPException as e:
        return e

现在如果URL在url_map中没有被匹配到,将会调用error_404()。我们在MovieApp类下面创建一个新的方法:

def error_404(self):
    response = self.render_template("404.html")
    response.status_code = 404
    return response

并且创建一个对应的模板templates/404.html:

% extends "base.html" %}

{% block body %}
<div class="error-description">
    <h2>Page Not Found (404)</h2>
    <h4>What you were looking for is just not there!</h4>
    <h4><a href="/">Werkzeug Movie App</a></h4>
</div>
{% endblock %}

错误页面就设置完成了。

与Flask比较:当full_dispatch_request ()在Flask类中检测到异常时,将在handle_user_exceptions()中正常处理。Flask 还允许所有 HTTP 错误代码的自定义错误页面。

请求处理

在本节中,我们将向应用程序添加一个表单,以允许用户输入他们喜欢的电影。

如前所述,我们将使用 Redis来持久化电影,因为它的读/写速度快且易于设置。

启动并运行 Redis 的最快方法是使用 Docker:

$ docker run --name some-redis -d -p 6379:6379 redis

要检查 Redis 容器是否正在运行,请执行以下操作:

$ docker ps

要停止正在运行的 Redis 容器,请执行以下操作:

$ docker stop some-redis  # Use name of Docker container

如果您不是 Docker 用户,请查看以下资源:

为了利用 Redis,首先更新MovieApp构造函数以创建一个StrictRedis实例:

from redis import StrictRedis

def __init__(self, config):  # Updated!!
    """Initializes the Jinja templating engine to render from the 'templates' folder,
    defines the mapping of URLs to view methods, and initializes the Redis interface."""
    template_path = os.path.join(os.path.dirname(__file__), 'templates')
    self.jinja_env = Environment(loader=FileSystemLoader(template_path),
                                 autoescape=True)
    self.url_map = Map([
        Rule('/', endpoint='index'),
        Rule('/movies', endpoint='movies'),
    ])
    self.redis = StrictRedis(config['redis_host'], config['redis_port'],
                             decode_responses=True)  # New!!

此外,构造函数还有一个附加参数(config),用于创建StrictRedis的实例。

传递给构造函数的配置参数需要在应用程序工厂函数中指定:

def create_app():
    """Application factory function that returns an instance of MovieApp."""
    app = MovieApp({'redis_host': 'localhost', 'redis_port': 6379})
    app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
        '/static': os.path.join(os.path.dirname(__file__), 'static')
    })
    return app

表单处理

为了让用户在Redis存储中添加电影,我们需要在url_map中新增一个视图函数:

def __init__(self, config):
    """Initializes the Jinja templating engine to render from the 'templates' folder,
    defines the mapping of URLs to view methods, and initializes the Redis interface."""

    ...

    self.url_map = Map([
        Rule('/', endpoint='index', methods=['GET']),
        Rule('/movies', endpoint='movies', methods=['GET']),
        Rule('/add', endpoint='add_movie', methods=['GET', 'POST']),  # !!!
    ])

    ...

在url_map的Rule规则中我们还扩展了HTTP方法。如果’/add’被GET或POST请求就会调用add_movie()视图函数。

接下来我们需要再MovieApp类中增加add_movie()视图:

from werkzeug.utils import redirect

def add_movie(self, request):
    """Adds a movie to the list of favorite movies."""
    if request.method == 'POST':
        movie_title = request.form['title']
        self.redis.lpush('movies', movie_title)
        return redirect('/movies')
    return self.render_template('add_movie.html')

如果是GET请求,则add_movie()渲染templates/add_movie.html文件,如果是POST请求则将数据存储到Redis中,并且重定向网址到电影列表页。

创建templates/add_movie.html模板文件:

{% extends "base.html" %}

{% block body %}
<div class="form-container">
    <form method="post">
        <div class="field">
            <label for="movieTitle">Movie Title:</label>
            <input type="text" id="movieTitle" name="title"/>
        </div>
        <div class="field">
            <button type="submit">Submit</button>
        </div>
    </form>
</div>
{% endblock %}

显示电影

由于我们现在将电影存储在 Redis 中,因此需要更新movie() 视图函数以从 Redis 中的列表中读取电影列表:

def movies(self, request):
    """Displays the list of favorite movies."""
    movies = self.redis.lrange('movies', 0, -1)
    return self.render_template('movies.html', movies=movies)

电影列表的信息会被传递到templates/movies.html模板文件,我们需要使用循环来读取这些信息:

{% extends "base.html" %}

{% block body %}
<div class="table-container">
    <table>
        <!-- Table Header -->
        <thead>
            <tr>
                <th>Index</th>
                <th>Movie Title</th>
            </tr>
        </thead>

        <!-- Table Elements (Rows) -->
        <tbody>
            {% for movie in movies %}
            <tr>
                <td>{{ loop.index }}</td>
                <td>{{ movie }}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</div>
{% endblock %}

至此,整个App基本上已经开发完毕。

为什么不使用Werkzeug替换掉Flask?

Werkzeug 提供了 Flask 中的许多关键功能,但 Flask 增加了许多强大的功能,例如:

  • Sessions
  • 应用程序和请求上下文
  • 蓝图
  • Request callback functions
  • Utilities:
    • @route 装饰器
    • url_for() 函数
  • CLI 命令
  • 异常处理
  • Test client
  • Flask shell
  • Logging
  • Signals
  • Extensions

与任何 Web 框架一样,不要重新发明轮子!Flask 是基于其丰富的功能集和大量扩展的 Web 开发选择(与 Werkzeug 相比)。

本文通过展示如何使用 Werkzeug 构建一个简单的 Web 应用程序,概述了 Flask 的关键组件之一 Werkzeug。虽然了解底层库在 Flask 中的工作方式很重要,但使用 Werkzeug 创建 Web 应用程序的复杂性应该说明使用 Flask 开发 Web 应用程序是多么容易!

参考链接:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注