(五)Web框架

这部分的内容比较琐碎,有几处没有看懂,先记下来,完成后再转回来看看。
更新app.py:

# -*- coding: utf-8 -*-
import logging; logging.basicConfig(level=logging.INFO)
import asyncio, os, json, time
from datetime import datetime
from aiohttp import web
import orm
from jinja2 import Environment, FileSystemLoader
from coroweb import add_routes, add_static

def init_jinja2(app, **kw):
    logging.info('Init jinja2...')
    options = dict(
        # 字典的get()方法返回指定键的值, 如果值不在字典中返回默认值
        autoescape = kw.get('autoescape', True),    # 自动转义
        block_start_string = kw.get('block_start_string', '%{'),
        block_end_string = kw.get('block_end_string','%}'),
        variable_start_string = kw.get('variable_start_string', '{{'),
        variable_end_string = kw.get('variable_end_string', '}}'),
        auto_reload = kw.get('auto_reload', True)    # 自动重新加载模板
    )
    path = kw.get('path', None)
    if path is None:
        # __file__获取当前执行脚本
        path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')
    logging.info('Set jinja2 template path: %s' % path)
    # Environment(loader=PackageLoader('path'), 其他高级参数...)
    # 创建一个默认设定下的模板环境和一个在path目录下寻找模板的加载器
    env = Environment(loader=FileSystemLoader(path), **options)
    filters = kw.get('filters', None)
    if filters is not None:
        for name, f in filters.items():
            env.filters[name] = f
    app['__templating__'] = env

# middlewar把通用的功能从每个URL处理函数中拿出来, 集中放到一个地方
# 接受一个app实例, 一个handler(URL处理函数, 如index), 并返回一个新的handler
async def logger_factory(app, handler):
    async def logger(request):
        logging.info('Request: %s %s' % (request.method, request.path))
        return (await handler(request))
    return logger

async def data_factory(app, handler):
    async def parse_data(request):
        if request.method == 'POST':
            if request.content_type.startswith('application/json'):
                request.__data__ = await request.json()
                logging.info('Request json: %s' % str(request.__data__))
            elif request.content_type.startswith('application/x-www-form-urlencoded'):
                request.__data__ = await request.post()
                logging.info('Request form: %s' % str(request.__data__))
        return (await handler(request))
    return parse_data

# 转化得到response对象的middleware
async def response_factory(app, handler):
    async def response(request):
        logging.info('Response handler...')
        r = await handler(request)
        # web.StreamResponse是HTTP响应处理的基类
        # 包含用于设置HTTP响应头,Cookie,响应状态码,写入HTTP响应BODY等的方法
        if isinstance(r, web.StreamResponse):
            return r
        if isinstance(r, bytes):
            # 转换为web.Response对象
            resp = web.Response(body=r)
            # .*( 二进制流,不知道下载文件类型)
            resp.content_type = 'application/octet-stream'
            return resp
        if isinstance(r, str):
            if r.startswith('redirect:'):
                return web.HTTPFound(r[9:])
            resp = web.Response(body=r.encode('utf-8'))
            # .html
            resp.content_type = 'text/html;charset=utf-8'
            return resp
        if isinstance(r, dict):
            template = r.get('__template__')
            if template is None:
                resp = web.Response(body=json.dumps(r, ensure_ascii=False, default=lambda o: o.__dict__).encode('utf-8'))
                # 序列化后的JSON字符串
                resp.content_type = 'application/json;charset=utf-8'
                return resp
            else:
                # 调用get_template()方法环境中加载模板,调用render()方法用若干变量来渲染它
                resp = web.Response(body=app['__templating__'].get_template(template).render(**r).encode('utf-8'))
                resp.content_type = 'text/html;charset=utf-8'
                return resp
        # HTTP状态码和响应头部
        if isinstance(r, int) and r >= 100 and r < 600:
            return web.Response(r)
        if isinstance(r, tuple) and len(r) == 3:
            t, m = r
            if isinstance(t, int) and t >= 100 and t < 600:
                return web.Response(t, str(m))
        resp = web.Response(body=str(r).encode('utf-8'))
        # .txt
        resp.content_type = 'text/plain;charset=utf-8'
        return resp
    return response

