1. 程式人生 > >python學習——編寫web框架

python學習——編寫web框架

在正式開始Web開發前,我們需要編寫一個Web框架。

aiohttp已經是一個Web框架了,為什麼我們還需要自己封裝一個?

原因是從使用者的角度來說,aiohttp相對比較底層,編寫一個URL的處理函式需要這麼幾步:

第一步,編寫一個用@asyncio.coroutine裝飾的函式:

@asyncio.coroutine
def handle_url_xxx(request):
    pass

第二步,傳入的引數需要自己從request中獲取:

url_param = request.match_info['key']
query_params = parse_qs(request.query_string)

最後,需要自己構造Response物件:

text = render('template', data)
return web.Response(text.encode('utf-8'))

這些重複的工作可以由框架完成。例如,處理帶引數的URL/blog/{id}可以這麼寫:

@get('/blog/{id}')
def get_blog(id):
    pass

處理query_string引數可以通過關鍵字引數**kw或者命名關鍵字引數接收:

@get('/api/comments')
def api_comments(*, page='1'):
    pass

對於函式的返回值,不一定是web.Response

物件,可以是strbytesdict

如果希望渲染模板,我們可以這麼返回一個dict

return {
    '__template__': 'index.html',
    'data': '...'
}

因此,Web框架的設計是完全從使用者出發,目的是讓使用者編寫儘可能少的程式碼。

編寫簡單的函式而非引入requestweb.Response還有一個額外的好處,就是可以單獨測試,否則,需要模擬一個request才能測試。

@get和@post

要把一個函式對映為一個URL處理函式,我們先定義@get()

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

這樣,一個函式通過@get()的裝飾就附帶了URL資訊。

@post@get定義類似。

定義RequestHandler

URL處理函式不一定是一個coroutine,因此我們用RequestHandler()來封裝一個URL處理函式。

RequestHandler是一個類,由於定義了__call__()方法,因此可以將其例項視為函式。

RequestHandler目的就是從URL函式中分析其需要接收的引數,從request中獲取必要的引數,呼叫URL函式,然後把結果轉換為web.Response物件,這樣,就完全符合aiohttp框架的要求:

class RequestHandler(object):

    def __init__(self, app, fn):
        self._app = app
        self._func = fn
        ...

    @asyncio.coroutine
    def __call__(self, request):
        kw = ... 獲取引數
        r = yield from self._func(**kw)
        return r

再編寫一個add_route函式,用來註冊一個URL處理函式:

def add_route(app, fn):
    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 asyncio.iscoroutinefunction(fn) and not inspect.isgeneratorfunction(fn):
        fn = asyncio.coroutine(fn)
    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))

最後一步,把很多次add_route()註冊的呼叫:

add_route(app, handles.index)
add_route(app, handles.blog)
add_route(app, handles.create_comment)
...

變成自動掃描:

# 自動把handler模組的所有符合條件的函式註冊了:
add_routes(app, 'handlers')

add_routes()定義如下:

def add_routes(app, module_name):
    n = module_name.rfind('.')
    if n == (-1):
        mod = __import__(module_name, globals(), locals())
    else:
        name = module_name[n+1:]
        mod = getattr(__import__(module_name[:n], globals(), locals(), [name]), name)
    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)

最後,在app.py中加入middlewarejinja2模板和自注冊的支援:

app = web.Application(loop=loop, middlewares=[
    logger_factory, response_factory
])
init_jinja2(app, filters=dict(datetime=datetime_filter))
add_routes(app, 'handlers')
add_static(app)

middleware

middleware是一種攔截器,一個URL在被某個函式處理前,可以經過一系列的middleware的處理。

一個middleware可以改變URL的輸入、輸出,甚至可以決定不繼續處理而直接返回。middleware的用處就在於把通用的功能從每個URL處理函式中拿出來,集中放到一個地方。例如,一個記錄URL日誌的logger可以簡單定義如下:

@asyncio.coroutine
def logger_factory(app, handler):
    @asyncio.coroutine
    def logger(request):
        # 記錄日誌:
        logging.info('Request: %s %s' % (request.method, request.path))
        # 繼續處理請求:
        return (yield from handler(request))
    return logger

response這個middleware把返回值轉換為web.Response物件再返回,以保證滿足aiohttp的要求:

@asyncio.coroutine
def response_factory(app, handler):
    @asyncio.coroutine
    def response(request):
        # 結果:
        r = yield from handler(request)
        if isinstance(r, web.StreamResponse):
            return r
        if isinstance(r, bytes):
            resp = web.Response(body=r)
            resp.content_type = 'application/octet-stream'
            return resp
        if isinstance(r, str):
            resp = web.Response(body=r.encode('utf-8'))
            resp.content_type = 'text/html;charset=utf-8'
            return resp
        if isinstance(r, dict):
            ...

有了這些基礎設施,我們就可以專注地往handlers模組不斷新增URL處理函數了,可以極大地提高開發效率。