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

前言

在上一篇中,从最简单使用形式入手,简单的过了一遍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/')
def hello(name):
    return f'Hello, {name}!'def hello(name):
    return f'Hello, {name}!'
    
app.add_url_rule('/hello/', 'hello', hello)

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

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

@app.route('/hello/', 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路由相关的内容就简单剖析完了,后面将接着分析上下文、请求与相应相关的内容,希望喜欢。

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

Flask源码剖析(二):路由原理_第1张图片

你可能感兴趣的:(Python,后端开发,语言开发,python,flask)