基于werkzeug库的python web框架
读了flask的源码和werkzeug的官方文档后用类似的原理写了这个框架,算是重复造轮子,增加理解吧。
GitHub地址:https://github.com/gamdwk/myflame
werkzeug是一个WSGI工具包,算是比较底层的东西。
WSGI是python的web服务器网关接口,Web Server Gateway Interface的缩写。WSGI的app要求是Callable类型。
Application
class Application(object):"""省略几行""" def __init__(self, template_path=‘template‘, static_folder=‘static‘, root_path=None): 。。。。。。。35 def __call__(self, environ, start_response): return self.wsgi_app(environ, start_response) def wsgi_app(self, environ, start_response): ctx = self.request_content(self, environ) ctx.push() try: response = self.dispatch_request(environ) return response(environ, start_response) finally: ctx.pop()
在application中,environ和start_response都不需要我们处理。environ是把进来的request解析成一个字典,我们使用request的时候还是要把它变为request对象进行使用,我的框架默认是使用werkzeug中的Request 类,这个类的功能其实已经够用了。通过路由算法,我们找到对应的处理函数,处理后返回response,这个过程就算结束了。
运行
werkzeug的run_simple()函数。默认是使用多线程,这个多线程可以防止阻塞。
Route
werkzeug在路由方面已经有很成熟的方案了。Map类和Rule类,通过url匹配路由。类似flask,添加路由可以通过装饰器或者直接添加。
def route(self, url_rule, endpoint=None, **kwargs): def decorator(f): self.add_url_rule(url_rule, f, endpoint, **kwargs) return f return decorator def add_url_rule(self, url_rule, view_func=None, endpoint=None, **kwargs): endpoint = endpoint or view_func.__name__ if endpoint in self.view_functions.keys(): raise rule = Rule(url_rule, endpoint=endpoint, **kwargs) self.url_map.add(rule) if view_func is not None:15 self.view_functions[endpoint] = view_func
添加函数后,会有一个endpoint和function的映射存在。匹配url函数的代码如下:
1 adapter = self.url_map.bind_to_environ(environ) 2 rv = adapter.dispatch(lambda e, v: self.view_functions[e](**v))
可以生成一个mapadapter,这个类是用来查找的,match()可以直接查找到rule或endpoint,我这里是直接获得函数返回值。‘
异常处理
对于异常的处理必不可少。werkzeug默认的HTTPEXCEPTION处理其实蛮好的,就没有多加修改,只是给用户添加了修改异常的接口。
abort函数就是使用默认的,它是从一个status和 HTTPEXCEPTION的map中获取错误码对应的异常。所以提供了两个接口,一个是修改这个map,还有一个是加入异常,在出现异常时会默认先使用这个异常的处理函数,然后才是HTTPEXCEPTION。
文件处理
对于静态文件,在初始化时就把静态文件路径注册到路由上了。对应的则是访问对应文件夹文件的函数,可以通过url直接访问
def send_file(path): if not isabs(path): path = join(default_root_path, path) if not isfile(path): raise NotFound file = open(path, ‘rb‘) data = wrap_file(request.environ, file) return current_app.response_class(data, mimetype="application/octet-stream", direct_passthrough=True)
最底层的 发送函数就是这个wrap_file是werkzeug对于二进制流的处理函数,从环境中得到file_wrapper
上下文
模仿flask,上下文分为应用上下文和请求上下文。如果不使用上下文,每个处理函数都要传入request,十分麻烦。问题就在线程上。肯定没办法单线程运行的啊,在werkzeug提供了线程的模块Local,类似于python的thead.local。我们使用的是LocalProxy,LocalStack。 使用这两个模块的目的,就是为了线程隔离,以及多app运行的情况。LocalStack是个线程隔离的栈,开两个栈,一个是应用上下文,一个是请求上下文。在每次请求的时候判断,这个线程的栈顶是否有与请求上下文的app相同的应用上下文。如果没有,就push进去,再把这个请求上下文push进去。当请求处理完毕的时候 ,再把这两个pop出来。这样保证在这个线程全程处理的都是这两个上下文,不同请求之间就不会干扰了。LocalProxy通过初始化时给的函数进行取值,获得真正的数据。有动态更新的效果,估计是每次取值的时候都会执行函数?但是有一点缺点就是在代理后的类IDEA无法联想属性了,flask不知道是如何做到的。
_request_ctx_content = LocalStack() _app_ctx_content = LocalStack() request = LocalProxy(partial(get_request_obj, ‘request‘)) session = LocalProxy(partial(get_request_obj, ‘session‘)) g = LocalProxy(partial(get_app_ctx_obj, ‘g‘)) current_app = LocalProxy(get_current_app)
g没有太多处理,就是一个dict()。current_app代理的就是目前使用的app,request则是Request(environ)。这些都放在请求上下文类和应用上下文类中。
session
在werkzeug 0.9.4版本中,有一些对于session,securecookie之类的操作,flask最初的版本也是使用这些类。但是现在这些模块都消失了,所以只能自己写一个session处理接口。
flask的session保存在cookie中,似乎是通过签名的方式来防止伪造。具体方法不是很清楚,为了防止篡改数据,还是把session放在了内存中,时间关系没有写redis版本的。session本身类似一个dict()的操作。
对于session处理有两步,一个是save到cookie,一个是从cookie中open.中间还踩了坑,在session处理中访问了上下文,结果是先处理session再push上下文,导致报错。
def open_session(self, app, request): sid = request.cookies.get(app.config[‘SESSION_NAME‘]) if sid is not None: sid = byte_to_str(base64.b64decode(sid)) if sid is None or sid not in self.session_map.keys(): sid = self.create_sid() s = self.session_class(sid, permanent=app.config["session_life_time"]) self.session_map[sid] = s return s else: return self.session_map[sid] def save_session(self, response, app, sess=session): sid = str_to_byte(sess.sid) sid = base64.b64encode(sid) max_age = session.get(‘permanent‘) or timedelta(days=31).total_seconds() response.set_cookie(app.config[‘SESSION_NAME‘], sid, max_age=max_age, httponly=self.is_http_only(app)) return response
对于sid,只是把它通过base64编码了下,不算加密,sid本身是uuid4。open_session函数在请求上下文初始化时使用,save_session则是在每次response返回时使用。在app初始化时初始这个接口类,接口类自带sid与session的dict()用于查询是否存在,不存在sid或者没有sid就重新生成一个。
钩子函数
app的两个函数list,一个在request处理函数前运行,一个对于每个response进行处理。会对他们遍历运行一遍,save_session就放在后者里面。
jinja2模板
def build_env(template_path=‘template‘, root_path=None): if root_path is None: template_path = find_folder(template_path) else: template_path = join(root_path, template_path) env = Environment(loader=FileSystemLoader(template_path), autoescape=guess_auto_escape, extensions=[‘jinja2.ext.autoescape‘]) env.globals.update( url_for=url_for, g=g, session=session, request=request, ) return env
通过jinja2的Environment类,指定模板文件夹。globals则是可以在html中直接访问的东西,默认有上下文和按照endpoint和参数生成相对路径的url_for
def rend_template(template_name, **context): t = current_app.jinja_env.get_template(template_name) return make_response(t.render(context), mimetype=‘text/html‘)
对于模板进行渲染。设置response的mimetype,flask在Response类中设置了,否则html代码会直接显示
Config
dict的子类,设置了一些默认值,只写了从另外一个类导入,和参数导入。应该说让我复习了一遍魔法方法。
缺少的部分
这个flame并不完善,一开始想写的,没有全部弄上去。数据库orm, test,log,参数验证,视图api之类等等等等,都可以拓展,比较可惜吧
我的一个小测试
写了一个小app来测试。登录和注册,测试了下session和模板引擎