WSGI 爲 Web Server Gateway Inferface 的縮寫,是 Python Web 框架(或應用程序)與 Web 服務器 (Web Server) 之間通訊的規範,本質上是定義了一種 Web server 與 Web application 解耦的規範。比如 Flask 就是運行在 WSGI 協議之上的 web 框架。
來看一幅圖:
左邊,Client 和 Server 之間,Client 發送請求,Server 返回響應,遵守 HTTP 協議; 右邊:Python 語言編寫的 Web Application 和 Web Server 之間通訊,建議遵守 WSGI 規範。該規範被定義在 PEP 333。
WSGI 規定:每個使用 Python 語言編寫的 Web Application 必須是一個可調用對象(實現了__call__ 函數的方法或者類),接受兩個參數 :
environ
:WSGI 的環境信息start_response
:回調函數,在發送 response body 之前被調用,也是一個可調用對象。
如果使用 werkzeug 來實現 Web Application 和 Web Server,只需要下面的代碼:
from werkzeug.serving import run_simple
def application(environ, start_response):
headers = [('Content-Type', 'text/plain')]
start_response('200 OK', headers)
return [b'Hello World']
if __name__ == "__main__":
run_simple('localhost', 5000, application)
start_response
函數必須接受兩個參數: status
(HTTP狀態)和 response_headers
(響應消息的頭)。
Request 和 Response
werkzeug 的 Request 對象對 environ 對象進行了封裝 (The Request class wraps the environ for easier access to request variables),Response 對象則封裝了 WSGI Application。經過 Request 和 Response 的封裝,編寫 web application 變得更加簡單。比如,下面的代碼實現了與剛纔程序代碼相同的功能。
from werkzeug.serving import run_simple
from werkzeug.wrappers import Request, Response
def application(environ, start_response):
req = Request(environ)
body = 'Hello World'
resp = Response(body, mimetype='text/plain')
return resp(environ, start_response)
if __name__ == "__main__":
run_simple('localhost', 5000, application)
理解了 WSGI 規範和 werkzeug 封裝的 Request 和 Response,接下來我們要實現 web application 的幾個主要功能:路由、模板渲染 (render template)、請求和響應循環。通過代碼的逐步演變,有助於理解 Flask 的思路和源碼。
基本框架
下面的代碼基於 werkzeug ,實現了 Web Application 和 Web Server 的功能。無論 url 的 path 是什麼,都返回 Hello World!
from werkzeug.serving import run_simple
from werkzeug.wrappers import Request, Response
class WebApp(object):
def __init__(self):
pass
def dispatch_request(self, request):
return Response('Hello World')
def wsgi_app(self, environ, start_response):
request = Request(environ)
response = self.dispatch_request(request)
return response(environ, start_response)
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
def create_app(host='localhost', port=5000):
app = WebApp()
return app
if __name__ == "__main__":
app = create_app()
run_simple('localhost', 5000, app)
實現路由
上面的代碼中,無論客戶端請求的 url path 是什麼,都返回固定的字符串。前面我們在深入理解Flask路由(2)- werkzeug 路由系統 博文中,介紹了 werkzeug 的路由系統,我們基於上面的代碼,實現可以處理下面兩個路徑的路由:
- root path
- /users/userid
如果客戶端請求其它的 url,將得到 Not Found 錯誤。
from werkzeug.serving import run_simple
from werkzeug.wrappers import Request, Response
from werkzeug.routing import Map, Rule
from werkzeug.exceptions import HTTPException
class WebApp(object):
def __init__(self):
self.url_map = Map([
Rule('/', endpoint='index'),
Rule('/users/<userid>', endpoint='userinfo')
])
def dispatch_request(self, request):
adapter = self.url_map.bind_to_environ(request.environ)
try:
endpoint, args = adapter.match()
# 根據endpoint,找到視圖函數 on_endpointname,並且執行
return getattr(self, 'on_'+endpoint)(request, **args)
except HTTPException as ex:
return ex
def wsgi_app(self, environ, start_response):
req = Request(environ)
resp = self.dispatch_request(req)
return resp(environ, start_response)
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
def on_index(self, request):
return Response('index page')
def on_userinfo(self, request, userid):
return Response('Hello, {}'.format(userid))
def create_app(host='localhost', port=5000):
app = WebApp()
return app
if __name__ == "__main__":
app = create_app()
run_simple('localhost', 5000, app)
代碼的主要變化在 __init__()
方法和 dispatch_request()
方法中:
實現視圖和模板渲染
對客戶端的請求,不能只是返回簡單的字符串。接下來,我們對程序的功能加上視圖函數,返回真正的頁面,並且藉助 jinjia2 的模板功能,允許向頁面傳遞參數。
首先編寫兩個 html 頁面,放在工程文件 templates 文件夾下面:
templates/index.html:
<!DOCTYPE html>
<html lang="en">
<link rel=stylesheet href=/static/style.css type=text/css>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>This is index page</h1>
</body>
</html>
templates/user.html
<!DOCTYPE html>
<html lang="en">
<link rel=stylesheet href=/static/style.css type=text/css>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Hello, {{ userid }} </h1>
</body>
</html>
然後在 WebApp 類中實現 render_template()
方法:
def __init__(self):
template_path = os.path.join(os.path.dirname(__file__), 'templates')
self.jinja_env = Environment(loader=FileSystemLoader(template_path),
autoescape=True)
# 其它代碼略
def render_template(self, template_name, **context):
t = self.jinja_env.get_template(template_name)
return Response(t.render(context), mimetype='text/html')
這樣,視圖函數 on_index()
和 on_userinfo()
就可以返回 html 文件了。完整代碼如下:
from werkzeug.serving import run_simple
from werkzeug.wrappers import Request, Response
from werkzeug.routing import Map, Rule
from werkzeug.wsgi import SharedDataMiddleware
from werkzeug.exceptions import HTTPException, NotFound
from jinja2 import Environment, FileSystemLoader
import os
class WebApp(object):
def __init__(self):
template_path = os.path.join(os.path.dirname(__file__), 'templates')
self.jinja_env = Environment(loader=FileSystemLoader(template_path),
autoescape=True)
self.url_map = Map([
Rule('/', endpoint='index'),
Rule('/users/<userid>', endpoint='userinfo')
])
self.view_functions = {
'index': self.on_index,
'userinfo': self.on_userinfo
}
def dispatch_request(self, request):
adapter = self.url_map.bind_to_environ(request.environ)
try:
endpoint, args = adapter.match()
return self.view_functions[endpoint](endpoint, **args)
except HTTPException as e:
return e
def render_template(self, template_name, **context):
t = self.jinja_env.get_template(template_name)
return Response(t.render(context), mimetype='text/html')
def wsgi_app(self, environ, start_response):
req = Request(environ)
resp = self.dispatch_request(req)
return resp(environ, start_response)
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
def on_index(self, request):
return self.render_template('index.html')
def on_userinfo(self, request, userid):
return self.render_template('user.html', userid=userid)
def create_app(host='localhost', port=5000, with_static=True):
app = WebApp()
if with_static:
app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
'/static': os.path.join(os.path.dirname(__file__), 'static')
})
return app
if __name__ == "__main__":
app = create_app()
run_simple('localhost', 5000, app)
代碼
完整代碼:github: werkzeug-web-app-evolve