最简单的示例
from pprika import PPrika app = PPrika() @app.route('/') def index(): return 'Hello world!' if __name__ == '__main__': app.run()
可以看到用法与flask几乎一样,但app生成不需要传递import_name
下方开始按请求处理顺序介绍PPrika类的方法
route
它是 add_url_rule 的装饰器版本
等同于 app.add_url_rule('/', view_func=index)
add_url_rule
def add_url_rule(self, path, endpoint=None, view_func=None, **options): if endpoint is None: assert view_func is not None, "无endpoint时view_func不可为空" endpoint = view_func.__name__
类似于flask的同名函数 `add_url_rule`,它将一个url注册到给定的endpoint上,并关联endpoint与视图函数view_func。其中url与endpoint多对一,endpoint与view_func一对一,通过endpoint作中介实现了url与func多对一映射。
参数中path即为要绑定的url路径,endpoint若省略则将视图函数名作为其值,因此二者不可同时为空。
methods = options.pop('methods', '') or ("GET",) if isinstance(methods, str): # 即允许了类似methods="POST"的method指定方式 methods = (methods,) methods = set(item.upper() for item in methods)
从options中尝试获取该视图函数所对应的methods,但即便没有显式 'HEAD' 方法,在路由时werkzeug会将HEAD请求作为GET匹配。
rule = Rule(path, methods=methods, endpoint=endpoint, **options)
self.url_map.add(rule)
利用之前处理好的参数构造Rule对象,并将其加入Map。
其中 self.url_map 是Map的实例,Map与Rule都是 werkzeug.routing 提供的类。
通过这一步就完成了一个路由关系的记录,是整个 add_url_rule 的核心步骤
if view_func is not None: old_func = self.view_functions.get(endpoint) if old_func is not None and old_func != view_func: raise AssertionError("此endpoint已有对应函数: %s" % endpoint) self.view_functions[endpoint] = view_func
上一步是在url与endpoint之间建立联系,而这一步就是在endpoint与view_func之间产生关联,并将view_func记录于self.view_functions这一字典中。除此之外还会判断endpoint是否已有对应的视图函数,这部分来自于flask,说实在不是很明白它的意义。
推测:当执行add_url_rule('/', 'index', index)时endpoint='index',绑定了index函数,这时若再执行add_url_rule('/home', 'index', func)要求 func == index,否则会违背endpoint与view_func一对一的原则。
run
from werkzeug.serving import run_simple def run(self, host='localhost', port=9000, **options): options.setdefault("threaded", True) # 线程隔离 run_simple(host, port, self, **options)
本质上是将参数传递给werkzeug提供的开发服务器
其中 self 指的是PPrika的实例,即上方的app。run_simple运行后会等待请求到来,当收到请求时会将所有的请求参数整理为一个变量传递给app,将其作为函数调用得到视图函数返回值,因此需要app实现 __call__ 方法。
ps: 一开始开放的是6000端口,测试了很久发现chrome居然拒绝发出请求,而Edge运行正常。
__call__
def __call__(self, environ, start_response): return self.wsgi_app(environ, start_response)
__call__仅负责转发请求给wsgi_app。这里的environ、start_response由run_simple提供,environ包含了所有请求信息,而start_response则是请求处理最后返回响应时调用的函数,但无需手动使用它。
wsgi_app
假设此时一个请求(http://localhost:9000/)到来,那么run_simple就会将请求传递给__call__,再进一步交由wsgi_app处理。赛这里会实现路由的匹配、处理的请求、响应结果返回以及异常的捕捉与处理。
def wsgi_app(self, environ, start_response): ctx = RequestContext(self, environ) # 请求上下文对象 try: try: ctx.bind() # 绑定请求上下文并匹配路由 rv = self.dispatch_request() response = make_response(rv) except Exception as e: response = self.handle_exception(e) return response(environ, start_response) finally: ctx.unbind()
ctx是请求上下文对象先不用在意,仅需知道通过ctx.bind全局变量request将变得可用。
通过ctx.bind匹配路由后wsgi_app会尝试调用dispatch_request去调用该路由相应的视图函数并获取函数返回值rv,并通过make_response生成响应对象,由于该对象是 werkzeug.wrappers.Response 的实例,以environ与start_response调用它可得到最终的响应,至此整个请求处理完成。
handle_exception作为外层处理函数负责处理没有绑定错误处理器的错误或其他错误。
ps:对比flask,由于pprika没有实现请求钩子之类的,因此也不需要额外的一层full_dispatch_request
dispatch_request
def dispatch_request(self): if request.routing_exception is not None: return self.handle_user_exception(request.routing_exception) # 'url_adapter.match' 时可能产生的路由错误 try: endpoint, args = request.rule.endpoint, request.view_args rv = self.view_functions[endpoint](**args) except Exception as e: rv = self.handle_user_exception(e) return rv
request.routing_exception是上文中ctx.bind时可能发生的路由错误
dispatch_request会尝试从request中获取 endpoint, args,通过endpoint得到对应视图函数并将args作为参数传入得到函数返回值rv。
假如一切正常,会将rv返回给上文的wsgi_app,流程顺利结束。
若视图函数中抛出了异常会先以 self.handle_user_exception 尝试处理,若无法处理则会再次将错误抛出交给wsgi_app中提及的handle_exception。
结语
至此整个路由、请求接受与响应的过程都介绍完毕,这些是框架最核心的功能。
下一篇将讲述错误处理、请求上下文与帮助函数。
[python] pprika:基于werkzeug编写的web框架(3) ——错误处理