深入理解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 只是使用者而已。

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