零基础小白必看篇:从0到1构建Python Web框架_第1页
零基础小白必看篇:从0到1构建Python Web框架_第2页
零基础小白必看篇:从0到1构建Python Web框架_第3页
零基础小白必看篇:从0到1构建Python Web框架_第4页
零基础小白必看篇:从0到1构建Python Web框架_第5页
已阅读5页,还剩8页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

零基础小白必看篇:从0到1构建PythonWeb框架造轮子是最好的一种学习方式,本文尝试从0开始造个PythonWeb框架的轮子,我称它为ToyWebF。本文操作环境为:MacOS,文中涉及的命令,请根据自己的系统进行替换。ToyWebF的简单特性:支持多种不同形式的路由注册方式支持静态HTML、CSS、JavaScript支持自定义错误支持中间件下面我们来实现这些特性。最简单的web服务首先,我们需要安装gunicorn,回忆一下Flask框架,该框架有内置的Web服务器,但不稳定,所以上线时通常会替换成uWSGI或gunicorn,这里不搞这个内置Web服务,直接使用gunicorn。这里多说一句,小编是一名python开发工程师,这里有我自己整理的一套最新的python系统学习教程,包括从基础的python脚本到web开发、爬虫、数据分析、数据可视化、机器学习等。想要这些资料的可以关注小编,并在后台私信小编即可领取。我们创建新的目录与Python虚拟环境,在该虚拟环境中安装gunicornmkdirToyWebFpython3-mvenvvenv#创建虚拟环境sourcevenv/bin/activat激活虚拟环境pipinstallgunicorn复制代码在啥都没有的情况下,构建最简单的Web服务,在ToyWebF目录下,创建app.py与api.py文件,写入下面代码。api.py文件classAPI:defcall(self,environ,start_response):response_body=b"Hello,World!"status="200OK"start_response(status,headers二口)returniter([response_body])app.py文件fromapiimportAPIapp=API()复制代码运行gunicornapp:app访问http://127.O.O.l:80O可以看见Hello,World!但现在请求体中的参数在environ变量中,难以解析,我们返回的response也是bytes形式。我们可以使用webob库,将environ中的数据转为Request对象,将需要返回的数据转为Response对象,处理起来更加直观方便,直接通过pip安装一下。pipinstallwebob复制代码然后修改一下API类的—_call_R法,代码如下。fromwebobimportRequest.ResponseclassAPI(object):defwsgi_app(self,environ,start_response):""通过webob将请求的环境信息转为request对象""“request二Request(environ)response=self.handle_request(request)returnresponse(environ,start_response)defcall(self,environ,start_response):self.wsgi_app(environ,start_response)复制代码上述代码中,通过webob库的Request类将environ对象(请求的环境信息)转为容易处理的request,随后调用handle_request方法对request进行处理,处理的结果,通过response对象返回。handle_request方法在ToyWebF中非常重要,它会匹配出某个路由对应的处理方法,然后调用该方法处理请求并将处理的结果返回,在解析handle_request前,需要先讨论路由注册实现,代码如下。classAPI(object):defini(self):#ur路由self.routes={}defroute(self,path):#添加路由的装饰器defwrapper(handler):self.add_route(path,handler)returnhandlerreturnwrapperdefadd_route(self,path,handler):#相同路径不可重复添加assertpathnotinself.routes,"Suchroutealreadyexists"self.routes[path]=handler复制代码其实就是将路由和方法存到self.route字典中,可以通过route装饰器的形式将路由和方法关联,也可以通过add_route方法关联,在app.py中使用一下。app=API()通过装饰器关联路由和方法@app.route("/home")defhome(request,response):response.text二"ThisisHome"路由中可以有变量,对应的方法也需要有对应的参数@app.route("/hello/{name}")defhello(requst,response,name):response.text二f"Hello,{name}"可以装饰类@app.route("/book")classBooksResource(object):defget(self,req,resp):resp.text二"BooksPage"defhandlerl(req,resp):resp.text二"handlerl"可以直接通过add_route方法添加app.add_route("/handler1",handler1)复制代码因为ur呻可以存在变量,如@app.route("/hello/{name}")所以在匹配时,需要进行解析,可以使用正则匹配的方式进行匹配,parse这个第三方库已经帮我们实现了相应的正则匹配逻辑,pip安装使用一下则可。pipinstallparseIn[1]:fromparseimportparse匹配In[2]:res=parse("/hello/{name}",二二两Olo/In[3]:dOut[3]:{'name'二两'}复制代码这里定义find_handle访法来实现对self.route的遍历。classAPI(object):deffind_handler(self,request_path):#遍历路由forpath,handlerinself.routes.items():#正则匹配路由parse_result=parse(path,request_path)ifparse_resultisnotNone:parse_result#返回路由对应的方法和路由本身returnhandler,parse_dreturnNone,None复制代码了解了路由与方法关联的原理后,就可以实现handle_request方法,该方法主要的路径就是根据路由调度对应的方法,代码如下。importinspectclassAPI(object):defhandle_request(self,request):〃〃请求调度〃〃〃response=Response()handler,kwargs=self.find_handler(request.path)try:ifhandlerisnotNone:ifinspect.isclass(handler如果是类,则获取其中的方法handler=getattr(handler(),request.method.lower().None)ifhandlerisNone:粪中该方法不存在,则该类不支持该请求类型raiseAttributeError(〃Methodnowallowed”,request.method)handler(request,response,**kwargs)else:#返回默认错误self.defalut_response(response)exceptExceptionase:raiseereturnresponse复制代码在该方法中,首先实例化webob库的Response对象,然后通过self.find_handl方法获取此次请求路由对应的方法和对应的参数,比如。@app.route("/hello/{name}")defhello(requst,response,name):response.text二f"Hello,{name}"复制代码它将返回hello方法对象和name参数,如果是/hello二两,那么name就是二两。因为route装饰器可能装饰器的类对象,比如。可以装饰类@app.route("/book")classBooksResource(object):defget(self,req,resp):resp.text二"BooksPage"复制代码此时self.find_handl方法返回的hanler就是个类,但我们希望调用的是类中的get、post、delete等方法,所以需要一个简单的判断逻辑,通过inspect.iscla方法判断handler如果是类对象,那么就通过getatt方法获取类对象实例的中对应的请求方法。获取请求方法,request.method.lower()可为get、post、deletehandler=getattr(handler(),request.method.lower(),None)复制代码如果类对象中没有该方法属性,则抛出该请求类型不被允许的错误,如果不是类对象或类对象中存在该方法属性,则直接调用则可。此外,如果方法的路由并没有注册到self.route中,即404的情况,定义了defalut_response方法返回其中内容,代码如下。classAPI(object):defdefalut_response(self,response):response.status_code=404response.text二"NotFound"复制代码如果handle_request方法中调度的过程出现问题,则直接raise将错误抛出。至此,一个最简单的web服务就编写完成了。支持静态文件回顾Flask,Flask可以支持HTML、CSS、JavaScript等静态文件,利用模板语言,可以构建出简单但美观的Web应用,我们让TopWebF也支持这一功能,最终实现图中的网站,完美兼容静态文件。Flask使用了jinja2乍为其html模板引擎,ToyWebF同样使用jinja2jinja2其实实现一种简单的DSL(领域内语言),让我们可以在HTML中通过特殊的语法改变HTML的结构,该项目非常值得研究学习。首先pipinstalljinj然2后就可以使用它了,在ToyWebF项目目录中创建templates目录,以该目录作为默认的HTML文件根目录,代码如下。fromjinja2importEnvironment,FileSystemLoaderclassAPI(object):definifself,templates_dir二"templates"):#html文件夹self.templates_env=Environment(loader二FileSystemLoader(os.path.abspath(self.templates_dir)))deftemplate(self,template_name,context二None):返回模板内容ifcontextisNone:context二{}returnself.templates_env.get_template(template_name).render(**context)复制代码首先利用jinja的FileSystemLoader类将filesyste中的某个文件夹作为loader,然后初始化Environment。在使用的过程中(即调用template方法),通过get_template方法获得具体的某个模板并通过render方法将对应的内容传递给模板中的变量。这里我们不写前端代码,一直接去互联网中下载模板,这里下载了Bootstrap提供的免费模板,可以自行去/themes/freela下^er/载,下载完后,你会获得index.html以及对应的css、jssimg等文件,将index.html移动到ToyWebF/templates中并简单修改了一下,添加一些变量。你好呀-{{name}}复制代码然后在app.py文件中为index.html定义路由以及需要的参数。@app.route("/index")defindex(req,resp):template=app.template("index.html",context二{"nan二"两"""title":"ToyWebF"})resp.body需要bytes,template方法返回的是unicodestring所以需要编码resp.body=template.encode()复制代码至此html文件的支持就完成了,但此时的html无法正常载入css和js导致页面布局非常丑陋且交互无法使用。接着就让ToyWebF支持css、js首先在ToyWebF目录下创建stati文件夹用于存放css、js或img等静态文件,随后直接将前面下载的模板,其中的静态文件复制到stati(中则可。通过whitenoise第三方库,可以通过简单的几行代码让web框架支持css和js不需要依赖nginx等服务,首先pipinstallwhitenoi随后修改API类的__init_方法,代码如下。classAPI(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.whitenoiseclassAPI(object):def__call__(self,environ,start_response):returnself.whitenoise(environ,start_response)复制代码此时,如果请求web服务获取css、js等静态资源,WhiteNoise会获取其内容并返回给client它在背后会匹配静态资源在系统中对应的文件并将其读取返回。至此,一开始的网页效果就实现好了。自定义错误web服务如果出现500时,默认会返回internalservererro这显得比较丑,为了让框架使用者可以自定义500时返回的错误,需要添加一些代码。首先API初始化时,初始self.exception_handle对象并定义对应的方法添加自定义的错误classAPI(object):def__init__(self,templates_dir二"templates",static_dir="static"):#自定义错误self.exception_handler=Nonedefadd_exception_handler(self,exception_handler):添加自定义errorhandlerself.exception_handler=exception_handler复制代码在handler_request方法进行请求调度时,调度的方法执行逻辑时报500,此时不再默认将错误抛出,而是先判断是否有自定义错误处理。classAPI(object):defhandle_request(self,request):〃〃请求调度〃〃〃try:..省略exceptExceptionase:为空,才返回internalservererrorifself.exception_handlerisNone:raiseeelse:#自定义错误返回形式self.exception_handler(request,response,e)returnresponse复制代码在app.py中,自定义错误返回方法,如下。defcustom_exception_handler(request,response,exception_cls):response.text二〃0ops!Somethingwentwrong.”#自定义错误app.add_exception_handler(custom_exception_handler)复制代码custom_exception_handler方法只返回自定义的一段话,你完全可以替换成美观的template。我们可以实验性定义一个路由来看效果。@app.route("/error")defexception_throwing_handler(request,response):raiseAssertionError(〃Thishandlershouldnotbeuser")复制代码支持中间件Web服务的中间件也可以理解成钩子,即在请求前可以对请求做一些处理或者返回Response前对Response做一下处理。为了支持中间件,在TopWebF目录下创建middleware.py文件,在编写代码前,思考一下如何实现?回顾一下现在请求的调度逻辑。1.通过routes装饰器关联路由和方法2.通过API.whitenoise处理3.如果是请求API接口,那么会将参数传递给API.wsgi_app4.API.wsgi_app最终会调用API.handle_request方法获取路由对应的方法并调用该方法执行相应的逻辑如果希望在request前以及response后做相应的操作,那么其实就需要让逻辑在API.handle_request前后执行,看一下代码。fromwebobimportRequestclassMiddleware(object):def__init__(self,app):self.app=app#AP类实例defadd(self,middleware_cls):#实例化Middleware对象,包裹self.appself.app=middleware_cls(self.app)defprocess_request(self,req):request前要做的处理passdefprocess_response(self,req,resp):response后要做的处理passdefhandle_request(self,request):cess_request(request)response=self.app.handle_request(request)cess_response(request,response)returnresponsedef__call__(self,environ,start_response):request二Request(environ)response=self.app.handle_request(request)returnresponse(environ,start_response)复制代码其中add方法会实例化Middleware对象,该对象会将当前的API类实例包裹起来。Middleware.handle_request方法其实就是在self.app.handle_reques前调用cess_reques方法处理request前的数据以及调用cess_response处理response后的数据,而核心的调度逻辑,依旧交由API.handle_request方法进行处理。这里的代码可能会让人感到疑惑,—call__^法和handle_request方法中都有self.app.handle_request(request)但其调用对象似乎不同?这个问题暂时放一下,先继续完善代码,然后再回来解释。接着在api.py中为API创建middleware属性以及添加新中间件的方法。classAPI(object):def__init__(self,templates_dir二"templates",static_dir="static"):#请求中间件,将api对象传入self.middleware=Middleware(self)defadd_middleware(self,middleware_cls):#添加中间件self.middleware.add(middleware_cls)复制代码随后,在app.py中,自定义一个简单的中间件,然后调用add_middleware方法将其添加。classSimpleCustomMiddleware(Middleware):defprocess_request(self,req):print处理request",req.url)defprocess_response(self,req,resp):print处理response",req.url)app.add_middleware(SimpleCustomMiddleware)复制代码定义好中间件后,在请求调度时,就需要使用中间件,为了兼容静态文件的情况,需要对css、jsing文件的请求路径做一下兼容,在其路径中加上/static前缀复制代码紧接着,修改API的—call__兼容中间件和静态文件,代码如下。classAPI(object):def__call__(self,environ,start_response):path_info=environ["PATH_INFO"]static="/"+self.static_dir#以/stati开头或中间件为空ifpath_info.startswith(static)ornotself.middleware:#"/static/index.css"只取/index.css/stati开头只是用于判断environ["PATH_INFO"]=path_info[len(static):]returnself.whitenoise(environ,start_response)returnself.middleware(environ,start_response)复制代码至此,中间件的逻辑就完成了。但代码中依旧有疑惑,Middleware类中的—call方法和handle_request方法其调用的self.app到底是谁?为了方便理解,这里一步步拆解。如果没有添加新的中间件,那么请求的调度逻辑如下。#属性映射关系API.middleware=MiddlewareAPI.middleware.app=API#调度逻辑API.__call__->middleware.__call__->self.app.handle_request->API.handle_request()复制代码在没有添加中间件的情况下,self.app其实就是API本身,所以middleware.__call中的self.app.handle_reques就是调用API.handle_request。如果添加了新的中间件,如上述代码中添加了名为SimpleCustomMiddleware的

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论