Werkzeug 教程 — Werkzeug 文档

来自菜鸟教程
Werkzeug/docs/2.0.x/tutorial
跳转至:导航、​搜索

Werkzeug 教程

欢迎来到 Werkzeug 教程,我们将在其中创建一个 TinyURL 克隆,将 URL 存储在 redis 实例中。 我们将用于此应用程序的库是 Jinja 2 用于模板,redis 用于数据库层,当然还有用于 WSGI 层的 Werkzeug。

您可以使用 pip 来安装所需的库:

pip install Jinja2 redis Werkzeug

还要确保在您的本地机器上运行了一个 redis 服务器。 如果你使用的是 OS X,你可以使用 brew 来安装它:

brew install redis

如果您使用的是 Ubuntu 或 Debian,则可以使用 apt-get:

sudo apt-get install redis-server

Redis 是为 UNIX 系统开发的,从来没有真正设计为在 Windows 上工作。 然而,出于开发目的,非官方端口运行良好。 你可以从 github 获取它们。

即将推出

在本教程中,我们将与 Werkzeug 一起创建一个简单的 URL 缩短器服务。 请记住,Werkzeug 不是一个框架,它是一个带有实用程序的库,用于创建您自己的框架或应用程序,因此非常灵活。 我们在这里使用的方法只是您可以使用的众多方法之一。

作为数据存储,我们将在这里使用 redis 而不是关系数据库来保持简单,因为这是 redis 擅长的工作。

最终结果将如下所示:

a screenshot of shortly

第 0 步:基本的 WSGI 介绍

Werkzeug 是 WSGI 的实用程序库。 WSGI 本身是一种协议或约定,可确保您的 Web 应用程序可以与 Web 服务器通信,更重要的是 Web 应用程序可以很好地协同工作。

在没有 Werkzeug 帮助的情况下,WSGI 中的基本“Hello World”应用程序如下所示:

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return ['Hello World!']

WSGI 应用程序是您可以调用并传递环境字典和 start_response 可调用的东西。 环境包含所有传入信息,start_response 函数可用于指示响应的开始。 使用 Werkzeug,您不必直接处理它们,因为提供了请求和响应对象来处理它们。

请求数据采用环境对象,并允许您以一种很好的方式访问该环境中的数据。 响应对象本身就是一个 WSGI 应用程序,它提供了一种更好的方式来创建响应。

以下是您将如何使用响应对象编写该应用程序:

from werkzeug.wrappers import Response

def application(environ, start_response):
    response = Response('Hello World!', mimetype='text/plain')
    return response(environ, start_response)

这里有一个扩展版本,它查看 URL 中的查询字符串(更重要的是在 URL 中的 name 参数中将“World”替换为另一个词):

from werkzeug.wrappers import Request, Response

def application(environ, start_response):
    request = Request(environ)
    text = f"Hello {request.args.get('name', 'World')}!"
    response = Response(text, mimetype='text/plain')
    return response(environ, start_response)

这就是您需要了解的有关 WSGI 的全部信息。


步骤 1:创建文件夹

在开始之前,让我们创建此应用程序所需的文件夹:

/shortly
    /static
    /templates

short 文件夹不是 python 包,而是我们放置文件的地方。 直接进入这个文件夹,然后我们将在以下步骤中放置我们的主模块。 应用程序的用户可以通过 HTTP 访问静态文件夹中的文件。 这是存放 CSS 和 JavaScript 文件的地方。 在模板文件夹中,我们将使 Jinja2 查找模板。 您稍后在本教程中创建的模板将位于此目录中。


第 2 步:基础结构

现在让我们进入它并为我们的应用程序创建一个模块。 让我们在 shortly 文件夹中创建一个名为 shortly.py 的文件。 首先,我们需要一堆导入。 我将在此处引入所有导入,即使它们没有立即使用,以防止混淆:

