一万英尺高度下的web框架-digwebs

Python Web框架【digwebs】

原文链接

在之前的文章中,我们已经使用了digwebs来快速实现一个简单的网页服务。一开始我们都会借助别人的框架来完成某种服务,通过这种方式,我们可以在一个大的应用场景下认识这些框架,但是要想在编程方面有本质的提升,就必须剖析优秀框架的源码,并且学习其中的设计理念。因此,为了能够更好的使用digwebs框架,接下来的内容将剖析它的每一个组件以及组件之间的关系。

Digwebs是一个开源的web框架,它是由Python来开发的,并且托管在这里。digwebs的源代码很少,而且容易阅读,此外,在这个系列的文章中也会一点点剖析这个框架的设计思想。读者可以通过学习这个系列的知识来为后续使用Web框架打下良好的基础。运行在digwebs之上的web应用有 www.digolds.cn

在这篇文章中,我将带领大家从整体上来认识构成digwebs的组件,以及这些组件之间的关系和作用。接下来让我们从以下这张图开始来认识digwebs中的组件。

上图揭示了digwebs主要的组件,每一个组件都由一个Python文件定义,每一个Python文件中都会提供类和函数来完成各自的功能。

在上图中,web.py封装了所有其它灰色部分模块,同时提供了对外的接口。记住,在开始研究digwebs时,web.py是最先开始的地方。在剖析一个框架的时候,你不仅要了解构成这个框架的组件以及它们之间的关系,此外你还得了解这个框架的处理流程。

digwebs的处理流程是这样的:digwebs启动,此时web.py会监听外部事件,当外部事件发生时,web.py会解析外部事件发生时的参数,request.py会建立一个实例来承载这些参数,紧接着router.py会根据这个实例来选择处理路径,每个处理路径是由使用digwebs的人来定义的,最后,response.py会根据处理路径返回结果构建一个实例返回给事件发生的地方。

除了以上提及的模块之外,还有一些模块apis.pyerrors.pycommon.pytemplate.py也提供了辅助的功能,比如apis.py定义了一些与restful api相关的方法与类。这些模块起到了辅助作用,是基础模块。

注意,digwebs启动的时候,router.pytemplate.py会率先初始化,作为一个全局变量,而request.pyresponse.py每次处理事件的时候都会重新实例化,作为一个局部变量。

以上通过静态逻辑和动态逻辑从全局上揭示了digwebs,为了更好的使用digwebs,接下来让我们看看每一个组件的作用以及内部一些具体的定义。

web.py

这个组件是使用digwebs框架的入口,其中定义了3个关键的元素,它们分别是digwebs全局变量current_appctx
全局变量current_appdigwebs的一个实例,而且每一个web应用都应该只有一个全局变量current_app。当初始化全局变量current_app之后,你可以通过全局变量current_app来使用digwebs所定义的方法以及一些装饰器。

为了使用digwebs,你需要在一个py文件里写下这些指令,其中digwebs_app就是current_appdigwebs_app.init_all()的作用是初始化之前提到的模块router.pytemplate.py

from digwebs.web import get_app
dir_path = os.path.dirname(os.path.realpath(__file__))
digwebs_app = get_app({'root_path':dir_path})
digwebs_app.init_all()
digwebs_app.run(9999, host='0.0.0.0')

为了配合上面的指令,你需要在另外一个py文件中定义路由事件,并且将这个文件放到目录controllers中。以下例子说明了如何通过全局变量current_app来使用digwebs所定义的装饰器。

from digwebs.web import current_app

@current_app.get('/signout')
def signout():
    ctx.response.delete_cookie(configs.session.name)
    raise seeother('/')

其中@current_app.get('/signout')就是一个装饰器,这个装饰器的作用是将函数signoutdigwebs建立绑定。触发这个绑定的指令是digwebs_app.init_all(),这句指令会在controllers中去查找所有py文件,然后针对每一个文件查找带有类似@current_app.get('/signout')指令修饰的函数,然后建立一个索引表,而digwebs就是根据这个索引表来完成路由选择的。digwebs里除了定义了get装饰器,还有其它类型的装饰器,它们有各自的功能,这些装饰器有viewpostdeleteputapi。比如以下例子组合使用了这些装饰器。

