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 全局注入模板的变量(如 requestuser

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_APPSaccount 排在 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)
# 输出实际加载的文件路径,一眼看出从哪加载的

七、今日成果

功能 方式 状态
理解 DIRSAPP_DIRS 的区别 理论 + 实验
掌握模板查找的"点名制"顺序 理论 + 验证
理解 INSTALLED_APPS 顺序决定覆盖权 理论 + 实验
模板按 App 子目录隔离(命名空间) 实践
get_template() 调试技巧 实践
模板系统与 Python MRO 的类比 理论

八、关键心法总结

  1. DIRS 是 VIP 通道,优先级最高。 项目级模板放这里,全局共享。
  2. APP_DIRS=True 让每个注册 App 的 templates/ 自动可见。 未注册的 App 等于不存在。
  3. INSTALLED_APPS 不是列表,是优先级声明。 排在后面的 App 拥有对同名模板的最终解释权。
  4. 模板查找是"先到先得",不是"就近原则"。 和 Python MRO 一样是线性查找。
  5. 永远用 app/templates/app/ 的子目录结构。 这是 Django 的"命名空间",防止同名覆盖。
  6. get_template().origin.name 是调试神器。 一眼看出 Django 从哪个文件加载了模板。
  7. 修改模板不生效?先强刷,再重启。 别跟缓存较劲。

九、下一步计划

  • Request 对象深入request.POSTQueryDict 本质、get() vs getlist()META 到底是什么
  • CBV 源码级拆解as_view() / view() / cls vs self / __init__ vs __new__ / dispatch() 调度机制
  • 数据库建模:定义 User 扩展模型、makemigrations / migrate
  • Vue 环境搭建:前后端分离,axios 调接口

今日总结: Day3 的核心收获是搞清楚了 Django 模板系统的"寻路算法"——从 DIRSAPP_DIRS,从 INSTALLED_APPS 顺序到命名空间隔离,每一步都是"先到先得"的线性查找。这和 Python 的 MRO 如出一辙,理解了查找顺序,就再也不会被"模板找不到"或"渲染了错误模板"这种问题困扰。💪


下回预告:Day4 深入 Request 对象,从 QueryDictMETA,搞清楚 HTTP 请求在 Django 中的完整表达。

posted @ 2026-08-07 14:36  YiYezc  阅读(1)  评论(0)    收藏  举报