深入理解Flask路由的實現機制

本篇介紹 Flask 路由的基本用法,並且通過部分源代碼深入淺出闡述 Flask 路由的實現機制。

路由的基本用法

我們先編寫一段簡單代碼,代碼包括兩個視圖函數。

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Index Page'

@app.route('/about')
def about():
    return 'About Page'

if __name__ == '__main__':
    print(app.url_map)
    app.run()

所謂路由,就是 Flask 根據客戶端 request 的 URL,查找對應的視圖函數 (view function),由視圖函數進行處理後,返回 response 到客戶端。Flask application 有兩個屬性來保存與路由相關的信息:

  • url_map: 儲存 url 和 endpoint 的映射(url_map 的數據類型是 werkzeug.routing.Map
  • view_functions: 儲存 endpoint 和 view function 的映射 (dict 類型)

就上面的例子來說,當客戶端請求的 URL 中,path 爲 /,Flask 就用 index 視圖函數進行處理,當客戶端請求的 URL 中,path 爲 /about, Flask 就用 about 視圖函數進行處理。運行程序,在 IDE 中打印瞭如下信息:

Map([<Rule '/about' (GET, OPTIONS, HEAD) -> about>,
 <Rule '/' (GET, OPTIONS, HEAD) -> index>,
 <Rule '/static/<filename>' (GET, OPTIONS, HEAD) -> static>])

endpoint 的作用

這裏有兩個知識點:第一個是 url 到視圖函數的映射以 endpoint 來作爲中介 (url -> endpoint -> view function)。爲什麼從 url 到視圖函數的映射使用 endpoint 作爲中介呢?如果不用 blueprint,endpoint 是沒什麼作用的。使用 blueprint 後,endpoint 就允許通過 blueprint 來進行區分。

接下來說明 endpoint 的作用,創建一個新的 Flask 工程,工程文件的結構如下:

flask-route-logic /
	admin /
		__init__.py
	user /
		__init__.py
	app.py

admin 和 user 作爲兩個藍圖 (blueprint),用於模塊化組織代碼。

admin/__init__.py 的代碼如下:

# admin/__init__.py

from flask import Blueprint

adminbp = Blueprint('adminbp', __name__, url_prefix='/admin')

@adminbp.route('/')
def index():
    return 'Admin blueprint, index page'

user/__init__.py 的代碼如下:

# user/__init__.py

from flask import Blueprint

userbp = Blueprint('userbp', __name__, url_prefix='/user')

@userbp.route('/')
def index():
    return 'User blueprint, index page'

app 主文件的代碼如下:

from flask import Flask, url_for
from user import userbp
from admin import adminbp

app = Flask(__name__)
app.register_blueprint(userbp)
app.register_blueprint(adminbp)

@app.route('/', endpoint='index')
def index():
    return 'Index Page'

if __name__ == '__main__':
    print(app.url_map)
    app.run()

在代碼中,一共定義了 3 個名稱都爲 index 的視圖函數。運行後,打印的 url_map 信息如下:

Map([<Rule '/admin/' (OPTIONS, HEAD, GET) -> adminbp.index>,
 <Rule '/user/' (OPTIONS, HEAD, GET) -> userbp.index>,
 <Rule '/' (OPTIONS, HEAD, GET) -> index>,
 <Rule '/static/<filename>' (OPTIONS, HEAD, GET) -> static>])

可以看到,使用 blueprint 後,3 個 index 函數,endpoint 的名稱分別爲 index, adminbp.index 和 userbp.index,這樣使用 url_for() 函數的時候就能區分,進行反向解析了。

定位 static 文件:static endpoint

第二個知識點,在本篇第一段代碼中,我們定義了兩個路由,爲什麼打印出來的 url_map 卻有 3 個 rule (/, /aboutstatic) 呢?這是因爲 Flask 在代碼中添加了一個名爲 static 的 endpoint,用於 url_for() 函數定位 static文件, 比如 css, images 等等。爲了便於理解,我們用示例代碼來說明。我們搭建一個如下所示的工程文件結構:

flask-route-logic /
	static /
		images /
			demo.png
	templates /
		index.html
	app2.py

在 static/imgage 文件夾下有一個圖片文件,我們在 index.html 中,將使用 url_for 函數來構建一個 url,指向 demo.png 圖像文件。

index.html 文件代碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>url_for 函數示例</title>
</head>
<body>
    <p1>Below is a plus size model:</p1><br/>
    <img src="{{ url_for('static', filename='images/demo.png') }}"/>
</body>
</html>

app2.py 的代碼:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

if __name__ == '__main__':
    app.run()

這裏,之所以能用 url_for('static', filename='images/demo.png) ,static 作爲 endpoint, 就是因爲 Flask 爲了處理靜態文件而在代碼中增加的一個 endpoint ( static) 的路由匹配規則。在 Flask 的源代碼 __init__() 方法中,我們可以看到這樣一段代碼(不同版本可能稍有出入):

if self.has_static_folder:
   # ...(省略無關代碼)
    self.add_url_rule(
        self.static_url_path + "/<path:filename>",
        endpoint="static",
        host=static_host,
        view_func=self.send_static_file,
    )

這段代碼用硬編碼的方式添加了 static 這個 endpoint

Flask 的路由通過 @route 裝飾器實現,本質上是調用 add_url_urle() 方法實現的,相關代碼如下:

# flask/app.py

class Flask(_PackageBoundObject):
	# ...
	def route(self, rule, **options):
		def decorator(f):
            endpoint = options.pop("endpoint", None)
            self.add_url_rule(rule, endpoint, f, **options)
            return f

        return decorator

add_url_rule() 函數的 3 個參數是 rule, endpoint 和 view functions,其核心代碼如下(我省略了無關代碼和部分細節代碼):

def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
	# 如果沒有指定endpoint,則默認爲view functon的函數名
	if endpoint is None:
	    endpoint = _endpoint_from_view_func(view_func)	
	# 將 endpoint 加入到 options(dict)
	options["endpoint"] = endpoint
	
	# methods: GET, HEAD, OPTIONS等
	methods = options.pop("methods", None)	
	# 從view function獲取method,沒有則爲GET
	if methods is None:
        methods = getattr(view_func, "methods", None) or ("GET",)   
    # 將 methods改變爲set類型
    methods = set(item.upper() for item in methods)
	
	# 將rule添加到url_map
	rule = self.url_rule_class(rule, methods=methods, **options)
    self.url_map.add(rule)
	
	# 添加view functions
	if view_func is not None:	   
	   self.view_functions[endpoint] = view_func

根據上面的核心代碼,我們知道,add_url_rule() 最主要做了 4 件事:

  • 處理 endpoint: 由函數參數提供,或者默認爲函數名稱
  • 處理 methods (GET, HEAD, OPTIONS, POST 等)
  • 將每個匹配規則作爲 rule 添加到 url_map
  • 將 endpoint 和 view function 的映射添加到 url_functions (dict)

既然 route 裝飾器的本質是調用 add_url_rule(),我們的代碼也可以這樣寫:

from flask import Flask

app = Flask(__name__)

def index():
    return 'Index Page'

def about():
    return 'About Page'

if __name__ == '__main__':
    app.add_url_rule('/', 'index', index)
    app.add_url_rule('/about', 'about', about)

    print(app.url_map)
    app.run()

另外,說明一下,Flask 路由的數據結構、路由匹配規則等,是由 werkzeug 實現的,Flask 只是使用者而已。

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