下面这段代码定义了一个blogs.html页面,同时绑定了函数list_blogs。它们一起动态生成了一个html页面,这个页面以列表的形式展示了每一篇文章的概要信息,而这些概要信息由list_blogs生成。注意blogs.html页面决定了展示内容的形式,而list_blogs决定了提供的内容。

@current_app.view('blogs.html')
@current_app.get('/views/blogs')
def list_blogs():
	blogs = []
	blogs.append({
	'title':'What is digwebs',
	'description':'A tiny web framework called digwebs which is developed by Python.',
	'detail_link':'######'})
	blogs.append({
	'title':'Why you should use digwebs',
	'description':'Digwebs is a Python web framework, which you can use to accelerate the development process of building a web service.',
	'detail_link':'######'})
	blogs.append({'title':'How to use digolds web framework','description':'You can use digwebs in a few steps. First pull the source code. Second install jinja2. Finally run python .\digwebs\project_generator.py to generate the project file structure.','detail_link':'######'})
	return dict(template_blogs=blogs)

以上代码片段揭示了装饰器view的作用:生成html页面(这个页面中包含了数据部分),并且返回给浏览器。如果有另外一个网站,它想以卡片的形式展示文章,那么它自然希望你的系统能提供文章数据(除去HTML那部分)。此时你需要使用装饰器api来帮助你完成这个功能。以下代码片段和上面的代码片段唯一不同的地方在于@current_app.api。这个装饰器的作用是:让函数返回json格式的数据(不包含HTML部分)。

@current_app.api
@current_app.get('/views/blogs')
def list_blogs():
	blogs = []
	blogs.append({
	'title':'What is digwebs',
	'description':'A tiny web framework called digwebs which is developed by Python.',
	'detail_link':'######'})
	blogs.append({
	'title':'Why you should use digwebs',
	'description':'Digwebs is a Python web framework, which you can use to accelerate the development process of building a web service.',
	'detail_link':'######'})
	blogs.append({'title':'How to use digolds web framework','description':'You can use digwebs in a few steps. First pull the source code. Second install jinja2. Finally run python .\digwebs\project_generator.py to generate the project file structure.','detail_link':'######'})
	return dict(template_blogs=blogs)

web.py中还有一个全局变量ctx,需要了解。ctx是一个类型为threading.local的变量,它是每次请求的数据上下文,每一次请求的数据都会存储在ctx上,不同请求的数据之间是独立的,互不影响。只有ctx是无法实现不同请求的数据之间的独立性的,还需借助gunicorn来实现,gunicorn是一个支持wsgi协议的容器,具体内容可以查看它的官网

router.py

这个组件提供了路由挂接和路由选择的功能,路由挂接是在digwebs启动的时候完成,而路由选择是在digwebs启动后并且处理请求时发生的。让我们来举一个例子来理解这2个功能。

假设你正在使用digwebs作为web框架设计一个web app,你为这个web app定义了2个路由处理,它们的事例代码如下所示:

@current_app.get('/')
def home():
    return dict()
		
@current_app.get('/about')
def about():
    return dict()

以上代码定义了2个路由,它们分别是//about,每个路由都绑定了对应的处理函数,它们分别是homeabout。digwebs在启动的时候,会自动查找这2个路由,同时将这个两个路由与对应的处理函数绑定起来,这个称为路由挂接。当浏览器向服务器发送这2个请求request('/')request('/about'),那么router.py会根据/选择home来处理请求request('/'),会根据/about选择about来处理请求request('/about'),而这个过程称为路由选择

