注:本文分析涉及到的源碼基于Django stable/2.0.x 分支。
計(jì)算機(jī)大部分思想都是來自于現(xiàn)實(shí)生活,所以完全可以用日常生活積累的常識(shí)去理解計(jì)算機(jī)里面的概念。
比如開發(fā)一套完整的軟件系統(tǒng),就好比去開發(fā)一片商業(yè)區(qū)。開發(fā)商業(yè)區(qū)的第一步就是要有一塊空地;軟件系統(tǒng)也是,第一步需要有一個(gè)空的項(xiàng)目。在Django世界里,這很簡(jiǎn)單,用下面命令就可以創(chuàng)建Django空項(xiàng)目。
django-admin.py startproject demo_project
有了一塊空地后,下一步就是在它上面建一棟棟大樓(以及給大樓起名)。而在Django中,與之對(duì)應(yīng)的就是創(chuàng)建app的過程,同樣非常簡(jiǎn)單的執(zhí)行下面命令就可,其中demo_app1就是給大樓起的名字。
python manage.py startapp demo_app1
然而要弄好一片商業(yè)區(qū)必然沒這么簡(jiǎn)單,還需要對(duì)功能不同的大樓進(jìn)行不同的設(shè)計(jì),建造和裝修,以及道路指引牌,甚至還有和安全相關(guān)的一系列配套設(shè)施等等。
這些在后續(xù)的文章中會(huì)一一道來,這篇文章并不打算繼續(xù)介紹它們,而是先深入分析上面兩個(gè)看似非常簡(jiǎn)單的過程(startproject和startapp),因?yàn)楸疚牟⒉幌雽懗扇绾谓倘耸褂肈jango的說明書類的文章。
目前為止僅僅執(zhí)行兩個(gè)命令,就可以讓Django幫你創(chuàng)建好了項(xiàng)目。下文將撥開云霧,來分析它們背后實(shí)際的代碼邏輯。
好了,先曬源碼,
django-admin.py源碼:
#!/usr/bin/env python
from django.core import management
if __name__ == "__main__":
management.execute_from_command_line()
manage.py源碼:
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo_project.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
文件django-admin.py和manage.py都用到了django.core.management模塊,并執(zhí)行execute_from_command_line方法。
此外后者多做了兩點(diǎn):1)設(shè)置環(huán)境變量DJANGO_SETTINGS_MODULE為當(dāng)前項(xiàng)目的settings文件;2)判斷Django是否能被正常導(dǎo)入使用。
上面的第一點(diǎn)不是本文的重點(diǎn),以后的文章再來討論(注意這里不討論,并不是說DJANGO_SETTINGS_MODULE不重要,恰恰相反,它很重要,它是Django項(xiàng)目的入口,指向項(xiàng)目的配置文件,該配置文件中指向ROOT_URLCONF,ROOT_URLCONF指向視圖以及其他的部分,Django項(xiàng)目需要定位到它們之后才能正常運(yùn)行)。
繼續(xù)看django.core.management.execute_from_command_line方法:
def execute_from_command_line(argv=None):
"""Run a ManagementUtility."""
utility = ManagementUtility(argv)
utility.execute()
其中execute_from_command_line方法只是實(shí)例化了類ManagementUtility,接下來的重點(diǎn)查看ManagementUtility.execute方法。
該方法有點(diǎn)長(zhǎng),為了方便我們拆開來分析(源碼位置django/core/management/__init__.py):
def execute(self):
"""
Given the command-line arguments, figure out which subcommand is being
run, create a parser appropriate to that command, and run it.
"""
try:
subcommand = self.argv[1]
except IndexError:
subcommand = 'help' # Display help if no arguments were given.
上面這段代碼的目的在于獲取命令參數(shù)self.argv[1];如果用戶沒有輸入命令參數(shù),將用help命令為默認(rèn)的參數(shù),對(duì)應(yīng)本文開頭的django-admin.py startproject demo_project和python manage.py startapp demo_app1兩條指令,系統(tǒng)獲取到的命令參數(shù)為startproject和startapp。
# Preprocess options to extract --settings and --pythonpath.
# These options could affect the commands that are available, so they
# must be processed early.
parser = CommandParser(None, usage="%(prog)s subcommand [options] [args]", add_help=False)
parser.add_argument('--settings')
parser.add_argument('--pythonpath')
parser.add_argument('args', nargs='*') # catch-all
try:
options, args = parser.parse_known_args(self.argv[2:])
handle_default_options(options)
except CommandError:
pass # Ignore any option errors at this point.
接下來用CommandParser類來解析剩下的命令行參數(shù)(該類只是對(duì)類ArgumentParser的簡(jiǎn)單封裝),這段代碼主要是預(yù)處理settings和pythonpath兩個(gè)可選參數(shù)。解析這兩個(gè)參數(shù)到options中(其他參數(shù)放在args),然后由方法handle_default_options去處理options。
def handle_default_options(options):
"""
Include any default options that all commands should accept here
so that ManagementUtility can handle them before searching for
user commands.
"""
if options.settings:
os.environ['DJANGO_SETTINGS_MODULE'] = options.settings
if options.pythonpath:
sys.path.insert(0, options.pythonpath)
從上面代碼中可以看到handle_default_options()根據(jù)對(duì)象options的settings和pythonpath來設(shè)置環(huán)境變量和設(shè)置python模塊的搜索路徑。
繼續(xù)分析ManagementUtility.execute函數(shù):
try:
settings.INSTALLED_APPS
except ImproperlyConfigured as exc:
self.settings_exception = exc
except ImportError as exc:
self.settings_exception = exc
這段代碼可能會(huì)讓讀者詫異,莫名出現(xiàn)了settings對(duì)象,其實(shí)在這個(gè)源代碼文件開頭有行代碼from django.conf import settings引入了settings,在執(zhí)行這行代碼時(shí),會(huì)讀取os.environ中的DJANGO_SETTINGS_MODULE配置,加載項(xiàng)目配置文件后生成了settings對(duì)象(這里先簡(jiǎn)單的說明下,以后會(huì)有專門的文章講配置文件加載過程)。
代碼settings.INSTALLED_APPS是用來導(dǎo)入配置文件中所有app(通過查看文件django/conf/__init__.py,發(fā)現(xiàn)settings = LazySettings(),它具有__getattr__方法,所以這行代碼相當(dāng)于調(diào)用了LazySettings().__getattr__(INSTALLED_APPS),該方法獲取屬性時(shí)如果沒有導(dǎo)入配置則導(dǎo)入)。
if settings.configured:
# Start the auto-reloading dev server even if the code is broken.
# The hardcoded condition is a code smell but we can't rely on a
# flag on the command class because we haven't located it yet.
if subcommand == 'runserver' and '--noreload' not in self.argv:
try:
autoreload.check_errors(django.setup)()
except Exception:
# The exception will be raised later in the child process
# started by the autoreloader. Pretend it didn't happen by
# loading an empty list of applications.
apps.all_models = defaultdict(OrderedDict)
apps.app_configs = OrderedDict()
apps.apps_ready = apps.models_ready = apps.ready = True
# Remove options not compatible with the built-in runserver
# (e.g. options for the contrib.staticfiles' runserver).
# Changes here require manually testing as described in
# #27522.
_parser = self.fetch_command('runserver').create_parser('django', 'runserver')
_options, _args = _parser.parse_known_args(self.argv[2:])
for _arg in _args:
self.argv.remove(_arg)
# In all other cases, django.setup() is required to succeed.
else:
django.setup()
第一行代碼表示如果settings對(duì)象已經(jīng)配置好,就會(huì)執(zhí)行django.setup()方法(注:第一個(gè)分支專門針對(duì)runserver啟動(dòng)的服務(wù),并且使用autoreload機(jī)制的時(shí)候做的特殊處理,autoreload機(jī)制也是挺有意思的地方,后續(xù)也會(huì)寫專門的文章來說明,這里先簡(jiǎn)單略過,只需要知道其實(shí)第一個(gè)分支的autoreload.check_errors(django.setup)()也是執(zhí)行了django.setup()即可)
接著看django.setup(),在文件django/__init__.py中:
from django.utils.version import get_version
VERSION = (2, 0, 5, 'alpha', 0)
__version__ = get_version(VERSION)
def setup(set_prefix=True):
"""
Configure the settings (this happens as a side effect of accessing the
first setting), configure logging and populate the app registry.
Set the thread-local urlresolvers script prefix if `set_prefix` is True.
"""
from django.apps import apps
from django.conf import settings
from django.urls import set_script_prefix
from django.utils.log import configure_logging
configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
if set_prefix:
set_script_prefix(
'/' if settings.FORCE_SCRIPT_NAME is None else settings.FORCE_SCRIPT_NAME
)
apps.populate(settings.INSTALLED_APPS)
django.setup()主要做了3件事:1)對(duì)logging模塊進(jìn)行了配置處理;2)設(shè)置當(dāng)前線程的script_prefix,可以理解成當(dāng)前目錄的前綴;3)循環(huán)載入之前從配置文件中導(dǎo)入的apps和models:(通過from django.apps import apps去查看文件django/apps/registry.py看到apps = Apps(installed_apps=None),這里Apps初始化的時(shí)候定義了全局的多個(gè)字典、變量、線程鎖等,然后通過apps.populate(settings.INSTALLED_APPS)方法循環(huán)載入之前從配置文件中導(dǎo)入的app和model,該方法是線程安全和冪等的,但不可重入,也等下篇文章再來詳細(xì)講解)。
繼續(xù)ManagementUtility.execute函數(shù):
self.autocomplete()
上面這行代碼主要為了提供命令自動(dòng)補(bǔ)全功能。不詳細(xì)講了,但是需要知道Bash幾個(gè)內(nèi)置的變量,COMP_WORDS: 類型為數(shù)組,存放當(dāng)前命令行中輸入的所有單詞;COMP_CWORD: 類型為整數(shù),當(dāng)前光標(biāo)下輸入的單詞位于COMP_WORDS數(shù)組中的索引;COMPREPLY: 類型為數(shù)組,候選的補(bǔ)全結(jié)果。
if subcommand == 'help':
if '--commands' in args:
sys.stdout.write(self.main_help_text(commands_only=True) + '\n')
elif not options.args:
sys.stdout.write(self.main_help_text() + '\n')
else:
self.fetch_command(options.args[0]).print_help(self.prog_name, options.args[0])
# Special-cases: We want 'django-admin --version' and
# 'django-admin --help' to work, for backwards compatibility.
elif subcommand == 'version' or self.argv[1:] == ['--version']:
sys.stdout.write(django.get_version() + '\n')
elif self.argv[1:] in (['--help'], ['-h']):
sys.stdout.write(self.main_help_text() + '\n')
else:
self.fetch_command(subcommand).run_from_argv(self.argv)
這段代碼前幾個(gè)分支,主要是處理help命令和version命令,最后一個(gè)分支self.fetch_command(subcommand).run_from_argv(self.argv)才是我們一開始討論的那兩個(gè)命令的真實(shí)入口。
其中fetch_command調(diào)用get_commands從下面幾個(gè)目錄找命令對(duì)象(命令類都繼承自BaseCommand,并實(shí)現(xiàn)handle()方法)
-
django/core/management/commands目錄 -
<INSTALLED_APPS>/management/commands/目錄
根據(jù)返回的subcommand實(shí)例,執(zhí)行run_from_argv()方法。
對(duì)應(yīng)我們文章開始討論的地方,也就是相當(dāng)于調(diào)用了startproject.run_from_argv(self.argv)和startapp.run_from_argv(self.argv),它們對(duì)應(yīng)的源碼如下
core/management/commands/startproject.py
from django.core.management.templates import TemplateCommand
from ..utils import get_random_secret_key
class Command(TemplateCommand):
help = (
"Creates a Django project directory structure for the given project "
"name in the current directory or optionally in the given directory."
)
missing_args_message = "You must provide a project name."
def handle(self, **options):
project_name = options.pop('name')
target = options.pop('directory')
# Create a random SECRET_KEY to put it in the main settings.
options['secret_key'] = get_random_secret_key()
super().handle('project', project_name, target, **options)
和core/management/commands/startapp.py
from django.core.management.templates import TemplateCommand
class Command(TemplateCommand):
help = (
"Creates a Django app directory structure for the given app name in "
"the current directory or optionally in the given directory."
)
missing_args_message = "You must provide an application name."
def handle(self, **options):
app_name = options.pop('name')
target = options.pop('directory')
super().handle('app', app_name, target, **options)
這兩個(gè)命令都繼承了TemplateCommand類,而且它們的代碼邏輯也幾乎一樣,只是傳入的參數(shù)不同,所以接下來通過分析TemplateCommand.handle()即可得知真相。
細(xì)心的讀者可能會(huì)發(fā)現(xiàn),上面明明調(diào)用的是方法run_from_argv(),但是startproject和startapp兩命令都沒有這個(gè)方法,其實(shí)方法run_from_argv()是它們繼承的父類BaseCommand里的方法(具體可以查看源碼django/core/management/base.py),run_from_argv()最后會(huì)調(diào)用它們的handle()方法。
現(xiàn)在已經(jīng)距離真相越來越近了,繼續(xù)回到TemplateCommand.handle()。
handle方法代碼也有點(diǎn)多,只挑關(guān)鍵的來分析
def handle(self, app_or_project, name, target=None, **options):
...
for root, dirs, files in os.walk(template_dir):
...
for filename in files:
old_path = path.join(root, filename)
new_path = path.join(top_dir, relative_dir,
filename.replace(base_name, name))
for old_suffix, new_suffix in self.rewrite_template_suffixes:
if new_path.endswith(old_suffix):
new_path = new_path[:-len(old_suffix)] + new_suffix
break # Only rewrite once
...
# Only render the Python files, as we don't want to
# accidentally render Django templates files
if new_path.endswith(extensions) or filename in extra_files:
with open(old_path, 'r', encoding='utf-8') as template_file:
content = template_file.read()
template = Engine().from_string(content)
content = template.render(context)
with open(new_path, 'w', encoding='utf-8') as new_file:
new_file.write(content)
else:
shutil.copyfile(old_path, new_path)
if self.verbosity >= 2:
self.stdout.write("Creating %s\n" % new_path)
try:
shutil.copymode(old_path, new_path)
self.make_writeable(new_path)
except OSError:
self.stderr.write(
"Notice: Couldn't set permission bits on %s. You're "
"probably using an uncommon filesystem setup. No "
"problem." % new_path, self.style.NOTICE)
...
核心邏輯是遍歷處理template_dir目錄下的文件,這里的template_dir在startproject命令和startapp命令中分別對(duì)應(yīng)目錄django/conf/project_template/和目錄django/conf/app_template/。目錄結(jié)構(gòu)如下