import os
import redis
from werkzeug.urls import url_parse
from werkzeug.wrappers import Request, Response
from werkzeug.routing import Map, Rule
from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.middleware.shared_data import SharedDataMiddleware
from werkzeug.utils import redirect
from jinja2 import Environment, FileSystemLoader

然后我们可以为我们的应用程序创建基本结构和一个函数来创建它的新实例,可以选择使用一个 WSGI 中间件来导出 Web 上 static 文件夹中的所有文件:

class Shortly(object):

    def __init__(self, config):
        self.redis = redis.Redis(
            config['redis_host'], config['redis_port'], decode_responses=True
        )

    def dispatch_request(self, request):
        return Response('Hello World!')

    def wsgi_app(self, environ, start_response):
        request = Request(environ)
        response = self.dispatch_request(request)
        return response(environ, start_response)

    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)


def create_app(redis_host='localhost', redis_port=6379, with_static=True):
    app = Shortly({
        'redis_host':       redis_host,
        'redis_port':       redis_port
    })
    if with_static:
        app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
            '/static':  os.path.join(os.path.dirname(__file__), 'static')
        })
    return app

最后,我们可以添加一段代码,该代码将启动具有自动代码重新加载和调试器的本地开发服务器:

if __name__ == '__main__':
    from werkzeug.serving import run_simple
    app = create_app()
    run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True)

这里的基本思想是我们的 Shortly 类是一个实际的 WSGI 应用程序。 __call__ 方法直接调度到 wsgi_app。 这样做是为了我们可以像在 create_app 函数中一样包装 wsgi_app 来应用中间件。 实际的 wsgi_app 方法然后创建一个 Request 对象并调用 dispatch_request 方法,然后该方法必须返回一个 Response 对象,然后再次将其评估为 WSGI 应用程序. 如您所见:海龟一路向下。 我们创建的 Shortly 类以及 Werkzeug 中的任何请求对象都实现了 WSGI 接口。 因此,您甚至可以从 dispatch_request 方法返回另一个 WSGI 应用程序。

create_app 工厂函数可用于创建我们应用程序的新实例。 它不仅会将一些参数作为配置传递给应用程序,而且还可以选择添加一个导出静态文件的 WSGI 中间件。 这样我们就可以访问静态文件夹中的文件,即使我们没有配置我们的服务器来提供它们,这对开发非常有帮助。


间奏曲:运行应用程序

现在您应该能够使用 python 执行该文件并在您的本地机器上看到一个服务器:

$ python shortly.py
 * Running on http://127.0.0.1:5000/
 * Restarting with reloader: stat() polling

它还告诉您重新加载器处于活动状态。 它将使用各种技术来确定磁盘上是否有任何文件更改,然后自动重新启动。

只需转到 URL,您应该会看到“Hello World!”。


第 3 步:环境

现在我们有了基本的应用程序类,我们可以让构造函数做一些有用的事情,并在那里提供一些可以派上用场的助手。 我们需要能够渲染模板并连接到 redis,所以让我们稍微扩展一下这个类:

def __init__(self, config):
    self.redis = redis.Redis(config['redis_host'], config['redis_port'])
    template_path = os.path.join(os.path.dirname(__file__), 'templates')
    self.jinja_env = Environment(loader=FileSystemLoader(template_path),
                                 autoescape=True)

def render_template(self, template_name, **context):
    t = self.jinja_env.get_template(template_name)
    return Response(t.render(context), mimetype='text/html')

第 4 步:路由

接下来是路由。 路由是将 URL 匹配和解析为我们可以使用的内容的过程。 Werkzeug 提供了一个灵活的集成路由系统,我们可以使用它。 它的工作方式是创建一个 Map 实例并添加一堆 Rule 对象。 每个规则都有一个模式,它将尝试匹配 URL 和一个“端点”。 端点通常是一个字符串,可用于唯一标识 URL。 我们也可以使用它来自动反转 URL,但这不是我们在本教程中要做的。

只需将其放入构造函数中:

