造轮子是最好的一种学习方式,本文尝试从0开始造个Python Web框架的轮子,我称它为ToyWebF。

本文操作环境为:MacOS,文中涉及的命令,请根据自己的系统进行替换。

ToyWebF的简单特性:

1.支持多种不同形式的路由注册方式

2.支持静态HTML、CSS、JavaScript

3.支持自定义错误

4.支持中间件

下面我们来实现这些特性。

最简单的web服务

首先,我们需要安装gunicorn,回忆一下Flask框架,该框架有内置的Web服务器,但不稳定,所以上线时通常会替换成uWSGI或gunicorn,这里不搞这个内置Web服务,直接使用gunicorn。

这里多说一句,小编是一名python开发工程师,这里有我自己整理的一套最新的python系统学习教程,包括从基础的python脚本到web开发、爬虫、数据分析、数据可视化、机器学习等。想要这些资料的可以关注小编,并在后台私信小编即可领取。

我们创建新的目录与Python虚拟环境,在该虚拟环境中安装gunicorn

mkdir ToyWebF

python3 -m venv venv # 创建虚拟环境

source venv/bin/activate #激活虚拟环境

pip install gunicorn

复制代码

在啥都没有的情况下,构建最简单的Web服务,在ToyWebF目录下,创建app.py与api.py文件,写入下面代码。

api.py 文件

class API:

def call(self, environ, start_response):

response_body = b"Hello, World!"

status = "200 OK"

start_response(status, headers=[])

return iter([response_body])

app.py 文件

from api import API

app = API()

复制代码

运行gunicorn app:app访问http://127.0.0.1:8000,可以看见Hello, World!,但现在请求体中的参数在environ变量中,难以解析,我们返回的response也是bytes形式。

我们可以使用webob库,将environ中的数据转为Request对象,将需要返回的数据转为Response对象,处理起来更加直观方便,直接通过pip安装一下。

pip install webob

复制代码

然后修改一下API类的__call__方法,代码如下。

from webob import Request, Response

class API(object):

def wsgi_app(self, environ, start_response):

"""通过 webob 将请求的环境信息转为request对象"""

request = Request(environ)

response = self.handle_request(request)

return response(environ, start_response)

def call(self, environ, start_response):

self.wsgi_app(environ, start_response)

复制代码

上述代码中,通过webob库的Request类将environ对象(请求的环境信息)转为容易处理的request,随后调用handle_request方法对request进行处理,处理的结果,通过response对象返回。

handle_request方法在ToyWebF中非常重要,它会匹配出某个路由对应的处理方法,然后调用该方法处理请求并将处理的结果返回,在解析handle_request前,需要先讨论路由注册实现,代码如下。

class API(object):

def init(self):

# url路由

self.routes = {}

def route(self, path):

# 添加路由的装饰器

def wrapper(handler):

self.add_route(path, handler)

return handler

return wrapper

def add_route(self, path, handler):

# 相同路径不可重复添加

assert path not in self.routes, "Such route already exists"

self.routes[path] = handler

复制代码

其实就是将路由和方法存到self.routes字典中,可以通过route装饰器的形式将路由和方法关联,也可以通过add_route方法关联,在app.py中使用一下。

app = API()

通过装饰器关联路由和方法

@app.route("/home")

def home(request, response):

response.text = "This is Home"

路由中可以有变量,对应的方法也需要有对应的参数

@app.route("/hello/{name}")

def hello(requst, response, name):

response.text = f"Hello, {name}"

可以装饰类

@app.route("/book")

class BooksResource(object):

def get(self, req, resp):

resp.text = "Books Page"

def handler1(req, resp):

resp.text = "handler1"

可以直接通过add_route方法添加

app.add_route("/handler1", handler1)

复制代码

因为url中可以存在变量,如@app.route("/hello/{name}"),所以在匹配时,需要进行解析,可以使用正则匹配的方式进行匹配,parse这个第三方库已经帮我们实现了相应的正则匹配逻辑,pip安装使用一下则可。

pip install parse

In [1]: from parse import parse

匹配

In [2]: res = parse("/hello/{name}", "/hello/二两")

In [3]: res.named

Out[3]: {'name': '二两'}

复制代码

这里定义find_handler方法来实现对self.routes的遍历。

class API(object):

def find_handler(self, request_path):

# 遍历路由

for path, handler in self.routes.items():

# 正则匹配路由

parse_result = parse(path, request_path)