router.py中你会看到2个关键的RouterRouteRouter管理许多Route,每一个Route实例就代表了一个路由。Router将路由的类型分成2类:动态路由和静态路由,分别由Python的字典类型dynamic_method_to_routestatic_method_to_route来表示。此外每种路由的调用方式可以是:GETPOSTDELETEPUT。因此每一个路由都必须说明调用方式,下图就是RouterRoute建立的一个索引关系表。

静态路由是形式如下:

@current_app.get('/')
def home():
    return dict()
		
@current_app.get('/about')
def about():
    return dict()

而动态路由的形式如下(注意@current_app.get('/u/:user_id')中的:user_id):

@current_app.view('user_profile.html')
@current_app.get('/u/:user_id')
def get_user_profile(user_id):
    u = User.get(user_id)
    for k,v in UserRole.items():
        if v == u.role:
            u['role'] = k
            break
    uies = []
    if u:
        uies = get_user_infos_by(u.id)
    return dict(other_user=u,user_infoes=uies)

也就是说,对于以上的动态路由,/u/123456/u/111111都会映射到函数get_user_profile(user_id)。因此你会发现动态路由最终是放在一个数组里。

为了让web.py选择路由Route,那么需要在web.py中初始化Router,并且通过Router所提供的函数create_controller来建立这个路由表。一旦这个路由表建立了,那么web.py就可以通过Router来选择Route。以下就是完成这一过程的代码片段。

class digwebs(object):
    def __init__(
        self,
        root_path = None,
        template_folder = 'views',
        middlewares_folder= 'middlewares',
        controller_folder = 'controllers',
        is_develop_mode = True):
        '''
        Init a digwebs.

        Args:
          root_path: root path.
        '''

        self.root_path = root_path if root_path else os.path.abspath(os.path.dirname(sys.argv[0]))
        self.middleware = []
        self.template_folder = template_folder
        self.middlewares_folder = middlewares_folder
        self.controller_folder = controller_folder
        self.is_develop_mode = is_develop_mode
        self.template_callbacks = set()
        self.router = None
    
    def init_all(self):
        if self.template_folder:
            self._init_template_engine(os.path.join(self.root_path, self.template_folder))
        
        self.router = Router(self.is_develop_mode)
        self.middleware.append(self.router.create_controller(self.root_path,self.controller_folder,))
        if self.middlewares_folder:
            self._init_middlewares(os.path.join(self.root_path, self.middlewares_folder))

重点关注以上代码的这2句指令:

self.router = Router(self.is_develop_mode)
self.middleware.append(self.router.create_controller(self.root_path,self.controller_folder,))

template.py

在之前的事例代码中我们定义了页面blogs.html,这个文件中的内容如下所示:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>digwebs</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="keywords" content="python web framework, digwebs web framework,open source web framework,write web service by digwebs"/>
    <meta name="description" content="A tiny web framework called digwebs which is developed by Python."/>
</head>
<body>
    <div id="main" class='uk-container uk-container-small uk-padding'>
    <div class="uk-child-width-1-1 uk-grid-small uk-grid-match" uk-grid>
	    {% for a in template_blogs %}
        <div>
            <div class="uk-card uk-card-default uk-card-body">
                <h3 class="uk-card-title">{{{{a.title}}}}</h3>
                <p>{{{{a.description}}}}</p>
                <a href="{{{{a.detail_link}}}}" class="uk-text-uppercase">Read articles ...</a>
            </div>
        </div>
		{% endfor %}
    </div>
</div>
    {% set static_file_prefix = 'https://cdn.jsdelivr.net/gh/digolds/digresources/' %}
    <link rel="stylesheet" href="{{{{static_file_prefix}}}}/css/uikit.min.css">
    <script src="{{{{static_file_prefix}}}}/js/jquery.min.js"></script>
    <script src="{{{{static_file_prefix}}}}/js/uikit.min.js"></script>
    <script src="{{{{static_file_prefix}}}}/js/uikit-icons.min.js"></script>
</body>

</html>

请注意带有花括号的语句,比如{{{{static_file_prefix}}}}{% for a in template_blogs %}。这些语句不是html指令,因此需要将这些语句转化成html指令。template.py就是做这件事情的。下图为template.py完成这一转化的示意图。

