「译文」如何编写 Python-Web 框架(四)

本文最后更新于:2021年11月27日 下午

本文为译文

原文链接: How to write a Python web framework. Part IV.

作者: Jahongir Rahmonov

Github 仓库: alcazar

查看第一部分, 「译文」如何编写 Python Web 框架(一)
查看第二部分,「译文」如何编写 Python Web 框架(二)
查看第三部分,「译文」如何编写 Python Web 框架(三)

提示:

这个系列是基于alcazar 框架, 我写它是为了学习的目的。如果你喜欢这个系列,通过 star 该 repo 来表达你的喜爱。

在之前的博客文章系列中,我们开始编写我们自己的 Python 框架,并实现了以下功能:

  • WSGI 兼容性
  • 请求处理程序
  • 路由:简单且参数化
  • 检查重复 path
  • 基于类的处理程序
  • 单元测试
  • 测试客户端
  • 添加 path 的替代方法(类似 Django)
  • 模板支持

在这部分,我们将添加一些更真棒功能到列表中:

  • 自定义异常处理程序
  • 静态文件的支持
  • 中间件

自定义异常处理程序

异常情况不可避免地会发生。用户可能会做一些我们意想不到的事情。我们可以编写一些在某些情况下不起作用的代码。用户可以转到不存在的页面。以我们现在所拥有的,如果出现一些异常,我们显示一个很大的丑陋的 Internal Server Error 信息。取而代之,我们可以展示一些友好的信息。如 Oops! Something went wrong.Please, contact our customer support . 为此,我们需要能够捕获这些异常,并根据我们的需求处理它们。

看起来是这样的:

1
2
3
4
5
6
7
8
9
# app.py
from api import API

app = API()

def custom_exception_handler(request, response, exception_cls):
response.text = "Oops! Something went wrong. Please, contact our customer support at +1-202-555-0127."

app.add_exception_handler(custom_exception_handler)

在这里,我们创建一个自定义异常处理程序。它看起来像我们简单的请求处理程序,除了它有作为它的第三个参数 exception_cls。现在,如果我们有一个抛出异常的请求处理程序,那么应该调用上述的自定义异常处理程序。

1
2
3
4
5
# app.py

@app.route("/home")
def exception_throwing_handler(request, response):
raise AssertionError("This handler should not be user")

如果我们访问http://localhost:8000/home,而不是之前的 Internal Server Error,我们应该能够看到我们的自定义消息 Oops! Something went wrong. Please, contact our customer support at +1-202-555-0127.。它看起来够好吗? 让我们继续并实现它。

我们首先需要的是在主 API 类中存储异常处理程序的一个变量:

1
2
3
4
5
6
# api.py

class API:
def __init__(self, templates_dir="templates"):
...
self.exception_handler = None

现在我们需要添加方法add_exception_handler

1
2
3
4
5
6
7
# api.py

class API:
...

def add_exception_handler(self, exception_handler):
self.exception_handler = exception_handler

注册了自定义异常处理程序后,我们需要在异常发生时调用。异常在哪里发生?没错:当处理程序 handle_request 被调用时。我们调用我们方法内的处理程序。因此,我们需要用 try/except 语句包装它,并调用我们的自定义异常处理器的 except 部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# api.py

class API:
...

def handle_request(self, request):
response = Response()

handler, kwargs = self.find_handler(request_path=request.path)

try:
if handler is not None:
if inspect.isclass(handler):
handler = getattr(handler(), request.method.lower(), None)
if handler is None:
raise AttributeError("Method now allowed", request.method)

handler(request, response, **kwargs)
else:
self.default_response(response)
except Exception as e:
if self.exception_handler is None:
raise e
else:
self.exception_handler(request, response, e)

return response

我们还需要确保,如果没有注册异常处理程序,则会传播异常。

我们已把所有东西都安排好。继续前进,重新启动你的 gunicorn,访问 http://localhost:8000/home。你应该看到我们的小可爱的消息,而不是大丑陋的默认之一。当然,请确保您的 app.py 有上述异常处理程序和错误的请求处理程序。

如果您想更进一步,请创建一个不错的模板,并在异常处理器内使用我们的方法api.template()。但是,我们的框架不支持静态文件,因此您将很难用 CSS 和 JavaScript 设计模板。不要伤心,因为这正是我们下一步要做的。

静态文件的支持

没有好的 CSS 和 JavaScript 的模板就不是真正的模板,不是吗? 那么我们是否需要添加对这些文件的支持呢?

就像我们使用 Jinja2 进行模板支持一样,我们将使用 WhiteNoise 用于静态文件服务。安装它:

1
pip install whitenoise

