04-路由-启动之时
在学习路由之前,我们需要了解一些常见的 WSGI 参数
WSGIRequestHandler 从 HTTP 请求中提取了 请求函数,请求路径,请求参数,请求类型,并将其存入到 env 中,然后返回
然后调用了 demo_app,因此路由解析是在 demo_app 中进行的。
进一步猜想,如果一个请求来了才去执行路由代码,生成路由对象,肯定很慢啊,正确的做法是,在启动 Django 的时候,就直接加载项目中所有的路由,回想一下,当我们在浏览器输入了一个不存在的路由的时候,控制台甚至会给我们罗列出已经存在的路由,是不是也验证了我们的猜想。所以这篇博客的主要任务是探寻 Django 的路由生成,进而探寻 Django 如何调用了对应的视图的。
下面用来举例的项目是我在公司写的一个用来查询 ip 所在的省市所用的项目。读者自己创建一个项目也可以用来调试。
断点到路由代码中
【1】项目下的总 urls 文件
【2】django 的的某一个 app 下的urls 文件,项目总 urls 文件会调用到下属应用中的 urls 文件
【3】调试区
我一共打上了两处断点,然后调试到子应用下的 urls 文件中,在右侧的调试区可以看到函数调用的完整堆栈,从下网上看,就可以发现 Django 启动时候的完整轨迹。
我们从下往上一个一个看过去,还可以顺带复习一下前面学习的启动命令的源码
执行了 manage.py 文件
manage.py 文件中 调用了 execute_from_command_line 函数,并且传入了执行命令时 传入的参数, runserver,后面还可以指定 ip 和端口号,只是我嫌麻烦,没有弄
execute_from_command_line
execute_from_command_line 函数主要任务就是生成了一个 ManagementUtility 对象,ManagementUtility类的初始化函数仅仅只是赋值而已,重要的是调用了 ManagementUtility.execute 方法
ManagementUtility.execute
ManagementUtility.execute 方法最主要的任务就是加载环境变量,加载所有的脚本,依据脚本命令,找到对应的脚本,然后执行 Command.run_from_argv 这里有什么不明白的可以看前面的博客,详细讲述了,Django 如何加载所有的脚本,生成 Command 对象。
Command.run_from_argv
Command.run_from_argv 最核心的一句就是执行了父类的 execute 方法,接下来去父类的 execute 中看看
Command.execute
【1】BaseCommand.run_from_argv 调用了子类也就是 django.core 下的 Command 的 execute 方法
【2】子类 django.core 下的 Command 的 execute 方法又调用了父类 BaseCommand 的 execute 方法
【3】父类 BaseCommand 的 execute 方法又调用了子类 django.core 下的 Command 的 handle 方法
上面虽然子类父类来回调用,但是这正是多态,通常来说子类的同名函数不是重写父类的同名函数,就是对父类的同名函数进行补充,并在其内部接着调用了父类的同名函数。上述的代码两个 execute 方法属于后者。
子类 django.core 下的 Command 的 run 方法
【1】run 方法调用了 autoreload.run_with_reloader
【2】autoreload.run_with_reloader 方法中有一个非常关键的函数 start_django,看名字就知道了是启动 Django 的函数
start_django
【1】生成了一个线程,这个线程的执行函数就是 inner_run, inner_run 方法就是创建一个 WSGI服务器,并将 Django 和 WSGI 服务器联系到一起
【2】将线程设置为守护线程并且跑起来
【3】执行了 Reloader 类的 run 方法
【4】调用了 wait_for_apps_ready 方法使守护线程休眠0.1秒,在这 0.1秒内,Django 还要做些准备工作
【5】这行代码非常关键,他在加载路由,下面将会对这行代码进行深入探讨
深入路由
【1】调用了 get_resolver 函数
【2】get_resolver 函数先是提取配置文件中的 ROOT_URLCONF 根路由。此时根路由还是个字符串的表示。然后调用了 _get_cached_resolver
【3】生成了一个 URLResolver 对象,这是个 URL 解析器,传入了一个正则解析对象[Django 封装了] 和 步骤1中提取到的 url 配置
【4】步骤1中得到的 URL 解析器对象,紧接着调用了解析器对象的 urlconf_module 方法,这个方法被 cached_property 装饰了一下,cached_property 装饰器既缓存了 urlconf_module 方法的返回值且将其封装成一个属性,使其可以像属性一样直接调用
import_module 方法已经没必要再进去看了,它的作用是导包,这是python自带的方法。既然这里导入了 settings 配置文件中配置的根路由,那么我们直接去看 根路由文件里面干了什么就行
urls
根路由里的代码同样有点复杂,接下来我详细说一说每一点。
【1】这里尝试调用 re_path 函数,这个函数后面会详细讲解的,现在你只需要知道它会生成一个对象。但是他会先调用 include 函数,将 api 这个应用下的的路由全部加载进来
【2】1处尝试加载的路由,这里主要是一个列表,列表里面都是 re_path 函数
include 的详细内容
【3】判断传进来的参数是不是一个元组,但是我们传进来的是一个字符串,很明显不是。于是 urlconf_module 指向了这个字符串
【4】urlconf_module 现在是一个字符串,于是调用了 import_moudle 将 api 下的 urls 文件里的内容导入,并且用 import_moudle 接收
【5】尝试对 patterns 进行校验,其实就是 api.urls 中的列表的每一个内容。需要注意的是 urlconf_module 是 api.urls 这个模块,而 patterns 则是 api.urls 文件中的列表,二者是有区别的
【6】返回一个三元组
右侧是调试中每一个元素的值,我们浏览代码就可以推测 app_name 为 None,而在调试器中显示的却是如此。
还剩下最后一个函数 re_path,这个是当前项目中的路由的关键
re_path
re_path 其实就是 _path 函数。partial 是支持高阶函数使用的一种用法,在代码中我们可以看到,他将 _path 的某一个参数固定了,于是 _path 就转换成了两个函数使用,一个是支持普通的路由,一个是支持正则的路由。
【1】判断 view, 也就是我们传进来的到底是不是 include 函数所返回的一个列表或者元组,因为路由有分发的作用,所以他支持依据路由分发到子路由中去,这个子路由正是 include 返回的元组或者列表
【2】判断他是不是一个可调用的对象,什么叫做可调用对象,类加上括号,就是生成一个对象,所以类叫做可调用对象,函数本身就是可调用的,所以函数也叫作可调用对象,而对象本身呢,只有重写了 call 方法才会具备可调用性。这一点在 WSGI 协议中没有指出来,WSGI协议要求app必须返回一个可迭代的对象,所以这个app有三种形式。具体怎么回事,读者可以自行百度。
【3】在不满足1和2的情况下都不符合要求,但是为了打印出详细信息,所以第三种才会进行拆分
通过 _path 源码我们知道为什么 re_path 或者 path 可以接收两种形式了。
其实到此,Django 已经启动了,Django 把路由全部加载进了内存中,但是我们仍然看不到 Django 是怎么依据请求调用路由寻找到对应的视图的。这个我们将会在下一篇中探寻。