通过上图可知,template.py依赖jinja(它是一个Python库,专门用于合并html和dict数据,最终生成完整的html页面)。接下来我们来分析template.py中的关键元素。

Jinja2TemplateEngine定义在template.py中,在它的构造函数__init__中通过以下指令引入了jinja。

from jinja2 import Environment, FileSystemLoader

这个类重载了函数__call__,这个函数就是用来执行上图的流程的。其中path是html页面的路径,比如/templates/blogs.html。model是Python中的数据,类型为dict,比如之前提到的return dict(template_blogs=blogs)

def __call__(self, path, model):
    return self._env.get_template(path).render(**model).encode('utf-8')

当执行以上函数__call__之后,返回一个html字符串,这个字符串已经将{{{{}}}}之类的指令替换掉,形成一个完整的html字符串。而我们经常看到的类似{{{{}}}}之类的指令就是jinja的语法规则。

有了以上概念之后,我们接下来看看template.py是如何被调用的。打开web.py文件,你会发现以下代码创建了一个Jinja2TemplateEngine实例,该实例的名字叫template_engine:

def _init_template_engine(self,template_path):
    self.template_engine = Jinja2TemplateEngine(template_path)

创建实例之后,还需要调用__call__函数,而以下代码就是调用之处,其中要特别注意这句指令self.template_engine(r.template_name, r.model),这句指令实质上就是调用了函数__call__

def wsgi(env, start_response):
    ctx.application = _application
    ctx.request = Request(env)
    response = ctx.response = Response()
    try:
         r = fn_exec(ctx, None)
         if isinstance(r, Template):
            tmp = []
            for cbf in self.template_callbacks:
                  r.model.update(cbf())
                  r.model['ctx'] = ctx
                  tmp.append(self.template_engine(r.template_name, r.model))
                  r = tmp
	except RedirectError as e:
           response.set_header('Location', e.location)
           start_response(e.status, response.headers)
           return []

以上代码还有一处if isinstance(r, Template)是需要留意的,为什么r的类型是Template?其原因在于以下代码片段:

    def view(self, path):
        '''
        A view decorator that render a view by dict.

        >>> @view('test/view.html')
        ... def hello():
        ...     return dict(name='Bob')
        >>> t = hello()
        >>> isinstance(t, Template)
        True
        >>> t.template_name
        'test/view.html'
        >>> @view('test/view.html')
        ... def hello2():
        ...     return ['a list']
        >>> t = hello2()
        Traceback (most recent call last):
        ...
        ValueError: Expect return a dict when using @view() decorator.
        '''

        def _decorator(func):
            @functools.wraps(func)
            def _wrapper(*args, **kw):
                r = func(*args, **kw)
                if isinstance(r, dict):
                    logging.info('return Template')
                    return Template(path, **r)
                raise ValueError(
                    'Expect return a dict when using @view() decorator.')

            return _wrapper

        return _decorator
@current_app.view('blogs.html')
@current_app.get('/views/blogs')
def list_blogs():
	blogs = []
	blogs.append({
	'title':'What is digwebs',
	'description':'A tiny web framework called digwebs which is developed by Python.',
	'detail_link':'######'})
	blogs.append({
	'title':'Why you should use digwebs',
	'description':'Digwebs is a Python web framework, which you can use to accelerate the development process of building a web service.',
	'detail_link':'######'})
	blogs.append({'title':'How to use digolds web framework','description':'You can use digwebs in a few steps. First pull the source code. Second install jinja2. Finally run python .\digwebs\project_generator.py to generate the project file structure.','detail_link':'######'})
	return dict(template_blogs=blogs)

其中@current_app.view('blogs.html')调用了函数view,它是一个装饰器,而函数view在digwebs中定义,返回的对象是Template实例,current_app其实是digwebs的一个实例。

request.py

