python 构建一个简单的web框架(使用werkzeug)

Werkzeug版本:1.0.2

 

Werkzeug功能特性(转自百度百科):

  1. HTTP头解析与封装
  2. 易于使用的request和response对象
  3. 基于浏览器的交互式JavaScript调试器
  4. 与 WSGI 1.0 规范100%兼容
  5. 支持Python 2.6,Python 2.7和Python3.3
  6. 支持Unicode
  7. 支持基本的会话管理及签名Cookie
  8. 支持URI和IRI的Unicode使用工具
  9. 内置支持兼容各种浏览器和WSGI服务器的实用工具
  10. 集成URL请求路由系统

 

实现功能:

  1. 创建一个简单的web服务器
  2. 添加模板功能
  3. 模仿(copy)flask的修饰器构建路由
  4. redis模块
  5. session模块

 

一、创建web服务器

  构建Web类,方便参数或函数的调用,后面函数的构建大部分都是此类的子函数  (werkzeug.wrappers->Request, Response)


from werkzeug.wrappers import Request
from werkzeug.wrappers import Response

class Web:
    def __init__(self, config):
        # 储存配置,保留
        pass
    def wsgi_app(self, environ, start_response):
     # 返回响应
        request = Request(environ)
        response = Response("Hello %s!" % request.args.get('name', 'World!'))
    def __call__(self, environ, start_response):    
        # 将Web类变成一个可调用对象
        return self.wsgi_app(environ, start_response)  

  定义创建对象的函数

def create_app(config):
    # 创建app对象
    app = Web(config)
    return app

  

二、模板功能

  为web添加templates地址并使用模板引擎jinja2  (jinja2->Environment, FileSustemLoader)  (werkzeug.urls->url_parse)

from jinja2 import Environment
from jinja2 import FileSystemLoader

from werkzeug.urls import url_parse
def get_hostname(url):
   # url解析
return url_parse(url).netloc class Web: def __init__(self, config):
     # template路径 template_path
= os.path.join(os.path.dirname(__file__), "templates")
     # 配置jinja2环境 self.jinja_env
= Environment( loader=FileSystemLoader(template_path), autoescape=True ) self.jinja_env.filters["hostname"] = get_hostname

  加载静态资源文件  (werkzeug.middleware.shared_data->SharedDataMiddleware)

from werkzeug.middleware.shared_data import SharedDataMiddleware
def
create_app(config, with_static=True): app.Web(config) # 加载静态资源文件 if with_static: app.wsgi_app = SharedDataMiddleware( app.wsgi_app, {"/static": os.path.join(os.path.dirname(__file__), "static")} ) return app

  添加模板渲染函数

class Web:
    ...
    def render_template(self, template_name, **context):
        # 渲染模板
        t = self.jinja_env.get_template(template_name)
        return Response(t.render(context), mimetype="text/html")    

  

三、构建路由

  为Web类添加路由空间和规则   (werkzeug.routing->Map, Rule)

from werkzeug.routing import Map
from werkzeug.routing import Rule
class Web:
    url_rule_class = Rule
    def __init__(self, config):
        ...
     # 路由空间

     self.url_map = Map([])
     # 存放自定义的函数
     self.view_functions = {}

  仿照flask构建路由规则生成函数(参考:https://www.cnblogs.com/eric_yi/p/8325044.html


def _endpoint_from_view_func(view_func):
  assert view_func is not None, "expected view func if endpoint is not provided."
  return view_func.__name__


class
Web: ...
    def add_url_rule(self, rule, endpoint=None, view_func=None, **options):   
if endpoint is None:    # 如果没有提供endpoint参数,则默认用view_func的名字   endpoint = _endpoint_from_view_func(view_func) # 把endpoint参数添加到options里面 options['endpoint'] = endpoint # 从options中pop出methods参数,并把值赋给methods变量,如果没有则置为None methods = options.pop('methods', None) # moehods的值为None的情况下 if methods is None: # 如果view_func函数中有这个methods参数,则使用view_func中的。如果没有则赋一个列表('GET',)给methods methods = getattr(view_func, 'methods', None) or ('GET',) # 如果methods是字符串类型 if isinstance(methods, string_types): # 抛出一个异常:methods需要是一个可以迭代的字符串 raise TypeError('methods需要是一个可以迭代的字符串, ' '例: @app.route(..., methods=["POST"])') # 把methods里面的item都改成大写 methods = set(item.upper() for item in methods) # 在view_func里面定义了一个属性required_methods = () # 作用:用来定义一些必须的方法,配合provide_automatic_options使用 required_methods = set(getattr(view_func, 'required_methods', ())) provide_automatic_options = getattr(view_func, 'provide_automatic_options', None) # 判断provide_automati_options是否为None if provide_automatic_options is None: # 如果OPTIONS字符串没有在methods里面 if 'OPTIONS' not in methods: # 则把provude_automatic_options改为True,并把OPTIONS添加到required_methods里面 provide_automatic_options = True required_methods.add('OPTIONS') # 如果OPTIONS在methods里面,则把provide_automatic_options设置为False else: provide_automatic_options = False # 合并required_methods和methods这两个集合到methods里面 methods |= required_methods # 创建路由规则 # 调用url_rule_class方法,由于在Flask类的全局变量中定义了:url_rule_class = Rule, Rule是werkzeug/routing.py里面的一个类 # 也就是相当于实例化了Rule得到了rule对象,具体实例化后的结果请看Rule源码分析 rule = self.url_rule_class(rule, methods=methods, **options) # 把provide_automatic_options属性添加到rule对象里面 rule.provide_automatic_options = provide_automatic_options # 在Flask类的__init__里面定义了self.url_map = Map(),Map是werkzeug/routing.py里面的一个类 # self.url_map相当与实例化了Map,.add则是调用了Map类里面的add方法 # 具体运行结果,请参考Map源码分析,以及Map源码中的add方法分析 self.url_map.add(rule) # 如果提供了view_func if view_func is not None: # 在flask类的__init__里面定义了self.view_functions = {}, # 从字典里面取endpoint值并赋值为old_func,(endpoint是传递的参数,默认为视图函数名) old_func = self.view_functions.get(endpoint) # 如果old_func有值,并且不等于view_func if old_func is not None and old_func != view_func: # 则抛出异常:视图函数映射被一个已经存在的函数名重写了 # 也就是说已经存在了一个endpoint:old_func的映射,但是old_fun却不是view_func,也就是说endpoint重复了 raise AssertionError('视图函数映射被一个已经存在的函数名重写了:' ' %s' % endpoint) # 添加视图函数与endpoint映射到view_functions字典里面 self.view_functions[endpoint] = view_func

  构建路由的修饰器函数(route)

class Web:
    ...
    def route(self, rule, **options):
        def decorator(f):  
            endpoint = options.pop("endpoint", None)
       # 调用上面的创建函数 self.add_url_rule(rule, endpoint, f,
**options) return f return decorator

  调用生成的函数   (werkzeug.exceptions->HTTPException, NotFound)