WhiteNoise 很简单。我们唯一需要做的是包装我们的 WSGI 应用程序,并给它静态文件夹路径作为参数。在我们这样做之前,让我们记住我们的 __call__ 方法是什么样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
# api.py

class API:
...

def __call__(self, environ, start_response):
request = Request(environ)

response = self.handle_request(request)

return response(environ, start_response)

...

这基本上是我们的 WSGI 应用程序的入口点, 这正是我们需要用 WhiteNoise 包装的。因此,让我们将其内容重构为单独的方法,以便更容易用 WhiteNoise 包装它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# api.py

class API:
...

def wsgi_app(self, environ, start_response):
request = Request(environ)

response = self.handle_request(request)

return response(environ, start_response)

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

现在,在我们的构造器中,我们可以初始化 WhiteNoise 实例:

1
2
3
4
5
6
7
8
9
10
11
12
# api.py
...
from whitenoise import WhiteNoise


class API:
...
def __init__(self, templates_dir="templates", static_dir="static"):
self.routes = {}
self.templates_env = Environment(loader=FileSystemLoader(templates_dir))
self.exception_handler = None
self.whitenoise = WhiteNoise(self.wsgi_app, root=static_dir)

正如你所看到的,我们用 WhiteNoise 包裹了我们的 wsgi_app,并给它一个路径静态文件夹作为第二个参数。唯一需要做的事情就是将此self.whitenoise 作为我们框架的切入点:

1
2
3
4
5
6
# api.py

class API:
...
def __call__(self, environ, start_response):
return self.whitenoise(environ, start_response)

在一切到位后,在项目根部创建文件夹static,在其中创建文件main.css,并将以下内容放入其中:

1
2
3
body {
background-color: chocolate;
}

「译文」如何编写 Python Web 框架(三) 中,我们创建了templates/index.html。现在,我们可以将新创建的 css 文件放入此模板中:

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<header>
<title>{{ title }}</title>

<link href="/main.css" type="text/css" rel="stylesheet">
</header>

<body>
<h1>The name of the framework is {{ name }}</h1>
</body>

</html>

重新启动你的 gunicorn,访问 http://localhost/template。你应该看到,整个背景的颜色是巧克力色,而不是白色,这意味着我们的静态文件正在被送达。棒!

中间件

如果您需要回顾一下中间件是什么以及它们的工作原理,请先阅读 这篇文章。否则,这部分可能看起来有点混乱。我将等着你们。回来了吗? 太好了。我们继续。

你知道他们是什么,他们是如何工作的,但你可能想知道他们被用来做什么。基本上,中间件是一个可以修改 HTTP 请求和 / 或响应的组件,它被设计成链接在一起,在请求处理期间形成行为更改的管道。中间件任务的示例可以请求日志记录和 HTTP 身份验证。关键是,这些都不完全负责对客户做出回应。相反,每个中间件都以某种方式更改作为管道的一部分的行为,让实际的响应来自管道后面的东西。在我们的情况下,真正响应客户端的是我们的请求处理程序。中间件是围绕我们的 WSGI 应用程序的包装,能够修改请求和响应。

从鸟瞰图看,代码会是这样的:

1
FirstMiddleware(SecondMiddleware(our_wsgi_app))

因此,当请求进来时,它首先进入FirstMiddleware。它修改请求并将其发送到SecondMiddleware。现在,SecondMiddleware 修改请求并将其发送给 our_wsgi_app。我们的应用程序处理请求,准备响应并将其发送回SecondMiddleware。如果它想,它可以修改响应,并将其发送回 FirstMiddleware。它修改了响应并将其发送回 Web 服务器(例如 gunicorn)。

让我们继续前进,创建一个类 Middleware,其他中间件将继承,并包裹我们的 wsgi 应用程序。

首先创建文件 middleware.py

1
touch middleware.py

现在,我们可以开始我们的 Middleware class

1
2
3
4
5
# middleware.py

class Middleware:
def __init__(self, app):
self.app = app

正如我们上面提到的,它应该包装一个 wsgi 应用程序 ,并在多个中间件的情况下,app也可以是另一个中间件。

作为基础中间件类,它也应该能够将另一个中间件添加到堆栈中:

1
2
3
4
5
6
7
# middleware.py

class Middleware:
...

def add(self, middleware_cls):
self.app = middleware_cls(self.app)

它只是将给定的中间件类包裹在我们当前的应用程序上。

它还应有其主要方法,即请求处理和响应处理。现在,他们什么也不做。继承自这个类的子类将实现这些方法:

1
2
3
4
5
6
7
8
9
10
# middleware.py

class Middleware:
...

def process_request(self, req):
pass

def process_response(self, req, resp):
pass

