Django + Vue 学习日志( PART03:Django 模板)

PART03:Django 模板系统(Template System)

记录时间:2026年8月14日
环境:Fedora 44 + Python 3.12.13 + Django 5.2.16


一、今日核心问题溯源

在搭建文章列表页的过程中,触发了对 Django 模板系统的一系列追问:

  1. 模板查找机制:Django 到底去哪里找模板文件?DIRSAPP_DIRS 是什么关系?
  2. 模板继承{% extends %}{% block %} 是怎么工作的?为什么 endblock 之后的内容不显示?
  3. 模板包含{% include %} 不显示内容是什么原因?
  4. 模板过滤器:为什么 | random 不生效?过滤器不存在时 Django 会怎样?
  5. cycle 标签:如何实现列表交替变色?
  6. 变量渲染{{ }} 的查找规则是什么?字典、列表、对象属性怎么取?

二、关键认知突破

1️⃣ 模板查找机制:Django 怎么找到 HTML 文件的

Django 按固定顺序查找模板,这是你反复踩坑后彻底搞懂的核心机制。

查找顺序

第 1 步:DIRS 里的目录(项目级,按列表顺序)
第 2 步:INSTALLED_APPS 中注册的 App(按注册顺序)

settings.py 配置

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],   # ← 项目级模板目录
        'APP_DIRS': True,                     # ← 是否搜索 App 内模板
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                ...
            ],
        },
    },
]

INSTALLED_APPS 决定 App 级模板的查找权

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    ...
    'app01',   # ← 注册了才能找到 app01/templates/
]

两种推荐的项目结构

方式 A:App 级模板(推荐,支持命名空间)

app01/
├── templates/
│   └── app01/
│       ├── list.html
│       └── page.html

视图里写:

render(request, 'app01/list.html', context)

方式 B:项目级模板

demo/
├── templates/
│   └── list.html

settings.py 里:

'DIRS': [BASE_DIR / 'templates'],

视图里写:

render(request, 'list.html', context)

模板命名空间的意义

app01/templates/app01/list.html
blog/templates/blog/list.html

两个 App 都有 list.html,如果不加命名空间(app01/ 前缀),先注册的 App 会覆盖后注册的

关键心法INSTALLED_APPS 的注册顺序就是模板查找的优先级顺序。


2️⃣ 模板继承:{% extends %} + {% block %}

这是 Django 模板最强大的功能,也是你踩坑最多的地方。

基本结构

base.html(父模板)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}默认标题{% endblock %}</title>
    {% block extra_css %}{% endblock %}
</head>
<body>
    <header>
        <h1>我的博客</h1>
        <hr>
    </header>

    <main>
        {% block content %}{% endblock %}
    </main>

    <footer>
        <hr>
        <p>© 2026 我的博客</p>
    </footer>
</body>
</html>

list.html(子模板)

{% extends 'base.html' %}

{% block title %}文章列表{% endblock %}

{% block content %}
    <h1>文章列表</h1>
    <ul>
        {% for article in article_list %}
            <li>{{ article }}</li>
        {% endfor %}
    </ul>
{% endblock %}

渲染规则(铁律)

规则 说明
extends 必须是模板第一行 前面只能有空白和注释
子模板只能覆盖 block 内的内容 block 外的内容会被忽略
endblock 之后的内容不渲染 直接丢弃,不报错
未覆盖的 block 显示默认值 父模板里写的默认内容
{{ block.super }} 可继承父模板内容 不是替换,是追加

你踩过的坑:endblock 之后写内容

{% extends 'base.html' %}

{% block content %}
    <p>这是内容</p>
    {% include 'app01/page.html' %}
{% endblock %}

