在正式開始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處理函數了,可以極大地提高開發效率。
### 參考源碼
[day-05](https://github.com/michaelliao/awesome-python3-webapp/tree/day-05)
- 關于
- Python簡介
- 安裝Python
- Python解釋器
- 第一個Python程序
- 使用文本編輯器
- Python代碼運行助手
- 輸入和輸出
- Python基礎
- 數據類型和變量
- 字符串和編碼
- 使用list和tuple
- 條件判斷
- 循環
- 使用dict和set
- 函數
- 調用函數
- 定義函數
- 函數的參數
- 遞歸函數
- 高級特性
- 切片
- 迭代
- 列表生成式
- 生成器
- 迭代器
- 函數式編程
- 高階函數
- 返回函數
- 匿名函數
- 裝飾器
- 偏函數
- 模塊
- 使用模塊
- 安裝第三方模塊
- 面向對象編程
- 類和實例
- 訪問限制
- 繼承和多態
- 獲取對象信息
- 實例屬性和類屬性
- 面向對象高級編程
- 使用slots
- 使用@property
- 多重繼承
- 定制類
- 使用枚舉類
- 使用元類
- 錯誤、調試和測試
- 錯誤處理
- 調試
- 單元測試
- 文檔測試
- IO編程
- 文件讀寫
- StringIO和BytesIO
- 操作文件和目錄
- 序列化
- 進程和線程
- 多進程
- 多線程
- ThreadLocal
- 進程 vs. 線程
- 分布式進程
- 正則表達式
- 常用內建模塊
- datetime
- collections
- base64
- struct
- hashlib
- itertools
- XML
- HTMLParser
- urllib
- 常用第三方模塊
- PIL
- virtualenv
- 圖形界面
- 網絡編程
- TCP/IP簡介
- TCP編程
- UDP編程
- 電子郵件
- SMTP發送郵件
- POP3收取郵件
- 訪問數據庫
- 使用SQLite
- 使用MySQL
- 使用SQLAlchemy
- Web開發
- HTTP協議簡介
- HTML簡介
- WSGI接口
- 使用Web框架
- 使用模板
- 異步IO
- 協程
- asyncio
- aiohttp
- 實戰
- Day 1 - 搭建開發環境
- Day 2 - 編寫Web App骨架
- Day 3 - 編寫ORM
- Day 4 - 編寫Model
- Day 5 - 編寫Web框架
- Day 6 - 編寫配置文件
- Day 7 - 編寫MVC
- Day 8 - 構建前端
- Day 9 - 編寫API
- Day 10 - 用戶注冊和登錄
- Day 11 - 編寫日志創建頁
- Day 12 - 編寫日志列表頁
- Day 13 - 提升開發效率
- Day 14 - 完成Web App
- Day 15 - 部署Web App
- Day 16 - 編寫移動App
- FAQ
- 期末總結