werkzeug實現WSGI Application

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

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章