基于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和模板引擎

相关推荐