现在,最重要的部分,处理传入请求的方法:

1
2
3
4
5
6
7
8
9
10
11
# middleware.py

class Middleware:
...

def handle_request(self, request):
self.process_request(request)
response = self.app.handle_request(request)
self.process_response(request, response)

return response

它首先调用 self.process_request 对请求做一些事情。然后将响应创建委托给它正在包装的应用程序。最后,它调用 process_response 来处理响应对象。然后简单地向上返回响应。

由于中间件现在是我们应用程序的第一个入口点,它们是由我们的 Web 服务器(如 gunicorn)调用的。因此,中间件应实现 WSGI 入口点接口:

1
2
3
4
5
6
7
8
9
# middleware.py
from webob import Request

class Middleware:

def __call__(self, environ, start_response):
request = Request(environ)
response = self.app.handle_request(request)
return response(environ, start_response)

这只是我们上面创建的 wsgi_app 函数的副本。

随着我们的中间件类的实现,让我们将其添加到我们的 main API类:

1
2
3
4
5
6
7
8
9
# api.py
...
from middleware import Middleware


class API:
def __init__(self, templates_dir="templates", static_dir="static"):
...
self.middleware = Middleware(self)

它环绕着我们的 wsgi 应用程序的self。现在,让我们给它添加中间件的能力:

1
2
3
4
5
6
7
# api.py

class API:
...

def add_middleware(self, middleware_cls):
self.middleware.add(middleware_cls)

唯一要做的就是在入口点调用此中间件,而不是我们自己的 wsgi 应用程序:

1
2
3
4
5
6
7
# api.py

class API:
...

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

你为什么这么问? 因为我们现在正在将作为中间件入口点的工作委托给中间件。记住,我们在 Middleware 类中实现了 WSGI 入口点接口。现在让我们来创建一个简单的中间件,它可以简单地打印到控制台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# app.py
from api import API
from middleware import Middleware

app = API()

...

class SimpleCustomMiddleware(Middleware):
def process_request(self, req):
print("Processing request", req.url)

def process_response(self, req, res):
print("Processing response", req.url)

app.add_middleware(SimpleCustomMiddleware)

...

重新启动你的 gunicorn,去访问任何网址(例如http://localhost:8000/home)。一切都应该像以前一样工作。唯一的区别是以下文本应该显示在控制台中。打开主机,您应该看到以下情况:

1
2
Processing request http://localhost:8000/home
Processing response http://localhost:8000/home

这里有一个陷阱。你找到了吗?静态文件现在不起作用。原因是我们停止使用WhiteNoise。我们删除了它。我们不是调用WhiteNoise,而是调用中间件。以下我们应该做的。我们需要区分静态文件请求和其他请求。当请求进入静态文件时,我们应该调用WhiteNoise。对于其他,我们应该调用中间件。问题是我们如何区分它们。现在,静态文件请求看起来像这样http://localhost:8000/main.css: . 其他请求看起来是这样的http://localhost:8000/home。对于我们 class API 来说,它们看起来是一样的。因此,我们将添加一个根到静态文件的网址,使他们看起来像这样http://localhost:8000/static/main.css。我们将检查请求路径是否以/static 开头。如果是这样,我们将调用WhiteNoise,否则我们将调用中间件。我们还应该确保剪切 /static 部分。 否则 WhiteNoise 将找不到文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
# api.py

class API:
...

def __call__(self, environ, start_response):
path_info = environ["PATH_INFO"]

if path_info.startswith("/static"):
environ["PATH_INFO"] = path_info[len("/static"):]
return self.whitenoise(environ, start_response)

return self.middleware(environ, start_response)

现在,在模板中,我们应该这样调用静态文件:

1
<link href="/static/main.css" type="text/css" rel="stylesheet">

去改变你的 index.html

重新启动你的 gunicorn,并检查一切是否正常工作。

我们将在以后的帖子中使用此中间件功能来将身份验证添加到我们的应用中。

我认为这个中间件部分比其他人更难理解。我也认为我没有做好解释它的工作。因此,请编写代码,让它深入理解,如果有什么不清楚的地方可以在评论中问我。

查看第一部分, 「译文」如何编写 Python Web 框架(一)
查看第二部分,「译文」如何编写 Python Web 框架(二)
查看第三部分,「译文」如何编写 Python Web 框架(三)

提示:

这个系列是基于alcazar 框架, 我写它是为了学习的目的。如果你喜欢这个系列,通过 star 该 repo 来表达你的喜爱。

这就是我今天要说的.

Fight on!


「译文」如何编写 Python-Web 框架(四)
https://ewhisper.cn/posts/63000/
作者
东风微鸣
发布于
2019年3月26日
许可协议