器→工具, 开源项目

Flask学习之Werkzeug

钱魏Way · · 145 次浏览
!文章内容如有错误或排版问题,请提交反馈,非常感谢!

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应用程序处理请求时,它将首先被路由到 app SharedDataMiddleware() 以确定是否已请求静态文件:

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

若要查看 SharedDataMiddleware wsgi_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 MovieApp</title>

    <!-- CSS file for styling the application -->
    <link rel="stylesheet" href="/static/css/style.css" type="text/css">
</head>
<body>
    <h1>Werkzeug MovieApp</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>
<!--TableHeader-->
<thead>
<tr>
<th>Index</th>
<th>Movie Title</th>
</tr>
</thead>

<!--TableElements(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应用程序是多么容易!

参考链接:

发表回复

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