200行代碼實現web框架(一):裝飾器實現簡單路由功能
上一篇文章簡單介紹了WSGI協議:
折騰君:python從小白到入門:10分鐘搞懂WSGI協議其實,別看PEP-333那麼長,字那麼多,但是當你真正開始寫一個WSGI框架的時候,你就發現,其實掌握了下面幾點就夠了:
- 接受environ 和start_response兩個參數
- 內部調用 start_respons生成header
- 返回一個可迭代的響應體
下面,我們就來從頭開始,一步一步,寫一個麻雀雖小、五臟俱全的web框架出來。
首先,根據PEP-333的小例子,對其進行稍加改動,我們可以寫出以下的Application類:
# -*- coding: UTF-8 -*-n#smartframe.pynnclass Application(object):nn def __init__(self):n passn #請注意__call__方法與原例子中__iter__方法的區別!n def __call__(self, environ, start_response):n status = 200 OKn #text/plain 別忘了改成text/htmln response_headers = [(Content-type, text/html)]n start_response(status, response_headers)n return "<h1>Hello world!</h1>"n
那麼,這個應用要怎樣才能運行起來咧?
由於這個應用遵守WSGI協議,因此我們可以讓它在任何支持WSGI協議的伺服器程序上運行。wsgiref(20.4. wsgiref - WSGI Utilities and Reference Implementation - Python 2.7.14 documentation)是python自帶的一個WSGI演示庫,它其中包含了一個WSGI伺服器(simple_server)和一個demo應用(demo_app)。藉助wsgiref,我們可以很方便地調試自己寫的web框架,使用下面的方法可以在本地將Application運行起來:
#coding: utf-8n#server.pynnfrom wsgiref.simple_server import make_servernfrom smartframe import Applicationnn#實例化一個Application對象napp = Application()n#本地建立伺服器nhttpd = make_server(, 8001, app)nprint "Server start on port 8001"nnhttpd.serve_forever()n
在終端中輸入(版本為python2.7):
python server.pyn
在瀏覽器中訪問127.0.0.1:8001就能看到一個大大的Hello World。
OK,一個小網站就搞好了,現在我們可以一步一步讓它變得更加強大。
現在你發現,雖然在瀏覽器地址欄中輸入127.0.0.1:8001可以訪問helloworld網頁,輸入127.0.0.1:8001/hehe/同樣可以訪問helloworld。其實,伺服器只負責解析URL的前半部分,也就是說,只要是伺服器看到你的網址是127.0.0.1:8001打頭的,它就調用app對象,根本不管斜線後面到底是什麼。而我們的app功能也比較單一,就是不管http請求是什麼,它都只返回了一個頁面。如何能夠讓app返回的頁面根據URL的變化而改變呢?我們需要在app中實現路由功能。
我們可以在__call__函數裡面使用下列判斷語句來實現路由功能:
def __call__(self, environ, start_response):n status = 200 OKn response_headers = [(Content-type, text/html)]n start_response(status, response_headers)n #使用environ[PATH_INFO]來獲取pathn path = environ[PATH_INFO]n if path == /hehe/:n return "<h1>hehe</h1>"n else:n return "<h1>Hello world!</h1>path:{}".format(path)n
這個時候,訪問127.0.0.1:8001/hehe/就可以看到大大的「hehe」
然鵝,這個方法真的很笨。那以後每做一個頁面出來就多一個if判斷,多累啊。更好的辦法是定義一個字典,讓path與處理頁面的函數相對應:
# -*- coding: UTF-8 -*-n#smartframe.pynnclass Application(object):nn def __init__(self):n self.routes = {n /hehe/:hehe,n }nn def __call__(self, environ, start_response):n path = environ[PATH_INFO]n if path in self.routes:n status = 200 OKn response_headers = [(Content-Type,text/html)]n start_response(status, response_headers)n return self.routes[path]()n else:n status = 404 Not Foundn response_headers = [(Content-Type,text/html)]n start_response(status, response_headers)n return "404"nndef hehe():n return "<h1>hehe</h1>"n
這樣,當你訪問一個頁面,如果頁面的path在routes中,就會返回相應的頁面。為了進一步簡化代碼的複雜度,我們可以藉助python的裝飾器來實現「自動註冊路由」的功能:
# -*- coding: UTF-8 -*-n#smartframe.pynclass Application(object):n def __init__(self):n passn self.routes = {}nn def route(self, path=None):n def wrapper(func):n self.routes[path] = funcn return funcn return wrappernn def __call__(self, environ, start_response):n path = environ[PATH_INFO]n if path in self.routes:n status = 200 OKn response_headers = [(Content-Type,text/html)]n start_response(status, response_headers)n #執行與path對應的func()n return self.routes[path]()n else:n status = 404 Not Foundn response_headers = [(Content-Type,text/html)]n start_response(status, response_headers)n return "404"n------------------------------------------------------------------------n#coding: utf-8n#server.pynfrom wsgiref.simple_server import make_servernfrom smartframe import *nnapp = Application()nn@app.route(/hehe/)ndef hehe():n return "hehe"nnhttpd = make_server(, 8001, app)nprint "Server start on port 8001"nhttpd.serve_forever()n
需要注意的是:
- 路由函數要放在server.py中,因為需要先將Application實例化,再由對象的實例——app來調用hehe()函數,才能在路由表裡註冊這個函數。
- 關於裝飾器,請參考:裝飾器。但是,請注意,這個裝飾器並不是下面代碼塊中的"簡單"裝飾器,它屬於帶參數的裝飾器!這個一定要搞明白(請參考:如何理解Python裝飾器? - ucag的回答 - 知乎;裝飾器-廖雪峰的python教程)。
#route裝飾器並不是這種類型的「簡單」裝飾器ndef log(func):n def wrapper(*args, **kw):n print(call %s(): % func.__name__)n return func(*args, **kw)n return wrappernn#route裝飾器可以看作下面這種帶參數的裝飾器減少了最裡面的那層ndef log(text):n def decorator(func):n def wrapper(*args, **kw):n print(%s %s(): % (text, func.__name__))n return func(*args, **kw)n return wrappern return decoratorn
注意:route裝飾器可以看作帶參數的裝飾器減少了最裡面的那層,因此才能在定義路由函數的時候就執行(而非定義)以下語句:
self.routes[path] = funcn
該語句的作用是把path: func添加到routes字典當中。
當然,上述代碼只能實現簡單的路由功能。
形如
http://host:port:/path1/path2?a=1&b=2n
這種類型的網址應該如何去解析其參數並返回相應內容?一個能想到的辦法是利用正則表達式去提取其中的各項參數。除此之外,還有許多問題需要考慮。如,上面的代碼默認處理的是GET請求,其他類型的請求如何處理?我們將在以後探討更深層次的問題。
代碼已上傳至github (v1文件夾) summerliehu/smartframe
推薦閱讀: