後端開發中,咱們常常使用web框架來實現各類應用,好比python中的flask,django等,go語言中的gin等。web框架提供了不少現成的工具,大大加快了開發速度。此次,咱們將動手實現本身的一個web框架。python
當咱們在瀏覽器打開連接發起請求以後發生了什麼?web
http請求會通過WSGI服務器轉發給web框架好比flask,flask處理請求並返回響應。WSGI就至關於中間商,處理客戶端和框架之間的信息交換。那麼WSGI究竟是個啥?數據庫
WSGI全稱服務器網關接口,你想一想,針對每一種服務器都須要實現對應的處理接口是件很麻煩的事,WSGI規定了統一的應用接口實現,具體來講,WSGI規定了application應該實現一個可調用的對象(函數,類,方法或者帶__call__
的實例),這個對象應該接受兩個位置參數:django
同時,該對象須要返回可迭代的響應文本。flask
爲了充分理解WSGI,咱們定義一個application,參數爲environ和回調函數。後端
def app(environ, start_response): response_body = b"Hello, World!" status = "200 OK" # 將響應狀態和header交給WSGI服務器好比gunicorn start_response(status, headers=[]) return iter([response_body])
當利用諸如gunicorn之類的服務器啓動該代碼,gunicorn app:app
,打開瀏覽器就能夠看到返回的「hello world」信息。api
能夠看到,app函數中的回調函數start_response將響應狀態和header交給了WSGI服務器。瀏覽器
web框架例如flask的核心就是實現WSGI規範,路由分發,視圖渲染等功能,這樣咱們就不用本身去寫相關的模塊了。服務器
以flask爲例,使用方法以下:app
from flask import Flask app = Flask(__name__) @app.route("/home") def hello(): return "hello world" if __name__ = '__main__': app.run()
首先定義了一個全局的app實例,而後在對應的函數上定義路由裝飾器,這樣不一樣的路由就分發給不一樣的函數處理。
爲了功能上的考量,咱們將application定義爲類的形式,新建一個api.py文件,首先實現WSGI。
這裏爲了方便使用了webob這個庫,它將WSGI處理封裝成了方便的接口,使用pip install webob
安裝。
from webob import Request, Response class API: def __call__(self, environ, start_response): request = Request(environ) response = Response() response.text = "Hello, World!" return response(environ, start_response)
API類中定義了`__call__
內置方法實現。很簡單,對吧。
路由是web框架中很重要的一個功能,它將不一樣的請求轉發給不一樣的處理程序,而後返回處理結果。好比:
對於路由 /home ,和路由/about, 像flask同樣,利用裝飾器將他們綁定到不一樣的函數上。
# app.py from api.py import API app = API() @app.route("/home") def home(request, response): response.text = "Hello from the HOME page" @app.route("/about") def about(request, response): response.text = "Hello from the ABOUT page"
這個裝飾器是如何實現的?
不一樣的路由對應不一樣的handler,應該用字典來存放對吧。這樣當新的路由過來以後,直接route.get(path, None) 便可。
class API: def __init__(self): self.routes = {} def route(self, path): def wrapper(handler): self.routes[path] = handler return handler return wrapper ...
如上所示,定義了一個routes字典,而後一個路由裝飾器方法,這樣就可使用@app.route("/home") 。
而後須要檢查每一個過來的request,將其轉發到不一樣的處理函數。
有一個問題,路由有靜態的也有動態的,怎麼辦?
用parse這個庫解析請求的path和存在的path,獲取動態參數。好比:
>>> from parse import parse >>> result = parse("/people/{name}", "/people/shiniao") >>> print(result.named) {'name': 'shiniao'}
除了動態路由,還要考慮到裝飾器是否是能夠綁定在類上,好比django。另外若是請求不存在路由,須要返回404。
import inspect from parse import parse from webob import Request, Response class API(object): def __init__(self): # 存放全部路由 self.routes = {} # WSGI要求實現的__call__ def __call__(self, environ, start_response): request = Request(environ) response = self.handle_request(request) # 將響應狀態和header交給WSGI服務器好比gunicorn # 同時返回響應正文 return response(environ, start_response) # 找到路由對應的處理對象和參數 def find_handler(self, request_path): for path, handler in self.routes.items(): parse_result = parse(path, request_path) if parse_result is not None: return handler, parse_result.named return None, None # 匹配請求路由,分發到不一樣處理函數 def handle_request(self, request): response = Response() handler, kwargs = self.find_handler(request.path) if handler is not None: # 若是handler是類的話 if inspect.isclass(handler): # 獲取類中定義的方法好比get/post handler = getattr(handler(), request.method.low(), None) # 若是不支持 if handler is None: raise AttributeError("method not allowed.", request.method) handler(request, response, **kwargs) else: self.default_response(response) return response def route(self, path): # if the path already exists if path in self.routes: raise AssertionError("route already exists.") # bind the route path and handler function def wrapper(handler): self.routes[path] = handler return handler return wrapper # 默認響應 def default_response(self, response): response.status_code = 404 response.text = "not found."
新建一個app.py文件,而後定義咱們的路由處理函數:
from api import API app = API() @app.route("/home") def home(request, response): response.text = "hello, ding." @app.route("/people/{name}") def people(req, resp, name) resp.text = "hello, {}".format(name)
而後咱們啓動gunicorn:
gunicorn app:app
。
打開瀏覽器訪問:
http://127.0.0.1:8000/people/shiniao
就能看到返回的信息:hello, shiniao
。
測試下訪問重複的路由會不會拋出異常,新建test_ding.py 文件。
使用pytest來測試。
import pytest from api import API @pytest.fixture def api(): return API() def test_basic_route(api): @api.route("/home") def home(req, resp): resp.text = "ding" with pytest.raises(AssertionError): @api.route("/home") def home2(req, resp): resp.text = "ding"
好了,以上就是實現了一個簡單的web框架,支持基本的路由轉發,動態路由等功能。我一直認爲最好的學習方法就是模仿,本身動手去實現不一樣的輪子,寫個解釋器啊,web框架啊,小的數據庫啊,這些對本身的技術進步都會頗有幫助。
另外,新年快樂!