范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

纯Python写一个Web框架,就是这么简单

  造轮子是最好的一种学习方式,本文尝试从0开始造个Python Web框架的轮子,我称它为 ToyWebF  。
  本文操作环境为:MacOS,文中涉及的命令,请根据自己的系统进行替换。
  ToyWebF的简单特性: 1.支持多种不同形式的路由注册方式 2.支持静态HTML、CSS、JavaScript 3.支持自定义错误 4.支持中间件
  下面我们来实现这些特性。 最简单的web服务
  首先,我们需要安装gunicorn,回忆一下Flask框架,该框架有内置的Web服务器,但不稳定,所以上线时通常会替换成uWSGI或gunicorn,这里不搞这个内置Web服务,直接使用gunicorn。
  我们创建新的目录与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服务后,其执行效果如下。


同一件白T,凭什么她穿出高级感而你却像地摊货?你必须学会挑选在机场暗暗比美的女明星们,早就用白色T恤演绎出不同的时髦机场look可爱教主杨丞琳用牛仔背带裤搭配白T恤,配以甜甜的微笑,少女感十足热巴用修身半裙搭配白色T恤,甜美中带点小性感宋祖又土又壮的关晓彤竟然不土了!第N次撞衫泫雅,居然赢了说到关晓彤,长得很精致,可是老是被扣上土味大妈壮汉各种头衔扣她身上也是蛮心疼的!之前的时候,她常常因为一些奇奇怪怪的造型,被全网吐槽时尚黑洞。说实话,关晓彤的这套造型当时真的是吓到守护每颗童心,让孩子们放下手机让孩子,开启一场纸上环球旅行孩子对学习没兴趣?多半是方法的问题。要知道,填鸭式的将知识通通塞给孩子,远没有孩子自己探索发现知识更容易被接受。就如风靡全球的乐高拼插玩具,不仅是小孩爱玩,就连很多大朋友也爱不释手3块钱给全屋蟑螂来一套黑人抬棺!诱捕收拾一步到位南方人的屋子,实在太难了。明明在厨房一心一意地炒着菜,突然窜出一只蟑螂!而且个头硕大无比,一言不合就起飞,能把豪迈的东北大汉吓得哭唧唧的逃跑!大概就是这个画面打不死的小强可不是白叫这款神奇的喜茶喝了不胖?0蔗糖低卡低热量,减肥男女的福利来啦马上就要到了露胳膊露腿的季节你们的减肥计划进行的怎么样了是否准备好漂亮的吊带裙子征服夏天了面对灵魂拷问手里的奶茶突然就不香了奈何有一颗减肥的心却受不住奶茶的诱惑毕竟奶茶是每天生活中裙子里安全裤别乱穿!这款轻薄冰爽的仙仙裤,告别闷汗蒸笼感五一一过,夏日气息扑面而来,看到满大街白晃晃的小胳膊小腿,就已经按耐不住穿起裙子扭扭扭的心了可是穿裙子总有尴尬的事情不经常被风吹起的裙摆裙子太短稍不小心就走光就连女明星也逃不过。穿综拓系统AxureRP原型编号0186综拓系统AxureRP原型版权说明本站所有资料主要来源于网络的公开信息,都保留了原来的版权信息,本站所有的资料文档仅限用于学习交流,如若有侵权的嫌疑,请及时告知进行删除原型赏析福利积分商城AxureRP原型编号0173福利积分商城AxureRP原型版权说明本站所有资料主要来源于网络的公开信息,都保留了原来的版权信息,本站所有的资料文档仅限用于学习交流,如若有侵权的嫌疑,请及时告知进行南站东西广场管理平台PC端AxureRP原型编号0189南站东西广场信息化综合管理平台PC端AxureRP原型版权说明本站所有资料主要来源于网络的公开信息,都保留了原来的版权信息,本站所有的资料文档仅限用于学习交流,如若有侵中联设备租赁系统AxureRP原型编号0183中联设备租赁系统AxureRP原型版权说明本站所有资料主要来源于网络的公开信息,都保留了原来的版权信息,本站所有的资料文档仅限用于学习交流,如若有侵权的嫌疑,请及时告知商城O2O商家端AxureRP原型编号0171商城O2O商家端AxureRP原型版权说明本站所有资料主要来源于网络的公开信息,都保留了原来的版权信息,本站所有的资料文档仅限用于学习交流,如若有侵权的嫌疑,请及时告知
新款iPadMini出现果冻屏苹果回应称是LCD屏的正常现象你看出来了吗?iPadmini6已于近日正式发售,然而有的消费者买到手之后,察觉到这款平板在竖屏状态下滑动屏幕时出现了果冻效应,简单地说,就是在上下拖动屏幕时,屏幕从右至左存在明显苹果的新iPhone需要更长的时间才能到达客户手中分析师表示,苹果公司(AAPL。O)的客户将不得不再等几周才能拿到新的iPhone13,因为供应链的延迟和强劲的需求导致该手机的等待时间是近年来最长的之一。苹果公司的iPhone在AppleWatch黑科技功能亮相,Watch6低至百元,买早的果粉心碎2017年,斯坦福大学和苹果联合启动了苹果心脏研究项目。后续研究表明,除了房颤(AFib),AppleWatch还可以检测其他类型的心律失常。目前,AppleWatch提供可能的心三星F42正式发布天玑70090Hz屏5000mAh电池,售价1831起最近三星在印度市场可以说是动作频频,连续发布了多款产品,抢占市场野心十分明显。9月29日,三星在印度市场正式发布三星F42手机,也是该系列的首款5G手机,售价20999卢比(约合人aigo多功能无线充插座M0230T为桌面添加一丝现代化艺术气息什么叫桌面上的美学文化?相信很多人都听过音乐美学舞蹈美学戏剧美学等专业名词,但唯独对桌面上的美学文化,有点大惑不解。实际上,桌面美学文化在目前已经成为了各大数码论坛网站上的热门板块大学老师的必备办公笔记本科大讯飞智能办公本T2评测我在体验过科大讯飞智能办公本T2之后只有一个想法毫无疑问,这是迄今为止最适合大学教师的一款智能笔记本,没有之一。至于我为什么会发出这样的感慨,这就要回溯到几天前了。前些日子,我和当买苹果,到底对不对?最近,有个男子在iphone门店喊要买华为才是爱国,买苹果是不爱国,这样的行为值得我们深思,难道买华为国产就是真正的爱国嘛!对此,任老爷子发话了,说到底,华为手机还是个产品,真心的OPPOK9Pro正式发布1999起售值得入手?详细配置参数了解一下9月最重磅的新机发布无非就是iPhone13系列了,其他各大手机厂商好像事先商量好了一样,纷纷发布了旗下的性价比中端价位机型OPPOK9Pro也在26日正式发布了。发布前大尾还预测这才是科技的温度,MIUI添加新功能,方便弱势群体手机本身是作为能够方便人们随时联络而应运而生的通讯工具存在的,随着科技的发展,手机被容纳了更多的功能,通讯功能不再是唯一,工作娱乐的属性也被纳入其中,毋庸置疑,手机已经成为我们日常劝退第三方维修?iPhone13未经官方授权换屏会丢失FaceID面部功能苹果近年来的iPhone机型一直以高集成度而出名,因此它的维修难度也越来越高,而到了iPhone13系列后,苹果对于维修方面可能有了更严格的管控。根据外媒MacRumors的相关报小米civi参数配置怎么样,详细评测这款手机采用6。55英寸3D微曲OLED柔性屏(FHD专业原色屏),后背采用特殊的丝绒AG设计,支持10。7亿色120Hz刷新率DCIP3色域950尼特局部峰值亮度50000001