Flask1.1.4 Werkzeug1.0.1 源码分析:路由

路由大致分为两部分:

  1. 路由绑定:将url_rule和view_func 绑定到falsk实例中,这个在之前的一篇 启动流程中已经讲过了
  2. 路由解析:当实际请求来时,根据environ去匹配到对应的Rule,并从environ中解析出参数,然后根据Rule.endpoint 去view_functions中找对应的view_function进行调用

此篇文章重点讲解第二部分,Flask的路由匹配其实基于 werkzeug.routing.Map 和 werkzeug.routing.Rule
我们可以先写个简单的demo,了解下这两个工具的使用方法。

# 构造map
url_map = Map([Rule("/a", endpoint='a'), Rule("/b", endpoint='b'), Rule("/c/<path_param>", endpoint='c')])
# 调用Map.bind() 返回 MapAdapter对象
urls = url_map.bind('eee.com', '/')
# MapAdapter.match() 进行请求匹配
print(urls.match("/a"))
print(urls.match("/c/123"))
print(urls.match("/dd"))

结果如下:

#匹配到了会返回 (endpoint, view_func_args)
('a', {})
('c', {'path_param': '123'})
#匹配不到 抛异常
Traceback (most recent call last):
  File "/Users/panc/Documents/projects/PycharmProjects/grpc-client/pycode/flask_demo.py", line 9, in <module>
    print(urls.match("/dd"))
  File "/usr/local/Caskroom/miniconda/base/envs/python37/lib/python3.7/site-packages/werkzeug/routing.py", line 1945, in match
    raise NotFound()
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.

ok,下面开始研究Flask的路由过程~
首先要注意下,Flask中并没有直接把URL和view_func关联起来,而是在其中添加了endpoint的概念。其实也好理解,URL很多时候并不是固定的,还包含了参数部分,不太适合作为key。另外,从处理请求的步骤来看,首先要根据请求url信息去匹配已经注册了的url_rule,然后再去找对应的view_func执行逻辑。这样分为两步的情况下,将url信息和view_func分开存储,然后通过endpoint来关联两者显得很合适。

启动流程一篇中已经讲过,HTTP格式数据转换为WSGI格式数据后,调用 flask_app(environ, start_response) 执行具体的处理逻辑

class Flask(_PackageBoundObject):
    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)
        
    def wsgi_app(self, environ, start_response):
    	# 此处构建了 flask.ctx.RequestContext 对象
    	# 构建过程中包含 路由处理逻辑
        ctx = self.request_context(environ)
        error = None
        try:
            try:
            	# 此处也包含了路由逻辑 将当前请求上下文入栈
            	# 入栈是为了用非传参的方式将一些信息传递给后面的view_function
                ctx.push()
                # RequestContext 对象构建和push的过程中已经处理好了路由的逻辑
                # 之后只需要获取当前请求的 RequestContext实例,用endpoint去获取并调用方法即可
                response = self.full_dispatch_request()
            except Exception as e:
                error = e
                response = self.handle_exception(e)
            except:  # noqa: B001
                error = sys.exc_info()[1]
                raise
            return response(environ, start_response)
        finally:
            if self.should_ignore_error(error):
                error = None
            # 请求处理完成后清理当前的上下文 即出栈
            ctx.auto_pop(error)

    def request_context(self, environ):
    	# 注意第一个参数为flask实例
        return RequestContext(self, environ)

    def create_url_adapter(self, request):
        if request is not None:
            subdomain = (
                (self.url_map.default_subdomain or None)
                if not self.subdomain_matching
                else None
            )
            # 之前的demo里面有看过这个 bind_to_environ 和bind作用是一样的
            # 返回一个 MapAdapter对象
            return self.url_map.bind_to_environ(
                request.environ,
                server_name=self.config["SERVER_NAME"],
                subdomain=subdomain,
            )

    def full_dispatch_request(self):
        self.try_trigger_before_first_request_functions()
        try:
            request_started.send(self)
            rv = self.preprocess_request()
            if rv is None:
            	# 执行请求
                rv = self.dispatch_request()
        except Exception as e:
            rv = self.handle_user_exception(e)
        # 此处将view_function的返回 转换为 Response对象
        return self.finalize_request(rv)

    def dispatch_request(self):
    	# 取出当前的RequestContext对象 并获取其request属性
        req = _request_ctx_stack.top.request
        # 路由匹配失败 抛异常
        if req.routing_exception is not None:
            self.raise_routing_exception(req)
        rule = req.url_rule
        # 最终 Rule对象的endpoint去找到对应的view_function并用参数调用 执行具体逻辑
        return self.view_functions[rule.endpoint](**req.view_args)

下面来看下路由处理的关键 RequestContext

class RequestContext(object):

    def __init__(self, app, environ, request=None, session=None):
    	# 此处保存了 flask app实例
        self.app = app
        if request is None:
        	# 使用environ 构建一个 Request对象
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = None
        try:
        	# 此处 就是进行Map.bind 获得了一个 MapAdapter对象
            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):
        try:
        	# 其实就是 MapAdapter.match() 此处没有传参因为之前是 Map.bind_to_environ()
            result = self.url_adapter.match(return_rule=True)
            # 拆包  url_rule即Rule对象包含endpoint信息 view_args即参数
            # 也就是说 RequestContext对象push()时,已经完成了路由解析过程了
            # 将解析结果绑定到 RequestContext对象的request属性上
            self.request.url_rule, self.request.view_args = result
        except HTTPException as e:
            self.request.routing_exception = e

    def push(self):
        """Binds the request context to the current context."""
       	# 将当前的请求上下文对象入栈 方便后续传递
        _request_ctx_stack.push(self)
		
        if self.url_adapter is not None:
        	# 关键
            self.match_request()

ok,至此路由的解析过程就结束啦~
总结一下:

  1. 核心的核心其实还是 werkzeug的 Map、Rule、MapAdapter对象
  2. 路由的绑定走的是 flask_app.add_url_rule() 方式有多种,可以是 app.route() 或者用 blueprint,但是本质都是 flask_app.add_url_rule()
  3. 路由的解析,本质就是 map_adapter = url_map.bind_to_environ() -> rule, view_args = map_adapter.match() -> view_functions[rule.endpoint] (**view_args)
  4. 其中 url_map.bind_to_environ() 在 构建RequestContext对象时执行, match操作在 RequestContext.push()时执行。上下文对象准备完成并入栈之后,只需取出当前的请求上下文对象,然后用endpoint去找到对应的view_func并执行即可。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值