本文自学用
原文链接: How to write a Python web framework. Part III.
作者: Jahongir Rahmonov
Github仓库: alcazar
前三篇中文翻译
在本系列之前的博客文章中,我们开始编写自己的Python框架并实现以下功能:
- WSGI兼容
- 请求处理程序
- 路由:简单和参数化
- 检查重复的路径
- 基于类的处理程序
- 单元测试
- 测试客户端
- 添加路由的替代方法(如类似
Django
的实现) - 支持模板
在这个部分,我们将添加一些更棒的功能
- 自定义异常处理
- 支持静态文件
- 中间件
自定义异常处理
异常发生是不可避免的,用户可能会做处一些我们无法预料的事情。我们也有可能编写出在某些场合无法工作的代码导致用户访问一个不存在的页面。根据我们现在编写的代码来看,如果发生了一些异常,将会显示的是一条丑陋的 Internal Server Error
信息。事实上我们可以显示一些更加美观的信息。类似于 Oops! Something went wrong.
或者Please, contact our customer support.
他看起来是这样的:
# 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
如果现在有一个请求处理程序抛出异常,上面的自定义异常处理程序将会被调用
# app.py
@app.route("/home")
def exception_throwing_handler(request, response):
raise AssertionError("This handler should not be user")
如果我们访问http://localhost:8000/home
,应该会看到我们自定义的信息 Oops! Something went wrong. Please, contact our customer support at +1-202-555-0127.
而不是我们之前见到的那个又大又丑的Internal Server Error
。这样就美观多了,让我们继续来实现它
首先我们需要在API类里创建一个变量用于存储异常处理程序:
# api.py
class API:
def __init__(self, templates_dir="templates"):
...
self.exception_handler = None
现在我们需要添加add_exception_handler
方法
# api.py
class API:
...
def add_exception_handler(self, exception_handler):
self.exception_handler = exception_handler
注册了自定义异常处理程序后,我们需要在异常发生时调用它。哪里会发生异常?对了,就是调用处理程序的时候。我们在handle_request
方法中调用处理程序。因此,我们需要用try/except
子句包装它,并在except
部分调用我们的自定义异常处理程序:
# 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
作为静态文件服务。安装:
pip install whitenoise
WhiteNoise
非常简单。我们唯一需要做的就是封装我们的WSGI
应用程序,并将静态文件夹路径作为参数给它。在我们这样做之前,让我们回忆我们的__call__
方法是什么样的:
# 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
封装:
# 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
实例:
# 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
成为的框架的入口点:
# api.py
class API:
...
def __call__(self, environ, start_response):
return self.whitenoise(environ, start_response)
一切就绪后,在项目根目录中创建静态文件夹static
,在其中创建main.css
文件,并添加一下内容:
body {
background-color: chocolate;
}
在第三篇博文中,我们创建了templates/index.html
。现在我们可以把我们刚才创建的css文件引入这个模板:
<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身份验证。主要的一点是,这些都不是完全负责响应客户端的。相反,每个中间件都作为管道的一部分以某种方式改变行为,而实际的响应来自管道中其他部分。在我们的示例中,实际上响应客户端的是request handler
。中间件是我们的WSGI
应用程序的包装器,它能够修改请求和响应。
大体来看,代码如下
FirstMiddleware(SecondMiddleware(our_wsgi_app))
因此,当一个请求进来时,它首先进入FirstMiddleware
。FirstMiddleware
修改请求并将其发送到SecondMiddleware
。现在,SecondMiddleware
修改请求并将其发送到our_wsgi_app
。我们的应用程序(our_wsgi_app
)处理请求,准备响应并将其发送回SecondMiddleware
。如果需要,SecondMiddleware
可以修改响应并将其发送回FirstMiddleware
。FirstMiddleware
再修改响应并将其发送回web服务器(例如gunicorn
)。
让我们继续创建一个中间件类,其他中间件将继承它并封装我们的wsgi
应用程序。
首先创建 middleware.py 文件
touch middleware.py
现在我们可以开始编写我们的Middleware class
# middleware.py
class Middleware:
def __init__(self, app):
self.app = app
正如我们上面提到的,它应该封装一个wsgi
应用程序,并且在多个中间件的情况下,该应用程序也可以是另一个中间件。
作为一个基础中间件类,它还应该能够向堆栈中添加另一个中间件:
# middleware.py
class Middleware:
...
def add(self, middleware_cls):
self.app = middleware_cls(self.app)
它只是简单地包装中间件类到当前的应用
它还应该有自己的主要方法,即处理请求和处理响应。目前,他们什么也不会做。继承自该类的子类将实现以下方法:
# middleware.py
class Middleware:
...
def process_request(self, req):
pass
def process_response(self, req, resp):
pass
现在,是最重要的部分,处理传入请求的方法:
# 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
入口点接口:
# 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
函数
实现了中间件类之后,让我们将它添加到我们的主API类
中:
# api.py
...
from middleware import Middleware
class API:
def __init__(self, templates_dir="templates", static_dir="static"):
...
self.middleware = Middleware(self)
它包装的self
就是我们的wsgi
应用,现在,让我们来使它能够添加中间件:
# api.py
class API:
...
def add_middleware(self, middleware_cls):
self.middleware.add(middleware_cls)
剩下唯一要做的就是在入口点调用这个中间件,而不是我们自己的wsgi
应用:
# api.py
class API:
...
def __call__(self, environ, start_response):
return self.middleware(environ, start_response)
我们现在把成为入口点的工作交给了中间件。请记住,我们在中间件类中实现了成为WSGI
入口点的接口。现在让我们来创建一个只简单地在控制台打印请求地址的中间件:
# 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
并转到任何url
(例如http://localhost:8000/home
)。一切都应该像以前一样。唯一的例外是,这些文本应该出现在控制台中。打开控制台,您应该会看到以下内容:
Processing request http://localhost:8000/home
Processing response http://localhost:8000/home
这里有一个陷阱,你发现了吗?静态文件现在不能工作。原因是我们并没有使用WhiteNoise
,我们没有调用WhiteNoise
,而是调用了中间件。我们需要区分对静态文件和其他文件的请求。当一个静态文件的请求传入时,我们应该调用WhiteNoise
。对于其他请求,我们应该调用中间件。问题是我们如何区分它们。现在,像http://localhost:8000/main.css
这种对静态文件的请求以及其他类似于http://localhost:8000/home
的请求。对于我们的API类
,它们看起来是一样的。因此,我们将向静态文件的url
添加一个根路径,使它们看起来像http://localhost:8000/static/main.css
。我们将检查请求路径是否以/static
开始。如果是以/static
开始,我们将调用WhiteNoise
,否则我们将调用中间件。我们还应该确保去掉路径中的/static
部分否则WhiteNoise
将无法找到这些文件(对下面这段代码倒数第三句的解释):
# 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)
现在,在模板中,我们应该像这样调用静态文件:
<link href="/static/main.css" type="text/css" rel="stylesheet">
继续修改你的index.html
文件。
重启gunicorn
,检查一切正常。
我们将在以后的文章中使用这个中间件特性为我们的应用程序添加身份验证。 我认为这个中间件部分比其他部分更难理解。我也认为我没有很好地解释它。因此,请编写代码,以便更深入理解它,如果有什么不清楚的地方,请在评论中向我提问。
稍微提醒一下,这个系列是基于我为学习目的而编写的Alcazar框架。如果你喜欢这个系列,请在这儿查看博客中的内容,一定要通过star该repo来表达你的喜爱。
Fight on!
注:该系列博文可在win10的Linux子系统下实现
网友评论