為了更好的理解,先把文章一開始執(zhí)行
startproject和startapp命令生成的目錄結(jié)構(gòu)也拿出來,對(duì)比后發(fā)現(xiàn)幾乎完全一樣,除了文件后綴名不一樣:

其實(shí)到這里為止,已經(jīng)能猜到大致過程了,在執(zhí)行
startproject的時(shí)候,會(huì)遍歷django/conf/project_template/下面的文件,把源文件的后綴.py-tpl改成.py,然后根據(jù)設(shè)置好的模版引擎生成相應(yīng)的文件。這樣項(xiàng)目就創(chuàng)建完成了。同樣,startapp也是一樣的道理。下面幾行代碼是專門改文件后綴的
for old_suffix, new_suffix in self.rewrite_template_suffixes:
if new_path.endswith(old_suffix):
new_path = new_path[:-len(old_suffix)] + new_suffix
break # Only rewrite once
其中self.rewrite_template_suffixes就是個(gè)python 元組對(duì)象,里面包含了原后綴名(py-tpl),以及新后綴名(py)
# Rewrite the following suffixes when determining the target filename.
rewrite_template_suffixes = (
# Allow shipping invalid .py files without byte-compilation.
('.py-tpl', '.py'),
)
現(xiàn)在還剩下模版引擎這塊,照樣先曬代碼
# Only render the Python files, as we don't want to
# accidentally render Django templates files
for filename in files:
if new_path.endswith(extensions) or filename in extra_files:
with open(old_path, 'r', encoding='utf-8') as template_file:
content = template_file.read()
template = Engine().from_string(content)
content = template.render(context)
with open(new_path, 'w', encoding='utf-8') as new_file:
new_file.write(content)
else:
shutil.copyfile(old_path, new_path)
這段代碼主要是讀取模版文件里面的內(nèi)容,通過調(diào)用方法Engine().from_string()生成Template模版對(duì)象。
比如下面settings.py-tpl模版文件內(nèi)容(只截取了部分)被用來生成Template模版對(duì)象
"""
Django settings for {{ project_name }} project.
Generated by 'django-admin startproject' using Django {{ django_version }}.
For more information on this file, see
https://docs.djangoproject.com/en/{{ docs_version }}/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '{{ secret_key }}'
...
接著template.render()會(huì)根據(jù)context內(nèi)容渲染模版,context內(nèi)容如下
context = Context({
**options,
base_name: name,
base_directory: top_dir,
camel_case_name: camel_case_value,
'docs_version': get_docs_version(),
'django_version': django.__version__,
}, autoescape=False)
可見,context包含的docs_version、django_version及options里面的內(nèi)容會(huì)用來替換模版文件里被{{}}包含的內(nèi)容,比如{{ docs_version }},然后替換后的新內(nèi)容會(huì)被寫入到對(duì)應(yīng)的新文件里,這樣Django就幫忙生成了項(xiàng)目需要的所有默認(rèn)文件。
到現(xiàn)在為止,我們已經(jīng)知道了文章開頭那兩個(gè)簡(jiǎn)單命令具體做了什么,可以讓我們輕松的完成項(xiàng)目的創(chuàng)建。
最后總結(jié):
startproject和startapp屬于初期創(chuàng)建項(xiàng)目階段的命令,所以更多的是完成項(xiàng)目配置相關(guān)的工作,之后再根據(jù)用戶輸入的參數(shù)subcommand到命令工具集中去找到對(duì)應(yīng)的命令對(duì)象。繼承自TemplateCommand類的命令對(duì)象,使用模版引擎把相應(yīng)template_dir下面的模版文件渲染生成新項(xiàng)目需要的文件和目錄,最終Django就幫我們創(chuàng)建好了新項(xiàng)目。