self.url_map = Map([
    Rule('/', endpoint='new_url'),
    Rule('/<short_id>', endpoint='follow_short_link'),
    Rule('/<short_id>+', endpoint='short_link_details')
])

在这里,我们创建了一个包含三个规则的 URL 映射。 / 用于 URL 空间的根,我们将在其中调度一个函数,该函数实现创建新 URL 的逻辑。 然后一个跟随目标 URL 的短链接,另一个具有相同规则但在末尾加号 (+) 以显示链接详细信息。

那么我们如何找到从端点到函数的方法呢? 随你(由你决定。 我们将在本教程中执行此操作的方法是在类本身上调用方法 on_ + 端点。 这是它的工作原理:

def dispatch_request(self, request):
    adapter = self.url_map.bind_to_environ(request.environ)
    try:
        endpoint, values = adapter.match()
        return getattr(self, f'on_{endpoint}')(request, **values)
    except HTTPException as e:
        return e

我们将 URL 映射绑定到当前环境并返回一个 URLAdapter。 适配器可用于匹配请求,也可用于反转 URL。 match 方法将返回端点和 URL 中的值字典。 例如,follow_short_link 的规则有一个变量部分叫做 short_id。 当我们转到 http://localhost:5000/foo 时,我们将得到以下值:

endpoint = 'follow_short_link'
values = {'short_id': 'foo'}

如果它不匹配任何内容,则会引发 NotFound 异常,即 HTTPException。 所有 HTTP 异常本身也是呈现默认错误页面的 WSGI 应用程序。 所以我们只是将它们全部捕获并返回错误本身。

如果一切正常,我们调用函数 on_ + 端点并将请求作为参数以及所有 URL 参数作为关键字参数传递给它,并返回该方法返回的响应对象。


第 5 步:第一个视图

让我们从第一个视图开始:新 URL 的视图:

def on_new_url(self, request):
    error = None
    url = ''
    if request.method == 'POST':
        url = request.form['url']
        if not is_valid_url(url):
            error = 'Please enter a valid URL'
        else:
            short_id = self.insert_url(url)
            return redirect(f"/{short_id}+")
    return self.render_template('new_url.html', error=error, url=url)

这个逻辑应该很容易理解。 基本上,我们正在检查请求方法是否为 POST,在这种情况下,我们验证 URL 并向数据库添加一个新条目,然后重定向到详细信息页面。 这意味着我们需要编写一个函数和一个辅助方法。 对于 URL 验证,这已经足够了:

def is_valid_url(url):
    parts = url_parse(url)
    return parts.scheme in ('http', 'https')

为了插入 URL,我们只需要类中的这个小方法:

def insert_url(self, url):
    short_id = self.redis.get(f'reverse-url:{url}')
    if short_id is not None:
        return short_id
    url_num = self.redis.incr('last-url-id')
    short_id = base36_encode(url_num)
    self.redis.set(f'url-target:{short_id}', url)
    self.redis.set(f'reverse-url:{url}', short_id)
    return short_id

reverse-url: + URL 将存储短 ID。 如果 URL 已经提交,这不会是 None,我们可以只返回该值,即短 ID。 否则,我们增加 last-url-id 键并将其转换为 base36。 然后我们将链接和反向条目存储在redis中。 这里是转换为基数 36 的函数:

def base36_encode(number):
    assert number >= 0, 'positive integer required'
    if number == 0:
        return '0'
    base36 = []
    while number != 0:
        number, i = divmod(number, 36)
        base36.append('0123456789abcdefghijklmnopqrstuvwxyz'[i])
    return ''.join(reversed(base36))

所以这个视图缺少的是模板。 我们稍后会创建这个,让我们先编写其他视图,然后一次性完成模板。


第 6 步:重定向视图

重定向视图很简单。 它所要做的就是在 redis 中查找链接并重定向到它。 此外,我们还将增加一个计数器,以便我们知道点击链接的频率:

def on_follow_short_link(self, request, short_id):
    link_target = self.redis.get(f'url-target:{short_id}')
    if link_target is None:
        raise NotFound()
    self.redis.incr(f'click-count:{short_id}')
    return redirect(link_target)

在这种情况下,如果 URL 不存在,我们将手动引发 NotFound 异常,这将冒泡到 dispatch_request 函数并转换为默认的 404 响应。


第 7 步:详细视图

链接详细视图非常相似,我们只是再次渲染一个模板。 除了查找目标之外,我们还向 redis 询问链接被点击的次数,如果这样的键尚不存在,则让它默认为零:

def on_short_link_details(self, request, short_id):
    link_target = self.redis.get(f'url-target:{short_id}')
    if link_target is None:
        raise NotFound()
    click_count = int(self.redis.get(f'click-count:{short_id}') or 0)
    return self.render_template('short_link_details.html',
        link_target=link_target,
        short_id=short_id,
        click_count=click_count
    )

请注意,redis 总是处理字符串,因此您必须手动将点击次数转换为 int


第 8 步:模板

这是所有模板。 只需将它们放入 templates 文件夹中。 Jinja2 支持模板继承,所以我们要做的第一件事是创建一个布局模板,其中包含充当占位符的块。 我们还设置了 Jinja2 以便它自动转义带有 HTML 规则的字符串,因此我们不必自己花时间在上面。 这可以防止 XSS 攻击和渲染错误。

layout.html

<!doctype html>
<title>{% block title %}{% endblock %} | shortly</title>
<link rel=stylesheet href=/static/style.css type=text/css>
<div class=box>
  <h1><a href=/>shortly</a></h1>
  <p class=tagline>Shortly is a URL shortener written with Werkzeug
  {% block body %}{% endblock %}
</div>

new_url.html

{% extends "layout.html" %}
{% block title %}Create New Short URL{% endblock %}
{% block body %}
  <h2>Submit URL</h2>
  <form action="" method=post>
    {% if error %}
      <p class=error><strong>Error:</strong> {{ error }}
    {% endif %}
    <p>URL:
      <input type=text name=url value="{{ url }}" class=urlinput>
      <input type=submit value="Shorten">
  </form>
{% endblock %}

short_link_details.html

{% extends "layout.html" %}
{% block title %}Details about /{{ short_id }}{% endblock %}
{% block body %}
  <h2><a href="/{{ short_id }}">/{{ short_id }}</a></h2>
  <dl>
    <dt>Full link
    <dd class=link><div>{{ link_target }}</div>
    <dt>Click count:
    <dd>{{ click_count }}
  </dl>
{% endblock %}

第 9 步:风格

为了让它看起来比丑陋的黑白更好,这里有一个简单的样式表:

静态/style.css

body        { background: #E8EFF0; margin: 0; padding: 0; }
body, input { font-family: 'Helvetica Neue', Arial,
              sans-serif; font-weight: 300; font-size: 18px; }
.box        { width: 500px; margin: 60px auto; padding: 20px;
              background: white; box-shadow: 0 1px 4px #BED1D4;
              border-radius: 2px; }
a           { color: #11557C; }
h1, h2      { margin: 0; color: #11557C; }
h1 a        { text-decoration: none; }
h2          { font-weight: normal; font-size: 24px; }
.tagline    { color: #888; font-style: italic; margin: 0 0 20px 0; }
.link div   { overflow: auto; font-size: 0.8em; white-space: pre;
              padding: 4px 10px; margin: 5px 0; background: #E5EAF1; }
dt          { font-weight: normal; }
.error      { background: #E8EFF0; padding: 3px 8px; color: #11557C;
              font-size: 0.9em; border-radius: 2px; }
.urlinput   { width: 300px; }

奖励:改进

查看 Werkzeug 存储库中示例字典中的实现,以查看本教程的一个版本,其中包含一些小的改进,例如自定义 404 页面。