Django+Vue 学习日志(03模板查找)
PART03:模板系统深入与 INSTALLED_APPS 权力排序
记录时间:2026年8月6日
环境:Fedora 44 + Python 3.12.13 + Django 5.2.16
前言
Day2 搞定了视图层和表单交互,Day3 深入 Django 的模板系统。今天的核心问题是:Django 到底怎么找到模板文件的? 从 TEMPLATES 配置到 INSTALLED_APPS 顺序,从模板查找的"点名制"到同名模板的覆盖规则——今天把模板系统的底层逻辑彻底吃透。
一、TEMPLATES 配置拆解
DIRS 是什么
DIRS 是一个目录列表,告诉 Django:"除了各 App 自带的模板目录,还要去这些地方找模板。"
# settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
BASE_DIR / 'templates', # 项目级模板目录
BASE_DIR / 'templates' / 'shared', # 公共组件目录
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
| 属性 | 作用 |
|---|---|
DIRS |
项目级模板目录列表,优先级最高 |
APP_DIRS |
是否自动搜索已注册 App 的 templates/ 目录 |
BACKEND |
模板引擎,默认 Django 自带引擎 |
OPTIONS.context_processors |
全局注入模板的变量(如 request、user) |
APP_DIRS 是什么
APP_DIRS: True 告诉 Django:
"去每个已注册 App 的根目录下,找名为
templates/的文件夹,里面就是模板。"
目录约定:
myproject/
├── account/
│ ├── templates/
│ │ └── account/
│ │ ├── login.html
│ │ └── register.html
│ ├── views.py
│ └── ...
├── blog/
│ ├── templates/
│ │ └── blog/
│ │ ├── list.html
│ │ └── detail.html
│ └── ...
└── templates/ ← DIRS 指向的项目级目录
├── base.html
└── shared/
└── navbar.html
代码中的两种写法
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
# 写法一:Path 对象(推荐,Django 3.1+)
'DIRS': [BASE_DIR / 'templates']
# 写法二:字符串拼接(老项目常见)
'DIRS': [str(BASE_DIR / 'templates')]
# 写法三:os.path.join(最老式)
import os
'DIRS': [os.path.join(BASE_DIR, 'templates')]
两者能否同时开启
✅ 完全可以,而且这是标准配置。
DIRS = [BASE_DIR / 'templates'] ← 项目级(VIP 通道)
APP_DIRS = True ← App 级(按注册顺序)
Django 先搜 DIRS,再搜 APP_DIRS。
二、模板查找的真实顺序
Django 找模板的"点名制"
当你在视图中写:
return render(request, 'account/login.html')
Django 按以下顺序逐个目录查找,找到第一个就停止:
第 1 步:DIRS[0] + 'account/login.html'
→ /project/templates/account/login.html ← 找到了就停
第 2 步:DIRS[1] + 'account/login.html'
→ /project/templates/shared/account/login.html
第 3 步:INSTALLED_APPS[0] 的 templates/ + 'account/login.html'
→ /project/account/templates/account/login.html
第 4 步:INSTALLED_APPS[1] 的 templates/ + 'account/login.html'
→ /project/blog/templates/account/login.html
... 直到找到或抛 TemplateDoesNotExist
关键认知
Django 模板查找是"先到先得",不是"就近原则"。
谁排在前面,谁就拥有优先权。
实验验证查找顺序
在 settings.py 中故意放两个同名模板:
templates/
└── test.html ← DIRS 中的(优先级高)
account/templates/
└── test.html ← App 中的(优先级低)
视图中渲染:
def test_view(request):
return render(request, 'test.html')
✅ 永远渲染 templates/test.html
把 DIRS 清空后再试:
'DIRS': [],
✅ 这次渲染 account/templates/test.html
三、INSTALLED_APPS 是"权力排序"
为什么顺序决定覆盖权
INSTALLED_APPS 不仅决定"哪些 App 被加载",还决定"同名资源谁赢"。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'account', # ← 排在 blog 前面
'blog', # ← 排在 account 后面
'user',
]
场景:account 和 blog 都有 shared/header.html
account/templates/shared/header.html
blog/templates/shared/header.html
查找顺序:
1. DIRS → 没找到
2. account/templates/shared/header.html → ✅ 找到,停止!
✅ account 的版本胜出
✅ blog 的版本永远不会被用到
如果把 blog 移到 account 前面呢?
INSTALLED_APPS = [
...
'blog', # ← 移到前面
'account', # ← 移到后面
]
查找顺序变成:
1. DIRS → 没找到
2. blog/templates/shared/header.html → ✅ 找到,停止!
✅ blog 的版本胜出
注册 vs 未注册的区别
| 状态 | App 的 templates/ 是否可见 |
|---|---|
| ✅ 已注册 | Django 会自动搜索其 templates/ |
| ❌ 未注册 | 完全不可见,即使目录存在 |
# 假设 'mytools' 没注册
INSTALLED_APPS = [
'account',
'blog',
# 'mytools' ← 注释掉了
]
mytools/templates/mytools/widget.html ← 存在但不可见
render(request, 'mytools/widget.html')
# ❌ TemplateDoesNotExist
✅ 注册是 App 模板被发现的唯一前提
四、模板命名空间与覆盖机制
错误写法:平铺模板
很多初学者这样放模板:
account/templates/
├── login.html ← ❌ 裸放,没有子目录
├── register.html ← ❌
└── profile.html ← ❌
blog App 也这样放:
blog/templates/
├── login.html ← ❌ 同名!
├── list.html
└── detail.html
问题来了
当 INSTALLED_APPS 中 account 排在 blog 前面:
account/templates/login.html → ✅ 被找到
blog/templates/login.html → ❌ 永远被遮蔽
即使你在 blog 的视图里写:
# blog/views.py
return render(request, 'login.html')
渲染的仍然是 account/templates/login.html!
正确写法:按 App 分子目录
Django 官方推荐的标准结构:
account/templates/
└── account/ ← 子目录,和 App 同名
├── login.html ✅
├── register.html ✅
└── profile.html ✅
blog/templates/
└── blog/ ← 子目录,和 App 同名
├── login.html ✅ 不会和 account 冲突
├── list.html ✅
└── detail.html ✅
视图中引用:
# account/views.py
return render(request, 'account/login.html')
# blog/views.py
return render(request, 'blog/login.html')
✅ 路径唯一,永不冲突
同名模板的覆盖规则总结
| 场景 | 谁赢 |
|---|---|
DIRS vs APP_DIRS |
DIRS 赢 |
DIRS[0] vs DIRS[1] |
索引小的赢 |
App A vs App B(都在 APP_DIRS) |
INSTALLED_APPS 中靠前的赢 |
子目录隔离(app/templates/app/) |
永不冲突 |
用 MRO 类比模板查找
和 Python 的 MRO(方法解析顺序)一模一样:
# Python MRO:从左到右找方法
class A: pass
class B(A): pass
# B → A → object
# Django 模板:从上到下找文件
# DIRS[0] → DIRS[1] → App[0] → App[1] → ...
都是"先到先得"的线性查找链。
五、settings.py 关键配置回顾
# settings.py 完整相关片段
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.debug',
],
},
},
]
INSTALLED_APPS = [
# Django 内置
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 自定义 App(顺序 = 优先级)
'account', # 对 account/* 模板拥有最高优先级
'blog',
'user',
]
六、踩坑记录
| 坑 | 原因 | 解决 |
|---|---|---|
TemplateDoesNotExist |
模板放在未注册的 App 中 | 在 INSTALLED_APPS 中注册该 App |
| 渲染了"错误的模板" | 两个 App 有同名模板,且都没用子目录隔离 | 按 app/templates/app/ 结构重组 |
| 修改了模板但页面没变 | 浏览器缓存 / Django 模板缓存 | Ctrl+Shift+R 强刷,或重启 runserver |
DIRS 路径写错 |
用了相对路径或拼错 | 用 BASE_DIR / 'templates' 绝对路径 |
模板里 {% include %} 找不到文件 |
被包含的模板也要遵循查找顺序 | 确认目标模板在 DIRS 或已注册 App 中 |
以为 APP_DIRS=False 就没问题 |
关掉后所有 App 模板全部不可见 | 除非纯 DIRS 项目,否则保持 True |
关于"模板缓存"的终极教训
Django 的
runserver会自动重载 Python 文件,但模板文件的修改有时不会立即生效。
遇到"改了模板没反应",第一反应是强刷浏览器,第二反应是重启 runserver。
验证模板是否被正确加载:
# 在 shell 中测试
python manage.py shell
>>> from django.template.loader import get_template
>>> t = get_template('account/login.html')
>>> print(t.origin.name)
# 输出实际加载的文件路径,一眼看出从哪加载的
七、今日成果
| 功能 | 方式 | 状态 |
|---|---|---|
理解 DIRS 与 APP_DIRS 的区别 |
理论 + 实验 | ✅ |
| 掌握模板查找的"点名制"顺序 | 理论 + 验证 | ✅ |
理解 INSTALLED_APPS 顺序决定覆盖权 |
理论 + 实验 | ✅ |
| 模板按 App 子目录隔离(命名空间) | 实践 | ✅ |
get_template() 调试技巧 |
实践 | ✅ |
| 模板系统与 Python MRO 的类比 | 理论 | ✅ |
八、关键心法总结
DIRS是 VIP 通道,优先级最高。 项目级模板放这里,全局共享。APP_DIRS=True让每个注册 App 的templates/自动可见。 未注册的 App 等于不存在。INSTALLED_APPS不是列表,是优先级声明。 排在后面的 App 拥有对同名模板的最终解释权。- 模板查找是"先到先得",不是"就近原则"。 和 Python MRO 一样是线性查找。
- 永远用
app/templates/app/的子目录结构。 这是 Django 的"命名空间",防止同名覆盖。get_template().origin.name是调试神器。 一眼看出 Django 从哪个文件加载了模板。- 修改模板不生效?先强刷,再重启。 别跟缓存较劲。
九、下一步计划
- Request 对象深入:
request.POST的QueryDict本质、get()vsgetlist()、META到底是什么 - CBV 源码级拆解:
as_view()/view()/clsvsself/__init__vs__new__/dispatch()调度机制 - 数据库建模:定义
User扩展模型、makemigrations/migrate - Vue 环境搭建:前后端分离,axios 调接口
今日总结: Day3 的核心收获是搞清楚了 Django 模板系统的"寻路算法"——从 DIRS 到 APP_DIRS,从 INSTALLED_APPS 顺序到命名空间隔离,每一步都是"先到先得"的线性查找。这和 Python 的 MRO 如出一辙,理解了查找顺序,就再也不会被"模板找不到"或"渲染了错误模板"这种问题困扰。💪
下回预告:Day4 深入 Request 对象,从 QueryDict 到 META,搞清楚 HTTP 请求在 Django 中的完整表达。

浙公网安备 33010602011771号