def datetime_filter(t):
    # time.time()返回当前时间的时间戳(1970纪元后经过的浮点秒数)
    delta = int(time.time() - t)
    if delta < 60:
        return u'1分钟前'
    if delta < 3600:
        return u'%s分钟前' % (delta // 60)
    if delta < 86400:
        return u'1小时前' % (delta // 3600)
    if delta < 604800:
        return u'%s天前' % (delta // 86400)
    # 把timestamp转换为datetime
    dt = datetime.fromtimestamp(t)
    return u'%s年%s月%s日' % (dt.year, dt.month, dt.day)

# 参数aiohttp.web.request实例,包含了所有浏览器发送过来的HTTP协议里面的信息
# def index(request):
#   return web.Response(body=b'

Day01 of My WebAPP

') # 构造一个HTTP响应 # 协程,不能直接运行,需要把协程加入到事件循环(loop), 由后者在适当的时候调用 async def init(loop): await orm.create_pool(loop=loop, host='192.168.179.140', port=3306, user='www', password='www', db='awesome') # 创建Web服务器,即aiohttp.web.Application类的实例,作用是处理URL、HTTP协议 # 添加middleware时自动变成倒序 app = web.Application(loop=loop, middlewares=[logger_factory, response_factory]) init_jinja2(app, filters=dict(datetime=datetime_filter)) # 没有参数t? # import handlers.py add_routes(app, 'handlers') add_static(app) # 用协程创建TCP服务(这里写的是我的虚拟机地址,为了本机也能访问) srv = await loop.create_server(app.make_handler(), '192.168.179.140', 9000) logging.info('Server started at http://192.168.179.140:9000...') return srv # 创建一个事件循环 loop = asyncio.get_event_loop() # 将协程注册到事件循环,并启动事件循环 loop.run_until_complete(init(loop)) # run_forever会一直运行,直到stop在协程中被调用 loop.run_forever()

新建apis.py:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

'''
JSON API defination
'''
import json, logging, inspect, functools

# Base APIError, 包含error(必需), data和message
class APIError(Exception):
    
    def __init__(self, error, data='', message=''):
        super(APIError, self).__init__(message)
        self.error = error
        self.data = data
        self.message = message

# 输入值有错或无效, data指定输入表单的错误字段。
class APIValueError(APIError):

    def __init__(self, field, message=''):
        super(APIValueError, self).__init__('value:invalid', field, message)

# 资源未找到, data指定资源名称。
class APIResourceNotFoundError(APIError):

    def __init__(self, field, message=''):
        super(APIResourceNotFoundError, self).__init__('vallue:notfound', field, message)

# API没有权限。
class APIPermissionError(APIError):

    def __init__(self, message=''):
        super(APIPermissionError, self).__init__('permission:forbidden', 'permission', message)

新建coroweb.py:

# -*- coding: utf-8 -*-

import asyncio, os, inspect, logging, functools
from urllib import parse
from aiohttp import web
from apis import APIError

# 装饰器, 在函数前面写上@get的话, 将执行装饰器返回的函数
# 作用是一个函数通过@get()的装饰就附带了URL信息
# 由于decorator本身需要传入参数, 需要编写一个返回decorator的高阶函数
def get(path):
    '''
    Define decorator @get('/path')
    '''
    def decorator(func):
        # @functools.wraps(func)用来把原始函数的__name__等属性复制到decorator()函数中
        @functools.wraps(func)
        # (*args, **kw)表示wrapper()函数可以接受任意参数的调用
        def wrapper(*args, **kw):
            return func(*args, **kw)
        wrapper.__method__ = 'GET'
        wrapper.__route__ = path
        return wrapper
    return decorator

def post(path):
    '''
    Define decorator @post('/path')
    '''
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            return func(*args, **kw)
        wrapper.__method__ = 'POST'
        wrapper.__route__ = path
        return wrapper
    return decorator

# 运用inspect模块,创建几个函数用以获取URL处理函数与request参数之间的关系
# 收集没有默认值的命名关键字参数
def get_required_kw_args(fn):
    args = []
    params = inspect.signature(fn).parameters
    # 如果纯使用for..in则只能取得每一对元素的key值
    for name, param in params.items():
        # 只能用关键字KEYWORD来传参,不可以用位置传参,因为位置参数全让前面的VAR_POSITIONAL类型参数接收完了
        if param.kind == inspect.Parameter.KEYWORD_ONLY and param.default == inspect.Parameter.empty:
            args.append(name)
        return tuple(args)

# 获取命名关键字参数
def get_named_kw_args(fn):
    args = []
    params = inspect.signature(fn).parameters
    for name, param in params.items():
        if param.kind == inspect.Parameter.KEYWORD_ONLY:
            args.append(name)
    return tuple(args)

# 判断有没有命名关键字参数
def has_named_kw_args(fn):
    params = inspect.signature(fn).parameters
    for name, param in params.items():
        if param.kind == inspect.Parameter.KEYWORD_ONLY:
            return True

# 判断有没有关键字参数
def has_var_kw_arg(fn):
    params = inspect.signature(fn).parameters
    for name, param in params.items():
        if param.kind == inspect.Parameter.VAR_KEYWORD:
            return True

# 判断是否含有名叫'request'参数,且该参数是否为最后一个参数
def has_request_arg(fn):
    sig = inspect.signature(fn)
    params = sig.parameters
    found = False
    for name, param in params.items():
        if name == 'request':
            found = True
            continue
        if found and (param.kind != inspect.Parameter.VAR_POSITIONAL and param.kind != inspect.Parameter.KEYWORD_ONLY and param.kind != inspect.Parameter.VAR_KEYWORD):
            raise ValueError('request parameter must be the last named parameter in function: %s%s' % (fn.__name__, str(sig)))
    return found

# 正式向request参数获取URL处理函数所需的参数
class RequestHandler(object):

    # 接受app参数
    def __init__(self, app, fn):
        self._app = app
        self._func = fn
        self._has_request_arg = has_request_arg(fn)
        self._has_var_kw_arg = has_var_kw_arg(fn)
        self._has_named_kw_args = has_named_kw_args(fn)
        self._named_kw_args = get_named_kw_args(fn)
        self._required_kw_args = get_required_kw_args(fn)

    async def __call__(self, request):    # request?
        kw = None
        if self._has_var_kw_arg or self._has_named_kw_args or self._required_kw_args:
            if request.method == 'POST':
                if not request.contnt_type:
                    return web.HTTPBadRequest(text='Missing Content-Type.')
                ct = request.content_type
                if ct.startswith('application/json'):
                    params = await request.json()    # .json()?
                    if not isinstance(params, dict):
                        return web.HTTPBadRequest(text='JSON body must be object.')
                    kw = params
                # 键值对或表单
                elif ct.startswith('application/x-www-form-urlencoded') or ct.startswith('multipart/form-data'):
                    params = await request.post()    # .post()?
                    # 内置函数dict(**kwarg)从一个字典参数构造一个新字典
                    kw = dict(**params)
                else:
                    return web.HTTPBadRequest(text='Unsupported Content-Type: %s'%request.content_type)
            if request.method == 'GET':
                qs = request.query_string
                if qs:
                    kw = dict()
                    # urllib.parse.parse_qs(qs, keep_blank_values, strict_parsing=False)
                    # 解析qs给出的查询字符串(例如'foo=bar&baz=qux'), 数据作为字典返回,保留空白字符串。
                    for k, v in parse.parse_qs(qs, True).items():
                        kw[k] = v[0]
        if kw is None:
            kw = dict(**request.match_info)
        else:
            if not self._has_var_kw_arg and self._named_kw_args:    # ?
                # 去掉不是命名关键字参数的参数
                copy = dict()
                for name in self._named_kw_args:
                    if name in kw:
                        copy[name] = kw[name]
                kw = copy
            # 检查命名关键字参数
            for k, v in request.match_info.items():    # ?
                if k in kw:
                    logging.warning('Duplicate arg name in named arg and kw args: %s' % k)
                kw[k] = v
        if self._has_request_arg:
            kw['request'] = request
        # 检查没有默认值的命名关键字参数
        if self._required_kw_args:
            for name in self._required_kw_args:
                if name not in kw:
                    return web.HTTPBadRequest(text='Missing argument: %s' % name)
        logging.info('Call with arg: %s' % str(kw))
        try:
            r = await self._func(**kw)
            return r
        except APIError as e:
            return dict(error=e.error, data=e.data, message=e.message)

# 添加静态资源路径
def add_static(app):
    # os.path.abspath()返回绝对路径, os.path.dirname()返回path的目录名, os.path.join()将多个路径组合
    path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
    # add_static(处理的静态资源的URL路径前缀, 文件系统中包含处理的静态资源的文件夹路径)
    app.router.add_static('/static/', path)
    logging.info('add static %s => %s' % ('/static/', path))

# 注册URL处理函数
def add_route(app, fn):
    # getattr(self, key, default值)
    method = getattr(fn, '__method__', None)
    path = getattr(fn, '__route__', None)
    if path is None or method is None:
        raise ValueError('@get or @post not defined in %s.' % str(fn))
    if not asynico.iscoroutinefunction(fn) and not inspect.isgeneratorfunction(fn):
        fn = asyncio.coroutine(fn)
    # inspect.signature(fn)将返回一个inspect.Signature类型的对象, 值为fn这个函数的所有参数
    # inspect.Signature对象的paramerters属性是一个mappingproxy(映射)类型的对象,值为一个有序字典(Orderdict)。
    # 这个字典里的key即为参数名,str类型; value是一个inspect.Parameter类型的对象,包含的一个参数的各种信息
    logging.info('add route %s %s => %s(%s)' % (method, path, fn.__name__, ', '.join(inspect.signature(fn).parameters.keys())))
    app.router.add_route(method, path, RequestHandler(app, fn))

def add_routes(app, module_name):
    # rfind(字符串)返回字符串最后一次出现的位置, 如果没有匹配项则返回-1
    n = method_name.rfind('.')
    if n == -1:
        # __import__(name[, globals[, locals[, fromlist[, level]]]])函数用于动态加载类和函数
        # globals()返回全局变量的字典, locals()返回当前局部变量的深拷贝(新建对象,不改变原值)
        mod = __import__(module_name, globals(), locals())
    else:
        name = module_name[n+1:]
        mod = getattr(__import__(module_name[:n], globals(), locals(), [name]), name)
    # dir()函数可以查看对象内所有属性及方法
    for attr in dir(mod):
        if attr.startswith('_'):
            continue
        fn = getattr(mod, attr)
        if callable(fn):
            method = getattr(fn, '__method__', None)
            path = getattr(fn, '__route__', None)
            if method and path:
                add_route(app, fn)

你可能感兴趣的:((五)Web框架)