{# ❌ 这里的内容永远不会显示! #}
<p>我放在了 endblock 之后</p>

原因extends 之后,所有内容必须包在 {% block %} 里,否则直接被丢弃。

解决:养成习惯,所有内容都在 block 内。


3️⃣ 模板包含:{% include %}

基本用法

{% include 'app01/page.html' %}

include 的查找规则

和模板查找规则完全一致:

  1. 先查 DIRS
  2. 再查 INSTALLED_APPS

include 的静默失败(大坑)

情况 Django 行为
文件存在 ✅ 正常渲染
文件不存在 静默失败,不报错,不显示
路径写错 静默失败,不报错,不显示
App 没注册 静默失败,不报错,不显示

调试技巧

{# 测试文件是否存在 #}
{% include 'app01/page.html' %}
{# 如果没显示,说明文件找不到或路径不对 #}

更安全的写法(Django 提供 with 传参):

{% include 'app01/page.html' with author=author %}

include vs extends 的区别

特性 extends include
用途 继承整个页面骨架 嵌入一段 HTML 片段
是否支持 block ✅ 是 ❌ 否
是否可多层嵌套 ✅ 可以 ✅ 可以
典型场景 全站布局(导航/页脚) 重复组件(评论框/分页条)

4️⃣ 模板变量:{{ }} 的查找规则

基本语法

{{ variable }}

点号查找规则(LEGB 风格的模板版)

Django 模板遇到 {{ info.name }} 时,按以下顺序查找:

顺序 查找方式 示例
1 字典 key info['name']
2 对象属性 info.name
3 对象方法(无参) info.get_name()
4 列表索引 info[0]

你的代码验证

视图传参:

info = {
    'name': 'andy',
    'age': 18,
    'gender': '男',
    'programming_languages': ['Python', 'JavaScript', 'C++']
}

return render(request, 'app01/list.html', {
    'author': author,
    'article_numbers': article_numbers,
    'article_list': article_list,
    'info': info
})

模板取值:

<p>姓名:{{ info.name }}</p>           {# 字典 key 查找 ✅ #}
<p>年龄:{{ info.age }}</p>            {# 字典 key 查找 ✅ #}
<p>第一门语言:{{ info.programming_languages.0 }}</p>  {# 列表索引 ✅ #}

5️⃣ 模板过滤器(Filter)

基本语法

{{ variable | filter }}
{{ variable | filter:"参数" }}

常用内置过滤器

过滤器 作用 示例 输出
default 变量为空时显示默认值 `{{ name default:"匿名" }}`
length 获取长度 `{{ article_list length }}`
upper 转大写 `{{ name upper }}`
lower 转小写 `{{ name lower }}`
date 格式化日期 `{{ now date:"Y-m-d" }}`
first 取第一个元素 `{{ languages first }}`
last 取最后一个元素 `{{ languages last }}`
join 用字符串连接 `{{ languages join:", " }}`
safe 关闭 HTML 转义 `{{ html safe }}`

过滤器链

{{ name|default:"匿名"|upper }}

执行顺序:从左到右。

random 过滤器不存在(你踩的大坑)

{{ info.programming_languages | random }}  {# ❌ 不显示 #}

原因:Django 没有内置 random 过滤器

Django 的行为

  • ❌ 不报错
  • ❌ 不渲染
  • ✅ 静默失败

正确做法(在视图里 random)

# views.py
import random

def list(request):
    ...
    random_language = random.choice(info['programming_languages'])
    return render(request, 'app01/list.html', {
        ...
        'random_language': random_language
    })
{# list.html #}
<p>掌握的编程语言:{{ random_language }}</p>

关键心法:模板是"展示层",不是"逻辑层"。random 是业务逻辑,必须放在视图里。


6️⃣ 模板标签(Tag):{% %}

循环:{% for %}

<ul>
    {% for article in article_list %}
        <li>{{ article }}</li>
    {% empty %}
        <li>暂无文章</li>
    {% endfor %}
</ul>
变量 含义
forloop.counter 从 1 开始的序号
forloop.counter0 从 0 开始的序号
forloop.first 是否是第一次循环(bool)
forloop.last 是否是最后一次循环(bool)
forloop.parentloop 嵌套循环时访问外层

条件:{% if %}

{% if article_numbers > 10 %}
    <p>文章很多!</p>
{% elif article_numbers > 5 %}
    <p>文章还行</p>
{% else %}
    <p>文章太少</p>
{% endif %}

交替样式:{% cycle %}(你用过的)

<li class="{% cycle 'red_row' 'blue_row' %}">{{ article }}</li>

渲染结果:

<li class="red_row">第一篇文章</li>
<li class="blue_row">第二篇文章</li>
<li class="red_row">第三篇文章</li>
...

对应的 CSS:

.red_row { color: red; }
.blue_row { color: blue; }

注意cycle 输出的是字符串,被塞进 class="..." 里,所以 CSS 要用类选择器 .red_row,不是标签选择器。


三、完整项目代码(你的最终版本)

views.py

import random
from django.shortcuts import render
from django.http import HttpResponse

def helloworld(request):
    return HttpResponse('Hello World!')

def article_create(request):
    return HttpResponse('这是文章1的内容')

def article_detail(request, article_id):
    return HttpResponse(f'这是第{article_id}篇文章的内容')

def list(request):
    author = 'andy'
    article_numbers = 20
    article_list = [
        '第一篇文章:什么是Django',
        '第二篇文章:C语言的艺术',
        '第三篇文章:JavaScript高级程序设计',
        '第四篇文章:Python编程:从入门到实践',
        '第五篇文章:深入理解计算机系统'
    ]
    info = {
        'name': 'andy',
        'age': 18,
        'gender': '男',
        'programming_languages': ['Python', 'JavaScript', 'C++']
    }

    # 在视图里做 random,不在模板里做
    random_language = random.choice(info['programming_languages'])

    return render(request, 'app01/list.html', {
        'author': author,
        'article_numbers': article_numbers,
        'article_list': article_list,
        'info': info,
        'random_language': random_language
    })

base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}我的博客{% endblock %}</title>
    {% block extra_css %}{% endblock %}
</head>
<body>
    <header>
        <h1>我的博客</h1>
        <hr>
    </header>

    <main>
        {% block content %}{% endblock %}
    </main>

    <footer>
        <hr>
        <p>© 2026 我的博客</p>
    </footer>
</body>
</html>

list.html

{% extends 'base.html' %}

{% block title %}文章列表{% endblock %}

{% block extra_css %}
<style>
    .red_row {
        color: red;
    }
    .blue_row {
        color: blue;
    }
</style>
{% endblock %}

{% block content %}
    <h1>文章列表</h1>
    <h2>作者:{{ author }} 文章数量:{{ article_numbers }}</h2>

    <ul>
        {% for article in article_list %}
            <li class="{% cycle 'red_row' 'blue_row' %}">{{ article }}</li>
            {% empty %}
                <li>暂无文章</li>
        {% endfor %}
    </ul>

    <p>作者简介:</p>
    <p>姓名:{{ info.name }},年龄:{{ info.age }},性别:{{ info.gender }}</p>
    <p>掌握的编程语言:{{ random_language }}</p>

    {% include 'app01/page.html' %}
{% endblock %}

page.html(被 include 的片段)

<div class="page-info">
    <p>这是被引入的页面片段</p>
</div>

四、常见报错与解决方案速查表

报错 原因 解决方案
TemplateDoesNotExist: list.html 文件不存在 / 路径不对 / App 未注册 检查文件位置、DIRSINSTALLED_APPS
TemplateDoesNotExist: base.html extends 引用的父模板找不到 同上,检查 base.html 是否存在
TemplateDoesNotExist: page.html include 引用的文件找不到 同上,且 include 静默失败不报错
{{ variable }} 不显示 变量没传到模板 / 过滤器不存在 检查 render() 的 context / 过滤器拼写
` random` 不显示 Django 没有内置 random 过滤器
endblock 后的内容不显示 不在任何 block 内,被丢弃 把所有内容移到 block 内
CSS 类名不生效 .h1 是类选择器,不是 <h1> 标签 .red 代替 .h1,避免和标签混淆
cycle 不交替 写在了 for 循环外面 cycle 必须在 for 循环内部

五、Django 模板设计哲学

原则 说明
展示与逻辑分离 模板只负责展示,业务逻辑放视图
静默失败 变量/过滤器/文件找不到时不报错,输出空
可预测性 模板渲染应该是确定性的(无随机性)
继承优先 extends + block 是页面布局的首选方式
命名空间 App 级模板用 app01/template.html 防止覆盖

六、学习金句

  1. "模板是展示层,不是逻辑层;random 是逻辑,不是展示。"
  2. "extends 之后,所有内容必须包在 block 里,否则直接被丢弃。"
  3. "Django 模板的静默失败是双刃剑:不炸页面,但调试时让你怀疑人生。"
  4. ".red_row. 是类选择器的标志,不是装饰;class 是属性,不是标签。"
  5. "INSTALLED_APPS 的注册顺序就是模板查找的优先级顺序。"

七、学习总结

  • 模板查找:掌握了 DIRS + INSTALLED_APPS 的两级查找机制,理解了命名空间的意义。
  • 模板继承:彻底搞懂了 extends + block 的工作原理,踩坑后明白了 endblock 之后内容被丢弃的规则。
  • 模板包含:理解了 include 的静默失败特性,学会了调试方法。
  • 变量渲染:掌握了点号查找规则(字典 → 属性 → 方法 → 索引)。
  • 过滤器:学会了常用内置过滤器,理解了"模板无 random"的设计哲学。
  • 标签:熟练使用 for / if / cycle / empty 等核心标签。
  • CSS 联动:理解了 Django cycle 输出字符串 → class 属性 → CSS 类选择器的完整链路。

posted @ 2026-08-14 09:17  YiYezc  阅读(2)  评论(0)    收藏  举报