class Web:
    ...
    def dispatch_request(self, request):
        adapter = self.url_map.bind_to_environ(request.environ)
        try:
            endpoint, values = adapter.match()
            response = self.view_functions.get(f"{endpoint}")(request, **values)
            if isinstance(response, str):
                # 字符串型的数据单独处理(直接输出)
                response = Response(response)
            return response
        except NotFound:
            # 404错误处理
            return self.error_404()
        except HTTPException as e:
            # 其他错误处理(不只有500,但模板只做了500)
            print(e)
            return self.error_500()             

  错误处理

class Web:
    ...
   # 渲染相应的错误模板并返回
def error_500(self): response = self.render_template("500.html") response.status_code = 500 return response def error_404(self): response = self.render_template("404.html") response.status_code = 404 return response

四、redis

  配置redis  (redis)

import redis
class
Web: def __init__(self, config, session_path=".session\\"): ... # 配置redis self.redis = redis.Redis(config.get("redis_host", "localhost"), config.get("redis_port", 6379))

五、session

  session模块(不太完善,暂时只能以数字作为session的键)  (base64, time, os, json)

import base64
import time
import os
import json

def get_session_id(request):
   # 通过id获取session
return request.cookies.get('session_id', '') class Session: __instance = None def __init__(self):
     # 初始化,创建session路由空间 self.
__session_map__ = {} self.__storage_path__ = None def set_storage_path(self, path):
     # 设置仓库地址 self.
__storage_path__ = path def storage(self, session_id):
     # 进行各种session操作(读写) session_path
= os.path.join(self.__storage_path__, session_id) if self.__storage_path__ is not None: with open(session_path, 'wb') as f: content = json.dumps(self.__session_map__[session_id]) f.write(base64.encodebytes(content.encode())) def __new__(cls, *args, **kwargs):
     # 返回创建的实例 
if cls.__instance is None: cls.__instance = super(Session, cls).__new__(cls, *args, **kwargs) return cls.__instance def push(self, request, item, value):
     # 添加session session_id
= get_session_id(request) if session in self.__session_map__: self.__session_map__[get_session_id(request)][item] = value else: self.__session_map__[session_id] = {} self.__session_map__[session_id][item] = value self.storage(session_id) def pop(self, request, item, value=True):
     # 删除session session_id
= get_session_id(request) current_session = self.__session_map__.get(get_session_id(request), {}) if item in current_session: current_session.pop(item, value) self.storage(session_id) def load_local_session(self):
     # 载入本地的session(创建app时使用)
if self.__storage_path__ is not None: session_path_list = os.listdir(self.__storage_path__) for session_id in session_path_list: path = os.path.join(self.__storage_path__, session_id) with open(path, 'rb') as f: content = f.read() content = base64.decodebytes(content) self.__session_map__[session_id] = json.loads(content.decode()) def map(self, request):       return self.__session_map__.get(get_session_id(request), {}) def get(self, request, item):
     # 获得session
return self.__session_map__.get(get_session_id(request), {}).get(item, None) session = Session()

 

  配置session

class Web:    
    def __init__(self, config, session_path=".session\\"):
  ...
  # session存放地址 self.session_path
= session_path   ...   def create_app(config, with_static=True):   ...   if not os.path.exists(app.session_path):    os.mkdir(app.session_path)
    # 设置地址   session.set_storage_path(app.session_path)
    # 加载以前的session   session.load_local_session()   
return app

  cookie保存

class Web:
    ...    
    def wsgi_app(self, environ, start_response):
        ...import hashlib
     # 根据ip加密后的字符串,通过cookies的形式传递给用户来识别用户身份(有缺陷,需要完善) m
= hashlib.md5(request.remote_addr.encode()) # 转成二进制,md5加密 value = m.hexdigest() response.set_cookie(key='session_id', value=value) return response(environ, start_response)

新手上路,还不太熟练。。

项目地址:https://github.com/1224667889/werkzeug_web.git

posted @ 2020-05-23 19:47  mirrorlied  阅读(634)  评论(0)    收藏  举报