我们之前经常提到,digwebs会收到来自外部的请求,然后根据请求来选择不同的路由,此外同一个路由接收到的参数会因为用户的不同而不同。为了能够高效的使用提取并使用这些参数,那么需要通过request.py来协助我们。接下来我们看看在request.py中哪些元素承担了重要角色。

该文件中定义了一个类Request,该类提供了解析请求参数和提取参数的相应函数。其中最需要知道的两个函数分别是_parse_inputget,它们的定义如下:

解析请求参数的代码

    def _parse_input(self):
        def _convert(item):
            if isinstance(item, list):
                return [to_str(i.value) for i in item]
            if item.filename:
                return MultipartFile(item)
            return to_str(item.value)
        fs = CustomFieldStorage(fp=self._environ['wsgi.input'], environ=self._environ, keep_blank_values=True)
        received_data = fs.value
        if isinstance(received_data,list):
            inputs = dict()
            for key in fs:
                inputs[key] = _convert(fs[key])
        else:
            raise ValueError('unknown received data type')
        return inputs

提取请求参数的代码

    def get(self, key, default=None):
        '''
        The same as request[key], but return default value if key is not found.

        >>> from StringIO import StringIO
        >>> r = Request({'REQUEST_METHOD':'POST', 'wsgi.input':StringIO('a=1&b=M%20M&c=ABC&c=XYZ&e=')})
        >>> r.get('a')
        u'1'
        >>> r.get('empty')
        >>> r.get('empty', 'DEFAULT')
        'DEFAULT'
        '''
        r = self._get_raw_input().get(key, default)
        if isinstance(r, list):
            return r[0]
        return r

一般情况下,解析请求参数的函数_parse_input会自动被提取参数函数get调用,因此我们只需要直接使用get函数就能够提取我们想要的参数。接下来,让我们看看request.py是如何被集成到web.py中的。打开web.py文件,你会发现以下代码片段:

def wsgi(env, start_response):
    ctx.application = _application
    ctx.request = Request(env)

该片段实例化了Request,变量存储在ctx上,叫request。此外该片段还说明了一件事,每次请求都会重新实例化Request,而且之前的request与下一次的request都是不同的,通过这一点我们就可以将每次请求都独立开来。当ctx中存有变量request后,那么后续的路由处理过程中就可以通过ctx来取得request,并且通过request提取到当前请求的参数。

response.py

当digwebs接收到外部请求,并且处理该请求,最终得到一个结果,这个结果会返回给谁呢?一般情况下,浏览器可以发送请求,而且浏览器期望返回的结果是html页面,有时也期望是一个文件。除此之外,第三方应用发送请求,并期望通过digwebs返回json格式的数据。由此可以看出,digwebs可以返回多种格式的结果,为了区分这些格式,模块response.py定义了一个类Response,这个类记录了返回数据的格式,数据长度以及状态码。有了这些记录返回数据的元数据,那么digwebs就可以统一地将这些元数据返回给发起请求方,比如浏览器,第三方应用。

与类Request一样,类Response也是在每一次请求处理的过程中实例化的。我们需要关注这个类中所定义的2个成员变量,它们分别是self._statusself._headers = {'CONTENT-TYPE': 'text/html; charset=utf-8'}

_status这个变量记录了返回的状态码,这些状态码定义在response_code.py里。这些状态码的定义如下:

