如何编写Python Web框架(四)

本文自学用
原文链接: 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()方法。然而,我们的框架不支持静态文件,因此您将很难用CSSJavaScript设计模板。不要难过,因为这正是我们接下来要做的。

静态文件支持

没有好的CSSJavaScript,模板就不是真正的模板。那么让我们来添加对这些文件的支持。

就像我们使用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))

因此,当一个请求进来时,它首先进入FirstMiddlewareFirstMiddleware修改请求并将其发送到SecondMiddleware。现在,SecondMiddleware修改请求并将其发送到our_wsgi_app。我们的应用程序(our_wsgi_app)处理请求,准备响应并将其发送回SecondMiddleware。如果需要,SecondMiddleware可以修改响应并将其发送回FirstMiddlewareFirstMiddleware再修改响应并将其发送回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子系统下实现

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • 谈论WEB编程的时候常说天天在写CGI,那么CGI是什么呢?可能很多时候并不会去深究这些基础概念,再比如除了CGI...
    __七把刀__阅读 2,138评论 2 11
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,034评论 1 32
  • 001 今年年初,因为加入了一个小型的读书营以及一个减脂社群,我就觉得自己特别努力,每天都恨不得把24小时掰成48...
    邱小美是Kelly阅读 314评论 3 3
  • 用料 金针菇一把,半个胡萝卜,大蒜,香菜,白糖,生抽,香醋,盐 1,先把金针菇切掉根部,然后用手拨散开 2,再把胡...
    清清美食阅读 260评论 0 0
  • 文/ 小婷半清 凌晨两点,整个城市都归于沉静,连一点喧嚣都没有了。春节期间,北京城变得空旷了许多,霓虹灯还在闪烁,...
    小婷半清阅读 12,203评论 280 429