Flask 源码剖析 (二): 路由原理

本文深入剖析 Flask 框架的路由原理,从 endpoint 的作用讲起,探讨为什么使用 endpoint,然后揭示 Flask 路由匹配的详细过程,包括 dispatch_request()、RequestContext 和 MapAdapter 的 match() 方法。通过对这些核心概念的理解,有助于更好地掌握 Flask 开发。
摘要由CSDN通过智能技术生成

前言

在上一篇中,从最简单使用形式入手,简单的过了一遍 Flask 应用启动流程以及其背后的原理,本篇将会以类似的风格剖析 Flask 路由相关的内容,同样不会涉及过多细节,力求从较高的维度去看。

Flask 版本:1.0.2

endpoint 端点

回归一下上一篇文章,在通过 @app.route () 装饰器将函数转为 Flask 视图函数时,多次提及了 endpoint,对应的 add_url_rule () 代码如下。

# flask/app.py/Flask
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
    methods = options.pop('methods', None)
    rule = self.url_rule_class(rule, methods=methods, **options)
    self.url_map.add(rule)
    if view_func is not None:
        old_func = self.view_functions.get(endpoint)
        if old_func is not None and old_func != view_func:
            raise AssertionError('View function mapping is overwriting an '
                                 'existing endpoint function: %s' % endpoint)
        self.view_functions[endpoint] = view_func

在 add_url_rule () 方法中通过 endpoint,将路由与视图函数关联在一起,为什么不直接将路由与视图函数关联?要多弄个 endpoint?

为了回答这个问题,先来了解一下 endpoint。

通常,可以通过两种方式将路由与视图函数关联。

@app.route('/hello/<name>')
def hello(name):
    return f'Hello, {name}!'
或
def hello(name):
    return f'Hello, {name}!'
app.add_url_rule('/hello/<name>', 'hello', hello)

本地运行起来,接着访问 localhost:5000/hello/二两,就会调用 hello () 方法,此时关联方式为:localhost:5000/hello/二两 -> endpoint:hello -> hello()方法

这是最简单的写法,endpoint 与方法名相同,可以通过 endpoint 参数修改 endpoint 名称。

@app.route('/hello/<name>', endpoint='sayhello')
def hello(name):
    return f'Hello, {name}!'

此时,关联改变为 localhost:5000/hello/二两 -> endpoint:sayhello -> hello()方法

通过 endpoint 可以快速构建 url,不再需要对 url 进行硬编码。

@app.route('/')
def index():
    # 将访问 hello/二两
    print url_for('hello', name='二两')

ok,ok,我明白了 endpoint 是做什么的,但还是一开始的问题,为什么要 endpoint?路由与函数直接对应上不就好了?

因为使用 endpoint 更方便,可以将所有后台管理的逻辑都放在 admin endpoint 下,将所用用户相关的放在 user endpoint 下,当然这要配合蓝图机制来使用。

# main.py:
from flask import Flask, Blueprint
from admin import admin
from user import user
app = Flask(__name__)
# 注册蓝图
app.register_blueprint(admin, url_prefix='admin')
app.register_blueprint(user, url_prefix='user')
# admin.py:
# 实例化蓝图
admin = Blueprint('admin', __name__)
@admin.route('/home')
def home():
    return 'Hello, root user!'
# user.py:
user = Blueprint('user', __name__)
@user.route('/home')
def home():
    return 'Hello, lowly normal user!'
# 使用时
print url_for('admin.home') # Prints '/admin/home'
print url_for('user.home') # Prints '/user/home'

Flask 路由机制

理解 endpoint 是理解 Flask 路由机制的前提,不然,当你浏览 Flask 路由机制匹配规则会比较蒙圈。

路由机制关键在于匹配,而匹配的逻辑在 dispatch_request () 方法中,该方法的调用路径为:__call__()->wsgi\_app()->full_dispatch_request()->dispatch_request(),方法代码如下。

# flask/app.py
def dispatch_request(self):
       req = _request_ctx_stack.top.request
       if req.routing_exception is not None:
           self.raise_routing_exception(req)
       rule = req.url_rule
       if getattr(rule, 'provide_automatic_options', False) \
          and req.method == 'OPTIONS':
           return self.make_default_options_response()
        # 通过endpoint获得相应的视图函数
       return self.view_functions[rule.endpoint](**req.view_args)

从_request_ctx_stack 上下文中获得当前请求的上下文,找到当前请求的路由并从中找到 endpoint,再通过 endpoint 找到对应的视图函数。

关键在于 req.url_rulerule.endpoint这两个变量怎么来的?

_request_ctx_stack 变量涉及上下文相关的内容,细节先不提,其中存储着 RequestContext 类的实例对象,该对象与路由匹配相关的代码如下。

# flask/ctx.py
class RequestContext(object):
    def __init__(self, app, environ, request=None, session=None):
        self.app = app
        if request is None:
            # 将environ转为request
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = None
        try:
            self.url_adapter = app.create_url_adapter(self.request)
        except HTTPException as e:
            self.request.routing_exception = e
        self.flashes = None
        self.session = session
        # ... 省略无关代码
    def match_request(self):
        """Can be overridden by a subclass to hook into the matching
        of the request.
        """
        try:
            # 匹配url
            result = self.url_adapter.match(return_rule=True)
            # 返回结果
            self.request.url_rule, self.request.view_args = result
        except HTTPException as e:
            self.request.routing_exception = e

在__init__() 中,通过 app.request_class () 方法,将 environ 转为 Request 类实例,接着使用 app.create_url_adapter () 方法将 request 相关信息存到 url_map 变量中。

在路由匹配时会调用 match_request () 方法,该方法具体的匹配规则又由 self.url_adapter.match () 方法完成,该方法会返回 url_rule 与 view_args。

为了进一步理解,剖析一下 create_url_adapter () 方法与 match () 方法,先看 create_url_adapter () 方法,一层层看下去,该方法的调用顺序为:create_url_adapter()->self.url_map.bind_to_environ()->Map.bind()->MapAdapter(),简单而言,该方法最后返回一个 MapAdapter 类实例,MapAdapter 类下就有 match () 方法,上面 self.url_adapter.match () 调用的就是这个方法,该方法实现具体的匹配逻辑,最终返回返回 url_rule 与 view_args (路由与视图函数的参数)

# werkzeug/routing.py/MapAdapter
def match(self, path_info=None, method=None, return_rule=False, query_args=None):
    for rule in self.map._rules:
        try:
            rv = rule.match(path, method)
        except:
        # ... 省略
        # 返回 路由与视图函数的参数
        if return_rule:
            return rule, rv
        else:
            return rule.endpoint, rv

match () 匹配规则的逻辑比较细节,有兴趣的可以去 werkzeug 的 routing.py 文件中查阅。

至此,Flask 路由的大致过程就分析完了,简单总结一下:

  • 1. 通过 @app.route 装饰器或者 app.add_url_rule () 方法注册视图函数

  • 2. 每次请求时,都会利用上下文的形式将路由匹配结果存储起来。匹配逻辑最终由 MapAdapter 类的 match () 方法完成。

  • 3. 最后,通过 dispatch_request () 方法获取此前匹配的路由结果,调用相应的视图函数

over!

结尾

Flask 路由相关的内容就简单剖析完了,后面将接着分析上下文、请求与相应相关的内容,希望喜欢。

如果本篇文章对你有些帮助,麻烦点一下「在看」支持二两,下篇文章见。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

懒编程-二两

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值