├── chapter-04-asgi.md ├── README.md ├── chapter-02-runserver.md ├── chapter-03-wsgi.md ├── chapter-08-settings懒加载.md ├── chapter-10-FBV&CBV.md ├── chapter-06-应用及模型加载.md ├── chapter-01-自动重启.md ├── chapter-09-路由匹配.md ├── chapter-05-请求来了.md └── chapter-07-命令解析.md /chapter-04-asgi.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 序 3 | 4 | 使用 `django` 俩年之余,给大家分享下相关的源码知识。 5 | 6 | 故而总结为相关笔记。 7 | 8 | 如果如下有任何错误,请指正,谢谢 ~ 9 | 10 | 基于 2.1.x版本。 11 | 12 | ## 目录 13 | 14 | - [x] 章节01:django 是如何做到自动重启的 15 | - [x] 章节02:django runserver 16 | - [x] 章节03:django wsgi 17 | - [x] 章节04:django asgi 18 | - [x] 章节05:django 请求来了 19 | - [x] 章节06:django 应用及模型加载 20 | - [x] 章节07:django 命令解析 21 | - [x] 章节08:django settings懒加载 22 | - [x] 章节09:django 路由匹配 23 | - [x] 章节10:django FBV&CBV 24 | - [ ] 章节11:未完待续... 25 | 26 | ## Support 27 | 28 | 2020 by LiuZhiChao. 29 | -------------------------------------------------------------------------------- /chapter-02-runserver.md: -------------------------------------------------------------------------------- 1 | ## 起步 2 | 3 | 上节知悉了 `django` 是如何做到自动重启的之后,那么接下来了解下它是如何运行起一个 web 服务的。 4 | 5 | ## 开始 6 | 7 | > 很熟悉的命令 8 | 9 | 通过下面的命令,我们便可将 django 跑起来。那么它发生了什么呢? 10 | 11 | `python3 manage.py runserver` 12 | 13 | ## 简单的 web 服务 14 | 15 | 在此之前,根据 django 运行的服务,我们也利用 wsgiref 这个模块实现一个简单的 web 服务。 16 | 17 | ```python 18 | 19 | # server_demo.py 20 | 21 | from wsgiref.simple_server import WSGIServer, WSGIRequestHandler 22 | 23 | def demo_app(environ, start_response): 24 | from io import StringIO 25 | stdout = StringIO() 26 | print("Hello world!", file=stdout) 27 | print(file=stdout) 28 | h = sorted(environ.items()) 29 | for k, v in h: 30 | print(k, '=', repr(v), file=stdout) 31 | start_response("200 OK", [('Content-Type', 'text/plain; charset=utf-8')]) 32 | return [stdout.getvalue().encode("utf-8")] 33 | 34 | 35 | s = WSGIServer(("127.0.0.1", 1234), WSGIRequestHandler) 36 | # 设置应用,注意了这里不一定非是个函数,可是一切可调用的对象 37 | # 例如 函数、方法、类或实现了 __call__ 方法的实例 38 | s.set_app(demo_app) 39 | # 开始监听请求,这里应该是用的 poll 或者 selector 的网络模型。 40 | s.serve_forever() 41 | 42 | ``` 43 | 44 | 运行起来 `server_demo.py` 这个文件,然后通过浏览器访问 `127.0.0.1:1234`,会发现页面响应了一堆内容(不必关心展示的内容具体是什么)。 45 | 46 | ## django 的服务 47 | 48 | 接下来观察源码。 49 | 50 | > django/core/servers/basehttp.py 下的 run 方法 51 | 52 | ```python 53 | 54 | def run(addr, port, wsgi_handler, ipv6=False, threading=False, server_cls=WSGIServer): 55 | server_address = (addr, port) 56 | if threading: 57 | httpd_cls = type('WSGIServer', (socketserver.ThreadingMixIn, server_cls), {}) 58 | else: 59 | httpd_cls = server_cls 60 | httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6) 61 | if threading: 62 | httpd.daemon_threads = True 63 | # 这里的 wsgi_handler 来自于 64 | # from django.core.handlers.wsgi import WSGIHandler 65 | httpd.set_app(wsgi_handler) 66 | httpd.serve_forever() 67 | 68 | ``` 69 | 70 | ## WSGIHandler 71 | 72 | 这是一个 wsgi 应用,它被定义在了 `proj/wsgi.py` 下的 `application` 属性。(`application` 对应刚刚上面代码中出现的 `wsgi_handler`) 73 | 74 | `application` 这个属性对应的是个 `get_wsgi_application` 函数,该函数调用的过程中,针对 `django.core.handlers.wsgi.WSGIHandler` 这个类进行了实例化,在 `__init__` 方法中,加载了 `django` 中间件。 75 | 76 | ```python 77 | 78 | # django.core.handlers.wsgi.py 79 | class WSGIHandler(base.BaseHandler): 80 | request_class = WSGIRequest 81 | 82 | def __init__(self, *args, **kwargs): 83 | super().__init__(*args, **kwargs) 84 | # 载入中间件(注意:这里很关键。) 85 | self.load_middleware() 86 | ``` 87 | 88 | ## 总结 89 | 90 | 至此,我们了解到了 django 的服务是从 `django/core/servers/basehttp.py` 下的 `run` 方法启动了一个 `wsgi` 服务。这个可调用的对象来自于 `django.core.handlers.wsgi.WSGIHandler`,这个类在实例化(`__init__`)的过程中,加载了中间件。 91 | 92 | 我们也更应该了解下 `wsgi`。 93 | -------------------------------------------------------------------------------- /chapter-03-wsgi.md: -------------------------------------------------------------------------------- 1 | 2 | ## 起步 3 | 4 | 上节我们知道了 `django` 是如何运行起一个服务的。 5 | 6 | 但是这里面涉及到一个 `wsgi` 的知识点。 7 | 8 | ## WSGI 历史 9 | 10 | 早起 python 编写的 web 程序很难运行在 `web server (Apache、Nginx)` 上,因此一些牛逼的人于 21世纪初开发了 wsgi。 11 | 12 | 在 wsgi 出现之前,实际上有一个 `Grisha Trubetskoy` 开发的 `Apache module mod_python` 可以运行大部分Python开发的web小程序,但是它过于简单,也没有官方标准文档可以依循,最关键的是,它不够安全,这也是当时WSGI开发的关键背景。 13 | 14 | wsgi 的目的就是制定一个标准的接口规范,准确描述如何路由到 PythonWeb 应用或框架到各种 WebServer。 15 | 16 | ## WSGI 协议 17 | 18 | > 全称叫做 `Web Server Gateway Interface` 19 | 20 | 首先这个如标题所说,它是一种协议,一种规范。它不是服务器、框架、模块、API或软件。 21 | 22 | 它描述了 `web server(web服务器)` 和 `web application(web应用)` 通信的规范。这种规范在 [PEP333](https://www.python.org/dev/peps/pep-0333/) 提出。(主要为了支持3.x并提供一个通用、高级的接口。) 23 | 24 | 既然是协议、规范,那么一定是为了解决一些问题的。 25 | 26 | 如果没有该规范,一个服务器调用应用是用这种方式,另一个服务器调度是那种方式,如此的话,编写的应用部署只能局限于某些服务器,达不到通用的效果。 27 | 28 | 例如我们常常接触的 web 框架有 `django`、`flask`、`bottle` 等等,它们均支持了 `wsgi` 协议。 29 | 30 | *关于支持了 WSGI 协议的 web server,具体可参见 [Servers which support WSGI](https://wsgi.readthedocs.io/en/latest/servers.html)。* 31 | 32 | ## 为什么需要 WSGI 33 | 34 | - wsgi 被设计用于处理大量并发请求,其实我们发现各种 web 开发框架的文档发现,web 框架本身并不处理这样的需求,它们也不考虑如何与 `web server` 交互。 35 | - wsgi 加速了 `python web` 开发,只需要了解基本的 wsgi 知识就可以用 wsgi 模式部署你的 web 程序,而 wsgi 标准也让 python web 服务器中间件的开发和优化更方便。 36 | - wsgi 是规范。web 框架可以自由选择 wsgi 组件来运行 web 程序而不是修改 web 程序代码。 37 | 38 | WSGI 加快了Python web应用程序的开发,因为只需了解关于WSGI的基本信息。如果使用 `django`、`cherrypy`,则不需要关心特定框架如何利用WSGI标准。但是,了解如何实现 WSGI 将有非常大的好处。 39 | 40 | ## WSGI 接口 41 | 42 | ### 概念 43 | 44 | WSGI接口有两个方面:服务器(网关)方面,以及应用程序(框架)方面。 45 | 46 | 服务器端调用应用程序端提供的可调用对象。如何提供该对象的细节取决于服务器或网关。假设某些服务器或网关需要应用程序的部署人员编写一个简短的脚本来创建服务器或网关的实例,并为其提供应用程序对象。其他服务器和网关可以使用配置文件或其他机制来指定应该从何处导入应用程序对象或以其他方式获取应用程序对象。 47 | 48 | ### 要求 49 | 50 | WSGI 对于 `application` 对象有如下三点要求: 51 | 52 | - 必须是一个可调用的的对象。 53 | - 接收俩个必选参数 `environ`、`start_response`。 54 | - 返回值必须是可迭代对象,用来表示 `http body`。 55 | 56 | ### 做什么 57 | 58 | - `web server` 负责从客户端接收请求,将 `request` 转发给 `application`,继而将 `application` 返回的 `response` 返回给客户端。 59 | - `application` 接收由 `web server` 转发的 `request`,并将处理结果返回给 `server`。 60 | 61 | 像 `django`、`falsk`、`bottle` 等框架都有自己实现的简单 `WSGI Server`,但是这种一般用于开发环境下调试,生产环境下建议使用其它的 `wsgi server`。 62 | 63 | ## 参考文献 64 | 65 | - [WSGI: The Server-Application Interface for Python](https://www.toptal.com/python/pythons-wsgi-server-application-interface) 66 | - [An Introduction to Python WSGI Servers](https://www.appdynamics.com/blog/engineering/an-introduction-to-python-wsgi-servers-part-1/) 67 | - [PEP333](https://www.python.org/dev/peps/pep-0333/) 68 | - [做python Web开发你要理解:WSGI & uwsgi](https://www.jianshu.com/p/679dee0a4193) 69 | - [理解Python WSGI](letiantian.me/2015-09-10-understand-python-wsgi/) 70 | - [花了两个星期,我终于把 WSGI 整明白了](https://juejin.im/post/5cff300a6fb9a07ef06f8a43#heading-8) 71 | -------------------------------------------------------------------------------- /chapter-08-settings懒加载.md: -------------------------------------------------------------------------------- 1 | ## 起步 2 | 3 | 在之前几篇的描述中,在 `django` 的配置文件 `settings` 也能看到一些配置的引用。例如 `INSTALLED_APPS`、`MIDDLEWARE`、`ROOT_URLCONF` 等配置。 4 | 5 | 那么这个配置模块是如何实现的呢? 6 | 7 | ## 开始 8 | 9 | 一般我们引用 django 配置推荐这样引入 10 | 11 | ```python 12 | from django.conf import settings 13 | ``` 14 | 15 | 而不是 16 | 17 | ```python 18 | from proj import settings 19 | ``` 20 | 21 | 如果使用第二种形式的话,`settings` 中引用一些其它模块的话,那么会有可能造成循环引用。使用第一种形式的话,django 使用的是**懒加载**机制(用到的时候才加载)。 22 | 23 | 详情我们可以通过源码去查看。 24 | 25 | ## 了解 26 | 27 | ```python 28 | 29 | settings = LazySettings() 30 | 31 | class LazySettings(LazyObject): 32 | def _setup(self, name=None): 33 | # 从环境变量中提取 settings 模块路径 34 | settings_module = os.environ.get(ENVIRONMENT_VARIABLE) 35 | self._wrapped = Settings(settings_module) 36 | 37 | def __getattr__(self, name): 38 | """返回设置的值并缓存在 __dict__ 中""" 39 | if self._wrapped is empty: 40 | self._setup(name) 41 | val = getattr(self._wrapped, name) 42 | self.__dict__[name] = val 43 | return val 44 | 45 | def __setattr__(self, name, value): 46 | if name == '_wrapped': 47 | self.__dict__.clear() 48 | else: 49 | self.__dict__.pop(name, None) 50 | super().__setattr__(name, value) 51 | 52 | ... # 省略其它函数 53 | ``` 54 | 55 | 所谓懒加载,就是需要的时候才去加载。django 通过代理类 `LazyObject` 实现这个机制。加载函数是 `_setup`,当获取属性时才去加载,并缓存至实例的 `__dict__` 中。 56 | 57 | `LazySettings` 继承了 `LazyObject`,重写了 `__setattr__` 和 `__getattr__`,假设调用 `settings.DEBUG` 属性时,会调用 `__getattr__` 方法实现。 58 | 59 | 自此,我们可以观察到,所有的属性都是从 `_wrapped` (也就是 `Settings(settings_module)` 实例)这个私有属性中获取到的。 60 | 61 | ## 配置加载 62 | 63 | 上述我们了解到从环境变量中提取 `settings` 模块的路径,继而 `_wrapped` 属性指向 `Settings` 这个类的实例。 64 | 65 | ```python 66 | class Settings: 67 | def __init__(self, settings_module): 68 | # 读取默认配置 69 | for setting in dir(global_settings): 70 | if setting.isupper(): 71 | setattr(self, setting, getattr(global_settings, setting)) 72 | 73 | # 配置模块 74 | self.SETTINGS_MODULE = settings_module 75 | 76 | # 动态导入 77 | mod = importlib.import_module(self.SETTINGS_MODULE) 78 | 79 | tuple_settings = ( 80 | "INSTALLED_APPS", 81 | "TEMPLATE_DIRS", 82 | "LOCALE_PATHS", 83 | ) 84 | self._explicit_settings = set() 85 | # 读取配置模块下的属性(可能会覆盖一些默认配置) 86 | for setting in dir(mod): 87 | if setting.isupper(): 88 | setting_value = getattr(mod, setting) 89 | setattr(self, setting, setting_value) 90 | self._explicit_settings.add(setting) 91 | ``` 92 | 93 | ## 总结 94 | 95 | 综上,我们在读取 `settings` 某个配置时,会触发 `__getattr__` 方法,如果 `_wrapped` 为空,则调用 `_setup` 方法,这个方法内部获取配置文件模块,`_wrapped` 属性指向 `Settings` 类的实例,这个类在实例化的时候,构造函数先读取 `global_settings` 来设置一些默认属性,接着通过动态导入模块的形式 `importlib.import_module` 加载配置模块的属性,继而读取的属性从 `_wrapped` 中获取。 96 | -------------------------------------------------------------------------------- /chapter-10-FBV&CBV.md: -------------------------------------------------------------------------------- 1 | ## 起步 2 | 3 | `django` 是支持视图进行 函数式 或 类 进行编写的。也就是我们所说的 `FBV` 和 `CBV`。 4 | 5 | 那么在请求信息匹配到相应的路由之后,这俩种模式是作了如何的处理的呢? 6 | 7 | ## 开始 8 | 9 | 在 `chapter-04-请求来了` 这个章节中,我们大概了解到路由匹配成功之后,会构造一个类似于一个函数的东西进行调用。 10 | 11 | 那么 `FBV` 模式我们没话说,直接调用函数传参数即可。 12 | 13 | 那么 `CBV` 模式是如何进行分发的。 14 | 15 | 在路由映射中,我们是这么配置路由和视图的: 16 | 17 | ```python 18 | 19 | from django.urls import path 20 | 21 | from . import views 22 | 23 | urlpatterns = [ 24 | # FBV (函数式我们没什么可说的,直接就到达了函数的内部) 25 | path("fbv/", views.fbv_api) 26 | # CBV 27 | path("cbv/", views.CbvApi.as_view()), 28 | ] 29 | ``` 30 | 31 | ## 潜入 32 | 33 | 配置 `CBV` 视图的时候,类调用了 `as_view()` 方法,我们来看看它干了什么? 34 | 35 | ```python 36 | 37 | # 写 CBV 视图的时候,默认视图类要继承来自于 django 提供的 View 类 38 | # 它来自于:from django.views.generic import View 39 | 40 | class View: 41 | 42 | ... 43 | 44 | @classonlymethod 45 | def as_view(cls, **initkwargs): 46 | """请求、响应的主要入口点""" 47 | 48 | for key in initkwargs: 49 | # 这里进行了一些参数初始化预检, 不必关心 50 | ... 51 | 52 | # 声明一个视图函数 53 | def view(request, *args, **kwargs): 54 | self = cls(**initkwargs) 55 | if hasattr(self, 'get') and not hasattr(self, 'head'): 56 | self.head = self.get 57 | self.request = request 58 | self.args = args 59 | self.kwargs = kwargs 60 | return self.dispatch(request, *args, **kwargs) 61 | 62 | # 赋值属性 63 | view.view_class = cls 64 | view.view_initkwargs = initkwargs 65 | 66 | # 以下这俩个的调用我们不必太过于关心。 67 | # 更新包装函数,使其看起来与包装函数相似 68 | update_wrapper(view, cls, updated=()) 69 | # and possible attributes set by decorators 70 | # like csrf_exempt from dispatch 71 | update_wrapper(view, cls.dispatch, assigned=()) 72 | 73 | # 这个方法返回了 view 视图函数 74 | return view 75 | ``` 76 | 77 | 到这里已然了解到,其实 `CBV` 最终得到的结果也是个函数。 78 | 79 | ## 了然于心 80 | 81 | 当路由匹配成功之后,`CBV` 模式映射到视图调用的时候,会调用 `view` 函数。 82 | 83 | ```python 84 | 85 | def view(request, *args, **kwargs): 86 | # 这里的 cls 表示你的那个视图类 87 | self = cls(**initkwargs) 88 | # 如果视图实现了 get 方法 和没有实现 head 方法,head 方法表示 get 方法。 89 | # 似乎没什么卵用。 90 | if hasattr(self, 'get') and not hasattr(self, 'head'): 91 | self.head = self.get 92 | 93 | # 将一些属性赋值到类实例上面 94 | self.request = request 95 | self.args = args 96 | self.kwargs = kwargs 97 | # 调用视图类的 dispatch 方法,接着向下看 dispatch 方法做了什么 98 | return self.dispatch(request, *args, **kwargs) 99 | 100 | ``` 101 | 102 | ```python 103 | 104 | def dispatch(self, request, *args, **kwargs): 105 | # 映射请求方式的方法,就是查看你有木有实现 http 的请求方法 106 | # 如果没有,会提醒 http 方法不被允许的错误提示 107 | if request.method.lower() in self.http_method_names: 108 | handler = getattr(self, request.method.lower(), self.http_method_not_allowed) 109 | else: 110 | handler = self.http_method_not_allowed 111 | # 这里就进入你的视图逻辑 112 | return handler(request, *args, **kwargs) 113 | 114 | ``` 115 | 116 | ## 总结 117 | 118 | `django` 提供了 `FBV` & `CBV` 俩种模式进行视图的编写。主要突出在了 `CBV` 上面,进行配置的时候使用了 `View.as_view()` 方法,`as_view` 返回了 `view` 函数,这样请求进来调用了 `view` 方法,继而又调用了类的 `dispatch` 方法,通过反射映射到请求方法。 119 | 120 | `CBV` 的模式优势在于,它可以给我们提供了很多可扩展的方法,比如在 `dispatch` 处进行一些定制,或者视图的属性封装。 121 | -------------------------------------------------------------------------------- /chapter-06-应用及模型加载.md: -------------------------------------------------------------------------------- 1 | ## 起步 2 | 3 | 上节我们了解到,`django` 是如何进行处理请求的。 4 | 5 | 那么 `django` 是如何进行 `app` 模块导入的。 6 | 7 | 我们所知道的。 8 | 9 | 运行 `django` 应用分为 生产 和 本地开发 模式。一个通过 `wsgi.py` 来达到生产环境的运行,另一个通过 `manage.py runserver` 来达到本地开发的运行。 10 | 11 | 但是我们发现,这俩处入口均有 `django.setup()` 这一步。 12 | 13 | *这俩点均有一些不同之处,但是整体功能相似。* 14 | 15 | ## 开始 16 | 17 | 我们从 `setup` 这个功能函数说起。 18 | 19 | ```python 20 | 21 | def setup(set_prefix=True): 22 | """配置 logging 及应用注册。 23 | 24 | """ 25 | # 这里的 apps 是一个单例模式,指向 django/apps/registry 下的 Apps 这个类 26 | from django.apps import apps 27 | from django.conf import settings 28 | from django.urls import set_script_prefix 29 | from django.utils.log import configure_logging 30 | 31 | # 从 settings 文件下读取 LOGGING_CONFIG 配置日志文件 32 | configure_logging(settings.LOGGING_CONFIG, settings.LOGGING) 33 | # 这里只针对开发模式下有效,生产环境忽略 34 | if set_prefix: 35 | set_script_prefix( 36 | '/' if settings.FORCE_SCRIPT_NAME is None else settings.FORCE_SCRIPT_NAME 37 | ) 38 | # 重点其实在这里,从 settings 文件获取 INSTALLED_APPS 配置 39 | apps.populate(settings.INSTALLED_APPS) 40 | 41 | ``` 42 | 43 | 接下来,我们移步到 `populate` 下的函数。 44 | 45 | ```python 46 | 47 | def populate(self, installed_apps=None): 48 | """加载应用配置和模型 49 | 50 | 导入每个应用程序模块,然后导入每个模型模块。 51 | 它是线程安全的、幂等的,但不是可重入的。 52 | 53 | """ 54 | if self.ready: 55 | return 56 | 57 | # 在初始化 WSGI 回调之前,在服务器上创建的线程可能会并行调用 `populate` 58 | # 此处设置了线程锁 59 | with self._lock: 60 | if self.ready: 61 | return 62 | 63 | # RLock 防止其他线程进入这个部分。下面的比较和设置操作是原子性的。 64 | if self.loading: 65 | # Prevent reentrant calls to avoid running AppConfig.ready() 66 | # methods twice. 67 | raise RuntimeError("populate() isn't reentrant") 68 | self.loading = True 69 | 70 | # 阶段 1: 初始化 app 配置和导入 app 模型 71 | for entry in installed_apps: 72 | if isinstance(entry, AppConfig): 73 | app_config = entry 74 | else: 75 | # 这里通过一个工厂方法创造出一个应用实例 76 | app_config = AppConfig.create(entry) 77 | if app_config.label in self.app_configs: 78 | raise ImproperlyConfigured( 79 | "Application labels aren't unique, " 80 | "duplicates: %s" % app_config.label) 81 | 82 | # 载入到 app_configs 配置 83 | # key 为 应用标签,值为 app 实例 84 | self.app_configs[app_config.label] = app_config 85 | app_config.apps = self 86 | 87 | # 检测重复的 app 名称,确保唯一性 88 | counts = Counter( 89 | app_config.name for app_config in self.app_configs.values()) 90 | duplicates = [ 91 | name for name, count in counts.most_common() if count > 1] 92 | if duplicates: 93 | raise ImproperlyConfigured( 94 | "Application names aren't unique, " 95 | "duplicates: %s" % ", ".join(duplicates)) 96 | 97 | # 设置应用加载完成标识位 98 | self.apps_ready = True 99 | 100 | # 阶段 2: 导入模型模块 101 | for app_config in self.app_configs.values(): 102 | app_config.import_models() 103 | 104 | # 清空缓存 105 | self.clear_cache() 106 | 107 | # 设置模型加载完成标识位 108 | self.models_ready = True 109 | 110 | # 阶段 3: 运行 app 下的 ready 方法(钩子方法) 111 | for app_config in self.get_app_configs(): 112 | app_config.ready() 113 | 114 | # 初始化完成 115 | self.ready = True 116 | 117 | ``` 118 | 119 | ## 总结 120 | 121 | 到此,我们在 `run` 一个 django 应用的时候,会调用 `django` 的 `setup` 方法,主要是载入所有的应用和模型。 122 | -------------------------------------------------------------------------------- /chapter-01-自动重启.md: -------------------------------------------------------------------------------- 1 | ## 初试 - 文件变化后 `server` 自动重启 2 | 3 | > 在此之前,不妨先了解下 `django` 是如何做到自动重启的 4 | 5 | ### 开始 6 | 7 | `django` 使用 `runserver` 命令的时候,会启动俩个进程。 8 | 9 | `runserver` 主要调用了 `django/utils/autoreload.py` 下 `main` 方法。 10 | *至于为何到这里的,我们这里不作详细的赘述,后面篇章会进行说明。* 11 | 12 | 主线程通过 `os.stat` 方法获取文件最后的修改时间进行比较,继而重新启动 `django` 服务(也就是子进程)。 13 | 14 | 大概每秒监控一次。 15 | 16 | ```python 17 | # django/utils/autoreload.py 的 reloader_thread 方法 18 | 19 | def reloader_thread(): 20 | ... 21 | # 监听文件变化 22 | # -- Start 23 | # 这里主要使用了 `pyinotify` 模块,因为目前可能暂时导入不成功,使用 else 块代码 24 | # USE_INOTIFY 该值为 False 25 | if USE_INOTIFY: 26 | fn = inotify_code_changed 27 | else: 28 | fn = code_changed 29 | # -- End 30 | while RUN_RELOADER: 31 | change = fn() 32 | if change == FILE_MODIFIED: 33 | sys.exit(3) # force reload 34 | elif change == I18N_MODIFIED: 35 | reset_translations() 36 | time.sleep(1) 37 | ``` 38 | 39 | `code_changed` 根据每个文件的最好修改时间是否发生变更,则返回 `True` 达到重启的目的。 40 | 41 | ### 父子进程&多线程 42 | 43 | 关于重启的代码在 `python_reloader` 函数内 44 | 45 | ```python 46 | 47 | # django/utils/autoreload.py 48 | 49 | def restart_with_reloader(): 50 | import django.__main__ 51 | while True: 52 | args = [sys.executable] + ['-W%s' % o for o in sys.warnoptions] 53 | if sys.argv[0] == django.__main__.__file__: 54 | # The server was started with `python -m django runserver`. 55 | args += ['-m', 'django'] 56 | args += sys.argv[1:] 57 | else: 58 | args += sys.argv 59 | new_environ = {**os.environ, 'RUN_MAIN': 'true'} 60 | exit_code = subprocess.call(args, env=new_environ) 61 | if exit_code != 3: 62 | return exit_code 63 | 64 | 65 | def python_reloader(main_func, args, kwargs): 66 | # 一开始环境配置是没有该变量的,所有走的是 else 语句块 67 | if os.environ.get("RUN_MAIN") == "true": 68 | # 开启一个新的线程启动服务 69 | _thread.start_new_thread(main_func, args, kwargs) 70 | try: 71 | # 程序接着向下走,监控文件变化 72 | # 文件变化,退出该进程,退出码反馈到了 subprocess.call 接收处... 73 | reloader_thread() 74 | except KeyboardInterrupt: 75 | pass 76 | else: 77 | try: 78 | # 而在 restart_with_reloader 这个函数设置了 RUN_MAIN 变量 79 | exit_code = restart_with_reloader() 80 | if exit_code < 0: 81 | os.kill(os.getpid(), -exit_code) 82 | else: 83 | sys.exit(exit_code) 84 | except KeyboardInterrupt: 85 | pass 86 | ``` 87 | 88 | 程序启动,因为没有 `RUN_MAIN` 变量,所以走的 else 语句块。 89 | 90 | 颇为有趣的是,`restart_with_reloader` 函数中使用 `subprocess.call` 方法执行了启动程序的命令( e.g:python3 manage.py runserver ),此刻 `RUN_MAIN` 的值为 `True` ,接着执行 `_thread.start_new_thread(main_func, args, kwargs)` 开启新线程,意味着启动了 `django` 服务。 91 | 92 | 如果子进程不退出,则停留在 `call` 方法这里(进行请求处理),如果子进程退出,退出码不是3,while 则被终结。反之就继续循环,重新创建子进程。 93 | 94 | ### 检测文件修改 95 | 96 | 具体检测文件发生改变的函数实现。 97 | 98 | ```python 99 | 100 | # django/utils/autoreload.py 101 | 102 | def code_changed(): 103 | global _mtimes, _win 104 | # 获取所有文件 105 | for filename in gen_filenames(): 106 | # 通过 os 模块查看每个文件的状态 107 | stat = os.stat(filename) 108 | # 获取最后修改时间 109 | mtime = stat.st_mtime 110 | if _win: 111 | mtime -= stat.st_ctime 112 | if filename not in _mtimes: 113 | _mtimes[filename] = mtime 114 | continue 115 | # 比较是否修改 116 | if mtime != _mtimes[filename]: 117 | _mtimes = {} 118 | try: 119 | del _error_files[_error_files.index(filename)] 120 | except ValueError: 121 | pass 122 | return I18N_MODIFIED if filename.endswith('.mo') else FILE_MODIFIED 123 | return False 124 | ``` 125 | 126 | ### 总结 127 | 128 | 以上就是 `django` 检测文件修改而达到重启服务的实现流程。 129 | 130 | 结合 `subprocess.call` 和 环境变量 创建俩个进程。主进程负责监控子进程和重启子进程。 131 | 子进程下通过开启一个新线程(也就是 `django` 服务)。主线程监控文件变化,如果变化则通过 `sys.exit(3)` 来退出子进程,父进程获取到退出码不是3则继续循环创建子进程,反之则退出整个程序。 132 | 133 | 好,到这里。我们勇敢的迈出了第一步,我们继续下一个环节!!! ヾ(◍°∇°◍)ノ゙ 134 | -------------------------------------------------------------------------------- /chapter-09-路由匹配.md: -------------------------------------------------------------------------------- 1 | ## 起步 2 | 3 | 接下来,我们可以了解下请求进来,是怎么进行路由匹配的。 4 | 5 | 在 `chapter-04-请求来了` 我们简单在那里提及过请求的匹配,但是并没有详细说明。 6 | 7 | 本篇章会了解到,我们编写的 `urls.py` 文件是如何加载至 django 的路由系统,以及是如何进行匹配的。 8 | 9 | ## 开始 10 | 11 | 在 `chapter-04-请求来了` 文章中,请求来了会映射到 `_get_response` 这个函数中去,其中有一段就是解析请求路径的,如下: 12 | 13 | ```python 14 | 15 | # 这里的 request 是否有 urlconf 属性,都会获得一个解析器 16 | if hasattr(request, 'urlconf'): 17 | urlconf = request.urlconf 18 | set_urlconf(urlconf) 19 | resolver = get_resolver(urlconf) 20 | else: 21 | resolver = get_resolver() 22 | 23 | resolver_match = resolver.resolve(request.path_info) 24 | callback, callback_args, callback_kwargs = resolver_match 25 | request.resolver_match = resolver_match 26 | 27 | ``` 28 | 29 | > *其它的暂不做分析,我们只针对路由匹配做解析* 30 | 31 | 接下来,我们先看第一步判断那里,这里面有个共同点,就是都会调用 `get_resolver` 这个方法,区别在于是否传递了 `urlconf` 这个参数。 32 | 33 | 我们可以看下 `get_resolver` 这个方法。 34 | 35 | ```python 36 | 37 | # 这里涉及到一个 lru,关于 lru 后续我们会有一个小的讲解。 38 | @functools.lru_cache(maxsize=None) 39 | def get_resolver(urlconf=None): 40 | # 从上述来看,似乎设置与否 `urlconf` 参数,这里都会进行检测 41 | # 如果没有传递,就进行设置,值为 settings下的 ROOT_URLCONF 42 | # 这个参数指向了 django 的路由根配置,也就是 `project.urls` 43 | if urlconf is None: 44 | urlconf = settings.ROOT_URLCONF 45 | 46 | # URLResolver 类实例化并传递了俩个参数 并返回该实例 47 | # 暂且先不必关心构造方法内部做了什么。 48 | return URLResolver(RegexPattern(r'^/'), urlconf) 49 | 50 | ``` 51 | 52 | 当调用 `get_resolver` 方法后,得到一个 `URLResolver` 类的实例,紧接着这个实例调用了 `resolve` 方法,并把当前请求路径( `e.g /api/banners/` )作为参数传递。 53 | 54 | ```python 55 | 56 | class URLResolver: 57 | 58 | def resolve(self, path): 59 | # 路径有可能是 reverse_lazy 对象,所以 str 一下 60 | path = str(path) 61 | tried = [] 62 | # 这一步是 正则校验,路径是否以 `/` 开头,匹配不到返回 404 63 | # match 很关键,后续可能会持续调用,进行匹配 64 | match = self.pattern.match(path) 65 | if match: 66 | # 从匹配中获取相关的匹配项 67 | # e.g 路径 -> str, 未命名参数 -> tuple, 命名参数 -> dict 68 | new_path, args, kwargs = match 69 | # url_patterns > List[URLResolver] 70 | for pattern in self.url_patterns: 71 | try: 72 | # 针对匹配到路径进行解析 73 | # ResolverMatch(func=func, args=(), kwargs={}, url_name=None, app_names=[], namespaces=[]) 74 | sub_match = pattern.resolve(new_path) 75 | except Resolver404 as e: 76 | ... 77 | else: 78 | if sub_match: 79 | ... 80 | # 返回解析匹配 81 | return ResolverMatch( 82 | sub_match.func, 83 | sub_match_args, 84 | sub_match_dict, 85 | sub_match.url_name, 86 | [self.app_name] + sub_match.app_names, 87 | [self.namespace] + sub_match.namespaces, 88 | ) 89 | tried.append([pattern]) 90 | raise Resolver404({'tried': tried, 'path': new_path}) 91 | raise Resolver404({'path': path}) 92 | 93 | def __init__(self, pattern, urlconf_name, default_kwargs=None, app_name=None, namespace=None): 94 | # 指向的是 RegexPattern 对象 95 | # 关于该对象是一个类似于 re 模块,可以进行正则匹配 96 | # 正则表达式为 '^/' 也就是说必须以 `/` 开头 97 | self.pattern = pattern 98 | # 它可以是一个模块、或是具有 `urlpattern` 属性的对象 99 | self.urlconf_name = urlconf_name 100 | 101 | # 其它参数 102 | self.callback = None 103 | self.default_kwargs = default_kwargs or {} 104 | self.namespace = namespace 105 | self.app_name = app_name 106 | self._reverse_dict = {} 107 | self._namespace_dict = {} 108 | self._app_dict = {} 109 | # set of dotted paths to all functions and classes that are used in 110 | # urlpatterns 111 | self._callback_strs = set() 112 | self._populated = False 113 | self._local = threading.local() 114 | 115 | ``` 116 | 117 | `resolve` 方法内部做路由解析,如果解析成功返回 `ResolverMatch` 对象,里面包含了解析的函数及相关参数等。 118 | 119 | 反之返回 `404`。 120 | 121 | ## 总结 122 | 123 | 当请求来临之后,`wsgi` 模块将请求信息转发到我们的 `django` 程序,`django` 会预先加载一个根路由解析器( `URLResolver` 对象 )。(根节点是项目下的 `urls.py` ) 124 | 125 | 在路由进行匹配的时候,调用 `URLResolver` 下的 `resolve` 方法,持续遍历 `url_patterns` 属性(这里也就是我们的 `url.py` 为什么要设置 `url_patterns` 属性)进行匹配。 126 | 127 | 解析成功就可以获取到视图方法及相关参数。 128 | -------------------------------------------------------------------------------- /chapter-05-请求来了.md: -------------------------------------------------------------------------------- 1 | ## 起步 2 | 3 | 上节我们了解到,`WSGI` 可以将请求信息转发给我们的应用。 4 | 5 | 这一点,原理以及已经请求的处理我们已经知道了,本节我们将专注于应用 `django` 是如何进行处理 `WSGI` 转发过来的请求的。 6 | 7 | ## 开始 8 | 9 | ```python 10 | # django.core.handlers.wsgi.py 11 | 12 | class WSGIHandler(base.BaseHandler): 13 | request_class = WSGIRequest 14 | 15 | def __init__(self, *args, **kwargs): 16 | super().__init__(*args, **kwargs) 17 | self.load_middleware() 18 | 19 | def __call__(self, environ, start_response): 20 | """请求会被转发到这里 21 | 22 | """ 23 | # 似乎是加 `URL` 后缀用的, 暂时不够清晰 24 | set_script_prefix(get_script_name(environ)) 25 | # 发送请求来了的信号, 关于信号的注册及处理我们在后面的篇章会进行介绍 26 | signals.request_started.send(sender=self.__class__, environ=environ) 27 | # 封装 `request` 请求对象 处理类是 -> WSGIRequest 28 | request = self.request_class(environ) 29 | 30 | # 划重点 -> 接下来我们看它背后发生了什么? (子类没有就去父类里面去找) 31 | response = self.get_response(request) 32 | 33 | # 下面的待上述讲解结束, 客官继续向下看(代码已被省略)... 34 | ... 35 | ``` 36 | 37 | 紧跟着,情节发展到了 `get_response` 这个方法这里。 38 | 39 | ```python 40 | # django.core.handlers.base.py BaseHandler 41 | 42 | class BaseHandler: 43 | _view_middleware = None 44 | _template_response_middleware = None 45 | _exception_middleware = None 46 | _middleware_chain = None 47 | 48 | def get_response(self, request): 49 | # 设置 url 解析器, 里面具体发生了我们暂且不关心。 50 | # 这里为后续的路由匹配做好了准备 51 | set_urlconf(settings.ROOT_URLCONF) 52 | 53 | # _middleware_chain 方法是什么 ... 54 | # 这里可能会有一些绕, 这个属性是在调用 `load_middleware` 这里赋值 55 | # 我们将视线转移到 `load_middleware` 处 ... 56 | # ---- 时间线 ---- 57 | # 通过 `load_middleware` 方法我们知道了 `_middleware_chain` 是个什么东西。 58 | # 有可能是 `_get_response` 或 某个中间件实例(实例内部有个属性是 `_get_response` ) 59 | # 假设它是个中间件实例, 请各位继续移步向下看。 60 | response = self._middleware_chain(request) 61 | 62 | # 这里删除了一些似乎不太重要的代码, 也就是说直接响应了 `response` 对象 63 | ... 64 | 65 | return response 66 | 67 | def load_middleware(self): 68 | """该方法在 runserver 的时候实例化应用的时候,在 __init__ 方法中调用了该方法 69 | 70 | """ 71 | 72 | self._view_middleware = [] 73 | self._template_response_middleware = [] 74 | self._exception_middleware = [] 75 | 76 | # `convert_exception_to_response` 写法上是一个装饰器。 77 | # 这个方法里面完成了对响应的一些异常的捕获 78 | # 比如 django 404、500 等页面提示以及请求异常信号的发送 79 | # 现在 `handler` 指向 `_get_response` 这个方法 80 | handler = convert_exception_to_response(self._get_response) 81 | 82 | # 获取 settings 中间件的配置 83 | for middleware_path in reversed(settings.MIDDLEWARE): 84 | # 动态导入模块 85 | middleware = import_string(middleware_path) 86 | try: 87 | # 注意:这里实例化的时候, 将 `handler` 传递为了属性 88 | mw_instance = middleware(handler) # 实例化 89 | except MiddlewareNotUsed as exc: 90 | # 一些不太重要的代码被我删除了.. 91 | ... 92 | 93 | # 塞入中间件实现的方法 94 | if hasattr(mw_instance, 'process_view'): 95 | self._view_middleware.insert(0, mw_instance.process_view) 96 | if hasattr(mw_instance, 'process_template_response'): 97 | self._template_response_middleware.append(mw_instance.process_template_response) 98 | if hasattr(mw_instance, 'process_exception'): 99 | self._exception_middleware.append(mw_instance.process_exception) 100 | 101 | # 这里的 handler 被重新赋值,变为了中间件实例(似乎是一直不断的被重新赋值) 102 | handler = convert_exception_to_response(mw_instance) 103 | 104 | # 这个属性可能是 `__get_response` 方法 或 是一个中间件实例 105 | self._middleware_chain = handler 106 | ``` 107 | 108 | 上述我们了解到,通过实现了 `wsgi` 框架跑起来一个服务的时候,将 `django.core.handlers.wsgi.py` 下的 `WSGIHandler` 的实例 设置为了应用程序。 109 | 110 | `WSGIHandler` 实例化的时候,执行了 `load_middleware` 方法,该方法载入了 `django` 的中间件,并设置了 `_middleware_chain` 属性。(该属性有可能是 `_get_response` 或 某个中间件的实例(实例中有 `_get_response` 一个属性)。) 111 | 112 | 请求被转发到应用程序 `django`,也就是 `WSGIHandler` 下的 `__call__`,这里设置了路由解析器,并调用了 `_middleware_chain`,返回值是一个 `response` 对象。 113 | 114 | 接下来,我们看下 `_middleware_chain` 发生了什么? 115 | 116 | ## 渐入佳境 117 | 118 | 既然上文我们设定 `_middleware_chain` 是一个中间件,且是一个类的实例,那么问题来了? 119 | 120 | 实例加括号调用什么方法? 答案是类的 `__call__` 方法。 121 | 122 | `__call__` 方法被定义在了 `django.utils.deprecation` 下的 `MiddlewareMixin`。(继承它的子类并没有实现 `__call__` 方法。) 123 | 124 | ```python 125 | 126 | # django.utils.deprecation.py 127 | 128 | class MiddlewareMixin: 129 | def __init__(self, get_response=None): 130 | # 还记得上文提到的 `get_response` 吗? 131 | self.get_response = get_response 132 | super().__init__() 133 | 134 | def __call__(self, request): 135 | """ 上文我们提到的执行 `_middleware_chain`,其实就是执行到了这里。 136 | 137 | """ 138 | response = None 139 | # 开始处理中间件方法, 请求来了... 140 | if hasattr(self, 'process_request'): 141 | # 如果中间件方法返回了 response 对象,相当于截断了后续的操作。 142 | response = self.process_request(request) 143 | # 真正有趣的地方来了, 我们假设中间件方法没有拦截,执行了 `get_response` 方法。 144 | # 请求处理的逻辑就是在这里了 ... 我们继续向下看 145 | response = response or self.get_response(request) 146 | # 继续执行中间件方法 147 | if hasattr(self, 'process_response'): 148 | response = self.process_response(request, response) 149 | return response 150 | ``` 151 | 152 | 上述主要调用了中间件的 `__call__` 方法,在该方法中,执行了中间件相关处理方法,继续向下剖析。 153 | 154 | ```python 155 | 156 | # django.core.handlers.base.py BaseHandler 下的 `_get_response` 方法 157 | 158 | def _get_response(self, request): 159 | """解析并调用视图,以及 视图、异常和模板响应 中间件。 160 | 161 | 这个方法是 请求/响应 中间件中发生的所有事情。 162 | """ 163 | response = None 164 | 165 | # 这个里面主要是根据 settings 配置的 url 节点,导入并获取一个解析器对象。 166 | if hasattr(request, 'urlconf'): 167 | urlconf = request.urlconf 168 | set_urlconf(urlconf) 169 | resolver = get_resolver(urlconf) 170 | else: 171 | resolver = get_resolver() 172 | 173 | # 根据访问的路由获取 django 路由解析器中的匹配的视图 174 | resolver_match = resolver.resolve(request.path_info) 175 | callback, callback_args, callback_kwargs = resolver_match 176 | request.resolver_match = resolver_match 177 | 178 | # 执行含有 `process_view` 方法的中间件 179 | for middleware_method in self._view_middleware: 180 | response = middleware_method(request, callback, callback_args, callback_kwargs) 181 | if response: 182 | break 183 | 184 | # 如果响应对象并没有拦截,执行视图函数 185 | if response is None: 186 | # 保证视图的原子性,这里的变量表示视图 187 | wrapped_callback = self.make_view_atomic(callback) 188 | try: 189 | # django 是视图写法有俩种,FBV 和 CBV 190 | # CBV 会调用 `as_view` 方法,内部实现了 view 函数,然后该函数分发到了类的 `dispatch` 方法,通过反射映射到具体的方法。 191 | # 其实本质上这里都是在调用视图函数。 192 | # 这一步就是真正在执行您的视图内的逻辑啦。 193 | response = wrapped_callback(request, *callback_args, **callback_kwargs) 194 | except Exception as e: 195 | # 如果异常,这里捕获掉,执行含有 `process_exception` 方法的中间件 196 | response = self.process_exception_by_middleware(e, request) 197 | 198 | if response is None: 199 | # 这里告知你,要必须返回一个 `response` 对象。 200 | ... 201 | 202 | # 如果上面的视图中间件返回有 response 对象且可被调用 203 | elif hasattr(response, 'render') and callable(response.render): 204 | # 执行含有 `process_template_response` 方法的中间件 205 | for middleware_method in self._template_response_middleware: 206 | response = middleware_method(request, response) 207 | 208 | try: 209 | response = response.render() 210 | except Exception as e: 211 | # 如果发生异常,这里捕获掉,执行含有 `process_exception` 方法的中间件 212 | response = self.process_exception_by_middleware(e, request) 213 | 214 | # 响应 response 对象。 215 | return response 216 | 217 | ``` 218 | 219 | 到这里,通过上述剖析,如果请求进来了,我们知悉了请求是如何到我们的视图逻辑中的。 220 | 221 | ## 总结 222 | 223 | 当我们的服务跑起来的时候,设置应用处理器 `WSGIHandler` 实例化的时候加载了中间件的操作。 224 | 225 | 请求来了,请求被转发到了 `WSGIHandler` 下的 `__call__` 方法,这里初始化了请求来了的信号,及将请求信息封装到了 `WSGIRequest` 这个对象中。 226 | 227 | 接着向下执行 `get_response` 方法主要设置了路由的配置模块路径,到了 `_middleware_chain` 方法这里,它其实是一个 `_get_response` 对象 或是某个中间件的实例(这个实例中有个 `get_response` 属性指向 `_get_response` 这个方法)。这里我们假设它是中间件实例,调用的实例的 `__call__` 方法(存在于 `MiddlewareMixin` 下)。这个方法下执行了 `process_request` 和 `process_response` 这俩个中间件,在它们中间执行 `_get_response`,这个方法会解析路由并调用视图方法及一些中间件方法。 228 | 229 | `_get_response` 方法下,获取了路由解析器实例,路由解析器根据请求信息匹配到视图,并执行视图方法,获取到响应结果(如果中间件设置了相关方法,会进行调用)。 230 | -------------------------------------------------------------------------------- /chapter-07-命令解析.md: -------------------------------------------------------------------------------- 1 | ## 起步 2 | 3 | 正如我们所知,`django` 根据 `manage.py` 这个文件来提供很多命令来执行。 4 | 5 | 例如:`runserver`,`startapp`,`makemigrations`,`migrate` ... 6 | 7 | 那么这些命令是如何对应执行的(这里我们不具体叙述具体实现的功能项)。 8 | 9 | ## 开始 10 | 11 | 我们先看下 `manage.py` 这个文件(本质上执行这些命令就是执行的是 `manage.py` 这个文件) 12 | 13 | ```python 14 | 15 | # manage.py 16 | 17 | import os 18 | import sys 19 | 20 | if __name__ == "__main__": 21 | # 设置环境变量(主要是配置文件的) 22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "proj.settings") 23 | try: 24 | from django.core.management import execute_from_command_line 25 | except ImportError: 26 | # 这里主要就是导入不成功,抛出 django 未安装的错误啥的 27 | ... 28 | 29 | # 执行这个函数,命令行参数作为参数传入 30 | execute_from_command_line(sys.argv) 31 | 32 | # django/core/management 33 | 34 | def execute_from_command_line(argv=None): 35 | """执行命令""" 36 | 37 | # 实例化 38 | utility = ManagementUtility(argv) 39 | # 执行命令 40 | utility.execute() 41 | 42 | # django/core/management 43 | 44 | class ManagementUtility: 45 | """封装了 django-admin 和 manage.py 的逻辑 46 | 47 | """ 48 | def __init__(self, argv=None): 49 | self.argv = argv or sys.argv[:] 50 | self.prog_name = os.path.basename(self.argv[0]) 51 | if self.prog_name == '__main__.py': 52 | self.prog_name = 'python -m django' 53 | self.settings_exception = None 54 | 55 | def execute(self): 56 | """给定命令行参数,找出命令,给这个命令创建一个解析器并运行它. 57 | 58 | """ 59 | 60 | # 如果没有命令参数,则默认展示帮助 61 | try: 62 | subcommand = self.argv[1] 63 | except IndexError: 64 | subcommand = 'help' 65 | 66 | # 预处理提取 --settings 和 --pythonpath 这俩项配置. 67 | # 这些参数会影响命令的可用性,因此它们必须要提前处理,当然了你不传也没任何影响. 68 | parser = CommandParser(usage='%(prog)s subcommand [options] [args]', add_help=False, allow_abbrev=False) 69 | parser.add_argument('--settings') 70 | parser.add_argument('--pythonpath') 71 | parser.add_argument('args', nargs='*') # catch-all 72 | try: 73 | # 处理已知的参数 74 | options, args = parser.parse_known_args(self.argv[2:]) 75 | # 处理默认参数 76 | handle_default_options(options) 77 | except CommandError: 78 | pass # 此时忽略任何选项错误. 79 | 80 | try: 81 | # 检测 INSTALLED_APPS 是否访问正常 82 | settings.INSTALLED_APPS 83 | except ImproperlyConfigured as exc: 84 | self.settings_exception = exc 85 | except ImportError as exc: 86 | self.settings_exception = exc 87 | 88 | # 这一项配置何时为 true 的似乎不得而知, 忽略以下代码 89 | if settings.configured: 90 | ... 91 | 92 | 93 | self.autocomplete() 94 | 95 | # 如果子命令为 help 96 | if subcommand == 'help': 97 | if '--commands' in args: 98 | sys.stdout.write(self.main_help_text(commands_only=True) + '\n') 99 | elif not options.args: 100 | sys.stdout.write(self.main_help_text() + '\n') 101 | else: 102 | self.fetch_command(options.args[0]).print_help(self.prog_name, options.args[0]) 103 | # 特殊情况,期望 django-admin 相关命令向后兼容 104 | elif subcommand == 'version' or self.argv[1:] == ['--version']: 105 | sys.stdout.write(django.get_version() + '\n') 106 | # 如果涵盖有 --help -h 等相关查看帮助信息的 107 | elif self.argv[1:] in (['--help'], ['-h']): 108 | sys.stdout.write(self.main_help_text() + '\n') 109 | # 如果是其它命令 110 | else: 111 | # 查找命令并运行 112 | self.fetch_command(subcommand).run_from_argv(self.argv) 113 | 114 | ``` 115 | 116 | 以上主要通过执行 `manage.py` 文件并执行 `execute_from_command_line` 函数,把 `sys.argv` 上获得的参数作为参数传入该函数,`execute_from_command_line` 下,`ManagementUtility` 实例化,并调用 `execute` 函数,该功能内部针对一些特殊的情况进行了预处理,最后解析命令并执行。 117 | 118 | 相关逻辑继续向下看。 119 | 120 | ## 深究 121 | 122 | 上面除了针对特殊情况的处理之外,核心主要在 `self.fetch_command(subcommand).run_from_argv(self.argv)` 这里。 123 | 124 | 这是一个链式操作,接连调用了俩个方法。 125 | 126 | 我们先从 `fetch_command` 开始。 127 | 128 | ```python 129 | 130 | def fetch_command(self, subcommand): 131 | # 获取所有相关的命令 132 | # 主要是通过文件查找获取每个 app 下的 management 文件夹下所有的文件 133 | # 是一个字典集合,key 为子命令名称,value 为 app 的名称 134 | commands = get_commands() 135 | try: 136 | # 通过字典映射到值 137 | app_name = commands[subcommand] 138 | except KeyError: 139 | # 如果发生异常,退出并进行提示 (并根据当前错误命令,提示与其相关的命令) 140 | # 这里用到名为 difflib 模块(标准库下的) 141 | ... 142 | 143 | # 如果命令已经加载,则直接使用它 144 | if isinstance(app_name, BaseCommand): 145 | klass = app_name 146 | else: 147 | # 一般会走这里,加载命令类 148 | # 传入俩个参数,app 名称 和 命令名称 149 | klass = load_command_class(app_name, subcommand) 150 | # 这里响应获取到一个 Command 实例,实则继承于 BaseCommand 151 | return klass 152 | 153 | # 加载命令类 154 | def load_command_class(app_name, name): 155 | # 从这里我们得知,如果我们需要自定义一些命令,需要在 app 下创建 management/commands 文件夹,命令名称为文件名 156 | module = import_module('%s.management.commands.%s' % (app_name, name)) 157 | # 动态导入该模块后,调用模块下的 Command 类并实例化(这里有也说明这个类的名称只能叫作 Command) 158 | return module.Command() 159 | 160 | ``` 161 | 162 | > django/core/management/base 下的 BaseCommand 163 | 164 | 接下来,链式调用到了 `run_from_argv`,调用者的基类为 `BaseCommand` 165 | 166 | 我们找到 `BaseCommand` 下的 `run_from_argv` 函数,并把命令行参数传递给该函数。 167 | 168 | ```python 169 | def run_from_argv(self, argv): 170 | # 以下为参数解析 171 | self._called_from_command_line = True 172 | parser = self.create_parser(argv[0], argv[1]) 173 | 174 | options = parser.parse_args(argv[2:]) 175 | cmd_options = vars(options) 176 | # Move positional args out of options to mimic legacy optparse 177 | args = cmd_options.pop('args', ()) 178 | # 处理默认参数 179 | handle_default_options(options) 180 | try: 181 | # 执行 182 | self.execute(*args, **cmd_options) 183 | except Exception as e: 184 | # 执行出现异常打印相关信息 185 | ... 186 | finally: 187 | try: 188 | # 关闭数据库连接 189 | connections.close_all() 190 | except ImproperlyConfigured: 191 | ... 192 | 193 | def execute(self, *args, **options): 194 | # 设置颜色等等相关 195 | if options['no_color']: 196 | self.style = no_style() 197 | self.stderr.style_func = None 198 | if options.get('stdout'): 199 | self.stdout = OutputWrapper(options['stdout']) 200 | if options.get('stderr'): 201 | self.stderr = OutputWrapper(options['stderr'], self.stderr.style_func) 202 | 203 | # 检查一些相关选项 204 | if self.requires_system_checks and not options.get('skip_checks'): 205 | self.check() 206 | if self.requires_migrations_checks: 207 | self.check_migrations() 208 | 209 | # 这里调用了 handle 方法 210 | # 也就是说自己定义的命令,需要实现 handle 方法 211 | # 这个方法内实现你自己的逻辑 212 | output = self.handle(*args, **options) 213 | 214 | if output: 215 | ... 216 | 217 | return output 218 | ``` 219 | 220 | ## 总结 221 | 222 | 通过上述表述,我们知晓了 `runserver`, `startapp` 等命令是如何进行执行的。 223 | 224 | 首先从 `manage.py` 这个文件开始,执行 `execute_from_command_line` 函数(`sys.argv` 作为参数),然后调用 `ManagementUtility` 类进行实例化,该实例调用 `execute` 方法。 225 | 226 | 这里主要进行了一些参数的预处理,然后调用 `fetch_command(subcommand)` 方法,这个先找到所有可用的命令,根据 `subcommand` 参数获取到一个基类为 `BaseCommand` 的一个实例,然后这个实例调用 `run_from_argv` 方法(该方法位于 `BaseCommand` 中),然后调用 `execute` 方法,该方法下又调用 `handle` 方法来实现自己的逻辑。 227 | 228 | 以上就是 `django` 所实现的命令调用。 229 | 230 | ## 扩展 231 | 232 | - `django` 自定义命令实现 233 | 234 | 通过上述源码解析,大概我们了解到如何实现自己定义的命令。 235 | 236 | 在 `app` 下创建 `management/commands` 文件夹(这个是必须的),文件名称你可以随便定义,例如 `custom_command.py`,那么调用的时候就是 `python manage.py custom_command` 237 | 238 | 然后在 `custom_command.py` 文件下定义一个 `Command` 类,并继承 `BaseCommand`(处于 `django/core/management/base` 下),然后类下实现一个 `handle` 方法(必须的)(实现你自己的逻辑) 239 | 240 | - `difflib` 模块 241 | 242 | 在我们输入错误的命令的时候,比如执行 `python manage.py runserver` ,假设 `runserver` 打成了 `runservev`,这里会提示你是否是打出 `runserver` 这个单词。 243 | 244 | 这里 `django` 使用的是 python 标准库下的 `difflib` 模块来达到这个目的。 245 | 246 | 该模块主要用于文本之间的对比等操作。例如我们举个例子。 247 | 248 | ```python 249 | >>> get_close_matches("appel", ["ape", "apple", "peach", "puppy"]) 250 | ['apple', 'ape'] 251 | >>> import keyword as _keyword 252 | >>> get_close_matches("wheel", _keyword.kwlist) 253 | ['while'] 254 | >>> get_close_matches("Apple", _keyword.kwlist) 255 | [] 256 | >>> get_close_matches("accept", _keyword.kwlist) 257 | ['except'] 258 | ``` 259 | 260 | 这个只是这个模块下的其中的一个方法(`django` 也是使用的它)。 261 | 262 | 更多我们可以了解官方文档,详情 -> [请戳这里](https://docs.python.org/3.8/library/difflib.html)。 263 | --------------------------------------------------------------------------------