if parse_result is not None:

# 返回路由对应的方法和路由本身

return handler, parse_result.named

return None, None

复制代码

了解了路由与方法关联的原理后,就可以实现handle_request方法,该方法主要的路径就是根据路由调度对应的方法,代码如下。

import inspect

class API(object):

def handle_request(self, request):

"""请求调度"""

response = Response()

handler, kwargs = self.find_handler(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.defalut_response(response)

except Exception as e:

raise e

return response

复制代码

在该方法中,首先实例化webob库的Response对象,然后通过self.find_handler方法获取此次请求路由对应的方法和对应的参数,比如。

@app.route("/hello/{name}")

def hello(requst, response, name):

response.text = f"Hello, {name}"

复制代码

它将返回hello方法对象和name参数,如果是/hello/二两,那么name就是二两。

因为route装饰器可能装饰器的类对象,比如。

可以装饰类

@app.route("/book")

class BooksResource(object):

def get(self, req, resp):

resp.text = "Books Page"

复制代码

此时self.find_handler方法返回的hanler就是个类,但我们希望调用的是类中的get、post、delete等方法,所以需要一个简单的判断逻辑,通过inspect.isclass方法判断handler如果是类对象,那么就通过getattr方法获取类对象实例的中对应的请求方法。

获取请求方法, request.method.lower() 可为 get、post、delete

handler = getattr(handler(), request.method.lower(), None)

复制代码

如果类对象中没有该方法属性,则抛出该请求类型不被允许的错误,如果不是类对象或类对象中存在该方法属性,则直接调用则可。

此外,如果方法的路由并没有注册到self.routes中,即404的情况,定义了defalut_response方法返回其中内容,代码如下。

class API(object):

def defalut_response(self, response):

response.status_code = 404

response.text = "Not Found"

复制代码

如果handle_request方法中调度的过程出现问题,则直接raise将错误抛出。

至此,一个最简单的web服务就编写完成了。

支持静态文件

回顾Flask,Flask可以支持HTML、CSS、JavaScript等静态文件,利用模板语言,可以构建出简单但美观的Web应用,我们让TopWebF也支持这一功能,最终实现图中的网站,完美兼容静态文件。

Flask使用了jinja2作为其html模板引擎,ToyWebF同样使用jinja2,jinja2其实实现一种简单的DSL(领域内语言),让我们可以在HTML中通过特殊的语法改变HTML的结构,该项目非常值得研究学习。

首先pip install jinja2,然后就可以使用它了,在ToyWebF项目目录中创建templates目录,以该目录作为默认的HTML文件根目录,代码如下。

from jinja2 import Environment, FileSystemLoader

class API(object):

def init(self, templates_dir="templates"):

# html文件夹

self.templates_env = Environment(loader=FileSystemLoader(os.path.abspath(self.templates_dir)))

def template(self, template_name, context=None):

"""返回模板内容"""

if context is None:

context = {}

return self.templates_env.get_template(template_name).render(**context)

复制代码

首先利用jinja2的FileSystemLoader类将file system中的某个文件夹作为loader,然后初始化Environment。

在使用的过程中(即调用template方法),通过get_template方法获得具体的某个模板并通过render方法将对应的内容传递给模板中的变量。

这里我们不写前端代码,直接去互联网中下载模板,这里下载了Bootstrap提供的免费模板,可以自行去https://startbootstrap.com/themes/freelancer/下载,下载完后,你会获得index.html以及对应的css、jss、img等文件,将index.html移动到ToyWebF/templates中并简单修改了一下,添加一些变量。

{{ title }}

你好呀-{{ name }}

复制代码
然后在app.py文件中为index.html定义路由以及需要的参数。
@app.route("/index")
def index(req, resp):
template = app.template("index.html", context={"name": "二两", "title": "ToyWebF"})
# resp.body需要bytes,template方法返回的是unicode string,所以需要编码
resp.body = template.encode()
复制代码
至此html文件的支持就完成了,但此时的html无法正常载入css和js,导致页面布局非常丑陋且交互无法使用。
接着就让ToyWebF支持css、js,首先在ToyWebF目录下创建static文件夹用于存放css、js或img等静态文件,随后直接将前面下载的模板,其中的静态文件复制到static中则可。
通过whitenoise第三方库,可以通过简单的几行代码让web框架支持css和js,不需要依赖nginx等服务,首先pip install whitenoise,随后修改API类的__init__方法,代码如下。
class API(object):
def __init__(self, templates_dir="templates", static_dir="static"):
# html文件夹
self.templates_env = Environment(loader=FileSystemLoader(os.path.abspath(self.templates_dir)))
# css、JavaScript文件夹
self.whitenoise = WhiteNoise(self.wsgi_app, root=static_dir)
复制代码
其实就是通过WhiteNoise将self.wsgi_app方法包裹起来,在调用API的__call__方法时,直接调用self.whitenoise。
class API(object):
def __call__(self, environ, start_response):
return self.whitenoise(environ, start_response)
复制代码
此时,如果请求web服务获取css、js等静态资源,WhiteNoise会获取其内容并返回给client,它在背后会匹配静态资源在系统中对应的文件并将其读取返回。
至此,一开始的网页效果就实现好了。
自定义错误
web服务如果出现500时,默认会返回internal server error,这显得比较丑,为了让框架使用者可以自定义500时返回的错误,需要添加一些代码。
首先API初始化时,初始self.exception_handler对象并定义对应的方法添加自定义的错误
class API(object):
def __init__(self, templates_dir="templates", static_dir="static"):
# 自定义错误
self.exception_handler = None
def add_exception_handler(self, exception_handler):
# 添加自定义error handler
self.exception_handler = exception_handler
复制代码
在handler_request方法进行请求调度时,调度的方法执行逻辑时报500,此时不再默认将错误抛出,而是先判断是否有自定义错误处理。
class API(object):
def handle_request(self, request):
"""请求调度"""
try:
# ...省略
except Exception as e:
# 为空,才返回internal server error
if self.exception_handler is None:
raise e
else:
# 自定义错误返回形式
self.exception_handler(request, response, e)
return response
复制代码
在app.py中,自定义错误返回方法,如下。
def custom_exception_handler(request, response, exception_cls):
response.text = "Oops! Something went wrong."
# 自定义错误
app.add_exception_handler(custom_exception_handler)
复制代码
custom_exception_handler方法只返回自定义的一段话,你完全可以替换成美观的template。
我们可以实验性定义一个路由来看效果。
@app.route("/error")
def exception_throwing_handler(request, response):
raise AssertionError("This handler should not be user")
复制代码
支持中间件
Web服务的中间件也可以理解成钩子,即在请求前可以对请求做一些处理或者返回Response前对Response做一下处理。
为了支持中间件,在TopWebF目录下创建middleware.py文件,在编写代码前,思考一下如何实现?
回顾一下现在请求的调度逻辑。
1.通过routes装饰器关联路由和方法 2.通过API.whitenoise处理 3.如果是请求API接口,那么会将参数传递给API.wsgi_app 4.API.wsgi_app最终会调用API.handle_request方法获取路由对应的方法并调用该方法执行相应的逻辑
如果希望在request前以及response后做相应的操作,那么其实就需要让逻辑在API.handle_request前后执行,看一下代码。
from webob import Request
class Middleware(object):
def __init__(self, app):
self.app = app # API类实例
def add(self, middleware_cls):
# 实例化Middleware对象,包裹self.app
self.app = middleware_cls(self.app)
def process_request(self, req):
# request前要做的处理
pass
def process_response(self, req, resp):
# response后要做的处理
pass
def handle_request(self, request):
self.process_request(request)
response = self.app.handle_request(request)
self.process_response(request, response)
return response
def __call__(self, environ, start_response):
request = Request(environ)
response = self.app.handle_request(request)
return response(environ, start_response)
复制代码
其中add方法会实例化Middleware对象,该对象会将当前的API类实例包裹起来。
Middleware.handle_request方法其实就是在self.app.handle_request前调用self.process_request方法处理request前的数据以及调用self.process_response处理response后的数据,而核心的调度逻辑,依旧交由API.handle_request方法进行处理。
这里的代码可能会让人感到疑惑,__call__方法和handle_request方法中都有self.app.handle_request(request),但其调用对象似乎不同?这个问题暂时放一下,先继续完善代码,然后再回来解释。
接着在api.py中为API创建middleware属性以及添加新中间件的方法。
class API(object):
def __init__(self, templates_dir="templates", static_dir="static"):
# 请求中间件,将api对象传入
self.middleware = Middleware(self)
def add_middleware(self, middleware_cls):
# 添加中间件
self.middleware.add(middleware_cls)
复制代码
随后,在app.py中,自定义一个简单的中间件,然后调用add_middleware方法将其添加。
class SimpleCustomMiddleware(Middleware):
def process_request(self, req):
print("处理request", req.url)
def process_response(self, req, resp):
print("处理response", req.url)
app.add_middleware(SimpleCustomMiddleware)
复制代码
定义好中间件后,在请求调度时,就需要使用中间件,为了兼容静态文件的情况,需要对css、js、ing文件的请求路径做一下兼容,在其路径中加上/static前缀

复制代码
紧接着,修改API的__call__,兼容中间件和静态文件,代码如下。
class API(object):
def __call__(self, environ, start_response):
path_info = environ["PATH_INFO"]
static = "/" + self.static_dir
# 以 /static 开头 或 中间件为空
if path_info.startswith(static) or not self.middleware:
# "/static/index.css" -> 只取 /index.css, /static开头只是用于判断
environ["PATH_INFO"] = path_info[len(static):]
return self.whitenoise(environ, start_response)
return self.middleware(environ, start_response)
复制代码
至此,中间件的逻辑就完成了。
但代码中依旧有疑惑,Middleware类中的__call__方法和handle_request方法其调用的self.app到底是谁?
为了方便理解,这里一步步拆解。
如果没有添加新的中间件,那么请求的调度逻辑如下。
# 属性映射关系
API.middleware = Middleware
API.middleware.app = API
# 调度逻辑
API.__call__ -> middleware.__call__ -> self.app.handle_request -> API.handle_request()
复制代码
在没有添加中间件的情况下,self.app其实就是API本身,所以middleware.__call__中的self.app.handle_request就是调用API.handle_request。
如果添加了新的中间件,如上述代码中添加了名为SimpleCustomMiddleware的中间件,此时的请求调度逻辑如下。
# 属性映射关系
API.middleware = Middleware
API.middleware.app = API
API.middleware.add(SimpleCustomMiddleware)
API.middleware.app = SimpleCustomMiddleware
API.middleware.app.app = api 相当于 API.middleware.SimpleCustomMiddleware.app = api
# 调度逻辑
API.__call__ -> middleware.__call__ -> self.app.handle_request -> SimpleCustomMiddleware.handle_request() -> self.app.handle_request -> API.handle_request()
复制代码
因为注册中间件时,Middleware.add方法替换了原始Middleware实例中的app对象,将其替换成了SimpleCustomMiddleware,而SimpleCustomMiddleware也有app对象,SimpleCustomMiddleware中的app对象,才是API类实例。
在请求调度的过程中,就会触发Middleware类的handle_request方法,该方法就会执行中间件相应的逻辑去处理request和response中的数据。
当然,你可以通过Middleware.add方法添加多个中间件,这就会构成栈式调用的效果,代码如下。
class SimpleCustomMiddleware(Middleware):
def process_request(self, req):
print("处理request", req.url)
def process_response(self, req, resp):
print("处理response", req.url)
class SimpleCustomMiddleware2(Middleware):
def process_request(self, req):
print("处理request2", req.url)
def process_response(self, req, resp):
print("处理response2", req.url)
app.add_middleware(SimpleCustomMiddleware)
app.add_middleware(SimpleCustomMiddleware2)
复制代码
启动web服务后,其执行效果如下。

零基础小白必看篇:从0到1构建Python Web框架的更多相关文章

  1. 2019最新WEB前端开发小白必看的学习路线(附学习视频教程)

    2019最新WEB前端开发小白必看的学习路线(附学习视频教程).web前端自学之路:史上最全web学习路线,HTML5是万维网的核心语言,标准通用标记语言下的一个应用超文本标记语言(HTML)的第五次 ...

  2. 【自动化基础】手把手教零基础小白搭建APP的UI自动化环境

    前言 帮助零基础小白一步步搭建UI自动化环境,完成Python+Appium+模拟器/真机的UI自动化环境搭建. 环境准备: jdk1.8.0 sdk Node.js appium python Ap ...

  3. 学Java必看!零基础小白再也不用退缩了

    程序员们!请往这儿看 对于JAVA的学习,可能你还会有许多的顾虑 不要担心 接着往下看吧 学Java前 一.数学差,英语也不好是不是学不好Java? 答案是:是~ 因为你在问这个问题的时候说明你对自己 ...

  4. 小白必看Python视频基础教程

    Python的排名从去年开始就借助人工智能持续上升,现在它已经成为了第一名.Python的火热,也带动了工程师们的就业热.可能你也想通过学习加入这个炙手可热的行业,可以看看Python视频基础教程,小 ...

  5. (转载)Android之有效防止按钮多次重复点击的方法(必看篇)

    为了防止测试妹子或者用户频繁点击某个按钮,导致程序在短时间内进行多次数据提交or数据处理,那到时候就比较坑了~ 那么如何有效避免这种情况的发生呢? 我的想法是,判断用户点击按钮间隔时间,如果间隔时间太 ...

  6. 基于StringUtils工具类的常用方法介绍(必看篇)

    前言:工作中看到项目组里的大牛写代码大量的用到了StringUtils工具类来做字符串的操作,便学习整理了一下,方便查阅. isEmpty(String str) 是否为空,空格字符为false is ...

  7. 利用JS提交表单的几种方法和验证(必看篇)

    第一种方式:表单提交,在form标签中增加onsubmit事件来判断表单提交是否成功 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <scr ...

  8. 从零构建一个简单的 Python Web框架

    为什么你想要自己构建一个 web 框架呢?我想,原因有以下几点: 你有一个新奇的想法,觉得将会取代其他的框架 你想要获得一些名气 你遇到的问题很独特,以至于现有的框架不太合适 你对 web 框架是如何 ...

  9. 零基础小白怎么用Python做表格?

    用Python操作Excel在工作中还是挺常用的,因为毕竟不懂Excel是一个用户庞大的数据管理软件.本文用Python3!在给大家分享之前呢,小编推荐一下一个挺不错的交流宝地,里面都是一群热爱并在学 ...

随机推荐

  1. 网站seo优化有什么优缺点

    http://www.wocaoseo.com/thread-94-1-1.html       seo是什么?这个可能是刚刚知道网络营销或搜索引擎营销的朋友们问的话,笔者在这里装一下,呵呵.说真的现 ...

  2. 基于postman的api自动化测试实践

    测试的好处 每个人都同意测试很重要,但并不是所有人都会去做.每当你添加新的代码,测试可以保证你的api按照预期运行.通过postman,你可以为所有api编写和运行测试脚本. postman中的测试 ...

  3. 释放数据价值:DAYU数据运营新能力解读

    摘要:从比特到信息,这说的其实就是企业数字化转型,让数据的价值充分发挥出来,变成信息. 今天,企业对数据越来越重视,数据已经成为了企业新型的资产,甚至是核心资产,最近流传一句非常有意思的话:从比特到信 ...

  4. 浅析LR.Net工作流引擎

    在当代信息化软件系统开发中,工作流引擎是其中非常重要的一环.所谓工作流引擎,是指工作流作为软件系统的一部分, 其中包括了流程的节点管理.流向管理.流程样例管理.审核管理等重要功能. 工作流引擎可根据角 ...

  5. 2020 最新python入门知识

    1. 基础语法 1.1 注释 在编写代码的时候,有些代码不需要执行或增加代码说明,那么就需要用到注释了. 被注释的文本或代码是不会被执行的. 注释可以使用如下三种方式: # 号 # 第一个注释,本行代 ...

  6. H5简单内容

    1.简单认识H5 HTML5不仅仅是作为HTML标记语言的一个最新版本,更重要的是它指定了Web开发的一系列标准,成为第一个将Web作为应用开发平台的HTML语言. 我们日常讨论的H5其实是有一个泛称 ...

  7. 用于测试XmlAnalyzer 1.00版的八个测试用例

    XmlAnalyzer 工程目的:将XML的属性和子节点按字母序排列,然后整形输出: 08. 原文=<project xmlns="http://maven.apache.org/PO ...

  8. 从IL角度彻底理解回调_委托_指针

    从IL角度彻底理解回调_委托_指针 目录 从IL角度彻底理解回调_委托_指针 1.创作此文的背景 1.1.委托能帮助代码更好地封装 1.2.委托能随时随地更方便地运行其他类中的方法 1.3.委托非常适 ...

  9. java输出1-100之间的数并求和for+while+do while实现

    public static void main(String args[]) {//do while int sum = 0; //当前之和 int i = 1; //加数 do { if (i%2= ...

  10. 关于LLVM源码在Linux下编译的过程

    1. 最好在gitee上查找llvm的源码或者直接git clone https://gitee.com/uniquesquirrel/llvm-project.git,是和github上的llvm项 ...