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
str
、bytes
或dict
。
如果希望渲染模板,我們可以這麼返回一個dict
:
return {
'__template__': 'index.html',
'data': '...'
}
因此,Web框架的設計是完全從使用者出發,目的是讓使用者編寫儘可能少的程式碼。
編寫簡單的函式而非引入request
和web.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
中加入middleware
、jinja2
模板和自注冊的支援:
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處理函數了,可以極大地提高開發效率。