本文是2023年技术复盘文章之一。
复盘带来思考,通过思考取其精华,精进自我,提升赚钱能力。
平时对django
的源码饶有兴趣,时不时翻看源码。本篇总结重在梳理django
启动流程,了解在该过程中,到底发生了哪些事,用了哪些值得学习的方法。
注:生产环境和开发环境,仅入口文件及前面的某些流程有别而已,所以就以开发服务器启动的流程来分析。另外,我制作了一张启动的流程图,如想获取,移步文末看获取的方法。
在上一篇django是如何加载settings的复盘中已有描述,manage.py
会启动一个命令行管理工具类,命令行工具名为MangemenUtility
类,执行它的execute
方法会进入到启动流程。
django.core.management.__init__.py
def execute_from_command_line(argv=None):
"""Run a ManagementUtility."""
utility = ManagementUtility(argv)
utility.execute()
django
模块这里不再赘述,可看上一篇文章django是如何加载settings的:
在载入配置文件后,会继续载入django
的各模块,加载内置的和开发者自定义的模型类:
django.core.management.__init__.py
class ManagementUtility:
# 省略其他代码
def exexute(self):
# 省略其他代码
django.setup()
# 省略其他代码
# 省略其他代码
对django.setup()
感兴趣的可自行查看源码。
只要运行了django.setup()
,就能在当前进程中使用django
的模块了,这也是我们要在子进程中使用django
时,需要使用该方法的原因。如:
当然,这个步骤还有不少琐碎之事,如:
Command
类运行命令需要先行注明的是,该环节有点绕,涉及到三个类,分别是:
1、当前Command
类(该步骤所说的当前类均指该类),位于:
django.contrib.staticfiles.management.commands.runserver
文件中
2、父类Command
类(子类和它同名,😳,该步骤所说的父类均指该类),位于:
django.core.management.commands.runserver
文件中
3、祖父类BaseCommand
该步骤所说的当祖父类均指该类),位于:
django.core.management.base
文件中
走入这个绕脑袋的流程,就从self.fetch_command(subcommand).run_from_argv(self.argv)
开始
class ManagementUtility:
# 省略其他代码
def exexute(self):
# 省略其他代码
django.setup()
# 省略其他代码
self.fetch_command(subcommand).run_from_argv(self.argv)
# 省略其他代码
subcommand
就是一个当前的Command
类实例,由它去调用run_from_argv
方法,这个方法,在祖父类中才有:
1、跳转到祖父类中的run_from_argv
方法,在该方法中又执行了execute
方法;
def run_from_argv(self, argv):
# 省略其他代码
try:
self.execute(*args, **cmd_options)
except CommandError as e:
if options.traceback:
raise
# 省略其他代码
2、当前类中并没有execute
方法,其父类中有,但也没干什么特别的事,又super().execute()
跑回祖父类的execute
方法了,
def execute(self, *args, **options):
# 省略其他代码
# 一灯注:检查迁移文件
if self.requires_migrations_checks:
self.check_migrations()
# 一灯注:handle是个非常重要的节点的入口
output = self.handle(*args, **options)
# 一灯注:连接数据库
if output:
if self.output_transaction:
connection = connections[options.get("database", DEFAULT_DB_ALIAS)]
output = "%s\n%s\n%s" % (
self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()),
output,
self.style.SQL_KEYWORD(connection.ops.end_transaction_sql()),
)
self.stdout.write(output)
return output
3、self.handle
是个非常重要的节点入口,父类中声明了该方法必须在子类中重写,当前类并无handle
方法,那么它一定在父类中实现了:
def handle(self, *args, **options):
if not settings.DEBUG and not settings.ALLOWED_HOSTS:
raise CommandError("You must set settings.ALLOWED_HOSTS if DEBUG is False.")
# 省略其他代码
self.run(**options)
4、self.run
方法在当前类中也没有,在父类中有,self.run
又迅速跳转到self.run_inner
方法,同样的,该方法依然在父类中:
def inner_run(self, *args, **options):
# 省略其他代码
# 一灯注:检查迁移文件,和上面的检查迁移文件是二选一,即总会有一个节点是要检查的
self.check_migrations()
try:
handler = self.get_handler(*args, **options)
# 一灯注 run函数入参,传入了handler
run(
self.addr,
int(self.port),
handler,
ipv6=self.use_ipv6,
threading=threading,
on_bind=self.on_bind,
server_cls=self.server_cls,
)
except OSError as e:
# 省略其他代码
pass
这里是一个重要的分水岭,在该代码中,handler = self.get_handler(*args, **options)
能获取到一个WSGIHandler
的实例,它专门用来处理请求和响应
的,即让请求和响应
进入到django
的内部中。既然是分水岭,那它到底分了什么呢?重点如下:
self.get_handler
方法在当前类和父类都有,我们是通过当前类进来的,那么它当然会使用当前类的get_handler
方法:
def get_handler(self, *args, **options):
"""
Return the static files serving handler wrapping the default handler,
if static files should be served. Otherwise return the default handler.
"""
handler = super().get_handler(*args, **options)
use_static_handler = options["use_static_handler"]
insecure_serving = options["insecure_serving"]
if use_static_handler and (settings.DEBUG or insecure_serving):
return StaticFilesHandler(handler)
return handler
StaticFilesHandler
的源码就不贴了,它的功能是,在开发服务器环境下,包装WSGIHandler
类,增加了静态文件处理的方法。故当我们使用python manage.py runserver
启动开发服务器时,之所以能处理静态文件,关键点就在这了。请注意,在上面 run
函数中,传入了StaticFilesHandler
类实例,形参为:handler
。
再来说说,父类的get_handler
方法,比当前类的更简单,它直接获取了一个WSGIHandler
的实例,DEBUG = False
时,会使用父类的get_handler
,我们在开发服务器上设置DEBUG = False
时会无法处理静态文件,样式文件
、js文件
统统丢失。
注:
1、
WSGIHandler
有__call__
方法,上面的流程,获取到的都是它的类实例,这个类实例最后会交给底层服务器,由底层服务器调用实例,也就是调用__call__
方法;2、它在实例化时,自动加载一次中间件,并把所有中间件可调用的方法组合成一个
中间件链(middleware_chain)
,供后面所使用。
再回头看看handler = self.get_handler(*args, **options)
下方的run
函数,它由外部文件引入,运行run
方法后,启动流程就基本结束了。
run
函数该方法在django
的服务器模块中:
def run(
addr,
port,
wsgi_handler,
ipv6=False,
threading=False,
on_bind=None,
server_cls=WSGIServer,
):
server_address = (addr, port)
if threading:
httpd_cls = type("WSGIServer", (socketserver.ThreadingMixIn, server_cls), {})
else:
httpd_cls = server_cls
httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6)
if on_bind is not None:
on_bind(getattr(httpd, "server_port", port))
if threading:
httpd.daemon_threads = True
httpd.set_app(wsgi_handler)
httpd.serve_forever()
这一步,我们只关注重点:
1、httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6)
,启动了一个WSGIServer
服务器,再往下看就追溯到底层了:socketserver
服务;
2、httpd.set_app(wsgi_handler)
把WSGIHandler
或子类StaticFilesHandler
设置为底层服务socketserver
的application
3、在底层中,如果有请求,就会调用application
即applicaiton()
,把请求转交给WSGIHandler
或子类StaticFilesHandler
的__call__
方法;
4、在httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6)
中,入参WSGIRequestHandler
,它继承与wsgiref
,看源码功推测应该是把原始的请求 包装成django
的规范,以便请求进入到WSGIHandler
或子类StaticFilesHandler
的__call__
方法时,就可以直接使用了,也就是我们在视图中经常接触到的HttpReqeust
和HttpReponse
对象。
至此,整个启动流程就结束了。
WSGIHandler
或子类StaticFilesHandler
的__call__
方法是django
处理请求响应的开始,从这里开始,流程如下:
url
,获取视图这里就不展开了,内容真的很多。
1、整个流程似乎没看到什么异步操作,毕竟上下文联系得很紧凑,完成一步才能走到下一步流程
2、OOP
的继承特性发挥到极致了,但也带来一些问题,阅读源码真的不容易,除了看不懂的,思路跟着流程在各个子类、父类、祖父类之间上蹿下跳也是一个大挑战;
3、不过继承特性也有好处,至少在django
的启动流程中,开发服务器的启动和生产服务器走的流程,使用继承
特性来满足分支需求显得非常容易;
4、会想起刚接触编程没多久,强行将各种不同功能函数揉成一个类,而各种教程也非常喜欢使用Person
、Car
之类的案例来讲述类的作用,随着经验见长,不断地阅读优秀的代码之后,对类才有了一些体会,在上面的Person
和Car
的基础上,增加一点个人拙见:
一个类,旨在完成一项工作,一项工作需要很多细节,如果几项工作的细节雷同,那么可以抽象一个基类了,N项工作都能继承该基类,如果方式相同,但细节不同,也可以抽象基类,子类可实现各自的细节。
在WSGIhandler
中,为何要设计一个__call__
来处理请求和响应呢?普通类成员方法也是可调用的,只能靠猜测了:
1、可能底层服务要求入参的handler
必须是一个类实例
2、该类实例必须可调用
个人水平有限,不当之处还请指出,感激不尽。
另外,我也将上面的流程绘制成了流程图,需要的可在评论区留言。