# all known response statues:
RESPONSE_STATUSES = {
    # Informational
    100: 'Continue',
    101: 'Switching Protocols',
    102: 'Processing',

    # Successful
    200: 'OK',
    201: 'Created',
    202: 'Accepted',
    203: 'Non-Authoritative Information',
    204: 'No Content',
    205: 'Reset Content',
    206: 'Partial Content',
    207: 'Multi Status',
    226: 'IM Used',

    # Redirection
    300: 'Multiple Choices',
    301: 'Moved Permanently',
    302: 'Found',
    303: 'See Other',
    304: 'Not Modified',
    305: 'Use Proxy',
    307: 'Temporary Redirect',

    # Client Error
    400: 'Bad Request',
    401: 'Unauthorized',
    402: 'Payment Required',
    403: 'Forbidden',
    404: 'Not Found',
    405: 'Method Not Allowed',
    406: 'Not Acceptable',
    407: 'Proxy Authentication Required',
    408: 'Request Timeout',
    409: 'Conflict',
    410: 'Gone',
    411: 'Length Required',
    412: 'Precondition Failed',
    413: 'Request Entity Too Large',
    414: 'Request URI Too Long',
    415: 'Unsupported Media Type',
    416: 'Requested Range Not Satisfiable',
    417: 'Expectation Failed',
    418: "I'm a teapot",
    422: 'Unprocessable Entity',
    423: 'Locked',
    424: 'Failed Dependency',
    426: 'Upgrade Required',

    # Server Error
    500: 'Internal Server Error',
    501: 'Not Implemented',
    502: 'Bad Gateway',
    503: 'Service Unavailable',
    504: 'Gateway Timeout',
    505: 'HTTP Version Not Supported',
    507: 'Insufficient Storage',
    510: 'Not Extended',
}

_headers这个变量是用来记录数据格式、数据长度、数据编码等信息的。

前面我们提到过,类Response是在每一次请求的过程中实例化的,而这个实例化发生的过程体现在web.pyresponse = ctx.response = Response()

def wsgi(env, start_response):
    response = ctx.response = Response()
	  start_response(response.status, response.headers)
	  return r

由此可以看到,使用者只需要调用ctx.response来记录_status_header信息,然后再调用start_response函数将这些信息返回给发起请求的源头,此时这些源头就知道所传输的数据是什么格式以及该数据的长度。只有知道这些信息,这些源头才能选择相应的策略来解析数据。上面代码还有一部分return r需要留意,其中的r就是数据部分,这部分数据和response中的数据共同返回给发起请求方,使得它能够拿到数据以及数据的格式,选择相应的解析策略来解析数据。

apis.pyerrors.pycommon.py

之前介绍了digwebs的关键构成组件,那些组件是需要深入了解的,而这一节中的组件只需要大概了解。

apis.py是一个定义了与restful相关的类,这些类主要代表了某类错误,比如APIPermissionError就代表了权限相关的错误。在这个文件中定义的类一般只会用在@current_app.api所修饰的路由中。

errors.py中也定义了很多类,与apis.py类似,这些类代表了某种类型的错误,它们一般只会用在@current_app.view所修饰的路由中。

common.py中定义了常用的工具函数,这些函数经常被digwebs的很多地方使用,因此会将这些常用的函数提取到这个模块中。这种方法是模块复用的一个简单的事例。

总结

当你阅读到这里之后,相信你已经迷惑了,那么不妨通过以下图片再来简单回顾一下digwebs的2个过程启动过程处理请求的过程

digwebs启动过程

1.获取全局对象digwebs_app

from digwebs.web import get_app
dir_path = os.path.dirname(os.path.realpath(__file__))
digwebs_app = get_app({'root_path':dir_path})

2.初始化digwebs_app所有依赖的组件,这些组件有router.pytemplate.py。其中router.py的主要任务是建立路由表,而template.py的主要任务是启动渲染html页面的实例。

digwebs_app.init_all()

3.启动digwebs_app

digwebs_app.run(9999, host='0.0.0.0')

digwebs处理请求的过程

1.digwebs接收到外部的请求,接收的请求入口在web.py

def wsgi(env, start_response)

2.接收到请求之后,实例化request.pyresponse.py中的类,其中Request负责解析接收到的数据,Response负责返回处理结果

ctx.request = Request(env)
response = ctx.response = Response()

3.根据ctx.request的内容,由route.py中的Router来选择路由

                def dispatch(i):
                    fn = self.middleware[i][0]
                    if i == len(self.middleware):
                        fn = next
                    return fn(context, lambda: dispatch(i + 1))

4.将处理的结果返回给请求发起方

start_response(response.status, response.headers)
return r
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值