Django Templates 進階:模板繼承、自訂標籤與靜態檔案 | Django 教學
當你的 Django 專案頁面越來越多,你會發現每個模板都重複著相同的 HTML 結構 – 導航列、頁尾、CSS 引入。模板繼承(Template Inheritance)正是 Django 解決這個問題的核心機制,它讓你用 DRY 原則(Don’t Repeat Yourself)組織模板架構。本文將深入探討 {% block %} 與 {% extends %} 的繼承機制、{% include %} 引入可重用片段、自訂 Template Tags 與 Template Filters,以及 靜態檔案(Static Files)的完整處理流程,帶你打造一套結構清晰、易於維護的模板系統。
模板繼承概念:DRY 原則
在沒有模板繼承的情況下,每個 HTML 頁面都需要重複撰寫 <head>、導航列、頁尾等共用結構。當你需要修改導航列的一個連結時,就必須逐一修改所有頁面 – 這不僅費時,更容易遺漏。
Django 的模板繼承機制採用「骨架 + 可覆蓋區塊」的設計:先建立一個包含完整 HTML 結構的 基礎模板(Base Template),在其中用 {% block %} 標籤定義可覆蓋的區域,子模板再透過 {% extends %} 繼承父模板並覆寫需要變動的區塊。
base.html(基礎骨架)
├── {% block title %}{% endblock %}
├── {% block extra_css %}{% endblock %}
├── {% block content %}{% endblock %}
└── {% block extra_js %}{% endblock %}
↓ extends
page.html(子模板)
├── {% extends "base.html" %}
├── {% block title %}文章列表{% endblock %}
└── {% block content %}...{% endblock %}
這樣一來,所有頁面共用的 HTML 結構只需要維護一份,而每個頁面只需要關注自己特有的內容。
base.html 基礎模板設計
一個設計良好的 base.html 應該涵蓋完整的 HTML 骨架,並預留足夠的 {% block %} 讓子模板可以彈性覆寫:
{# templates/base.html #}
{% load static %}
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}我的部落格{% endblock %} | MyBlog</title>
<link rel="stylesheet" href="{% static 'css/main.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
{# 導航列 #}
<nav class="navbar">
<a href="{% url 'home' %}" class="logo">MyBlog</a>
<ul>
<li><a href="{% url 'post_list' %}">文章</a></li>
<li><a href="{% url 'about' %}">關於</a></li>
{% if user.is_authenticated %}
<li><a href="{% url 'logout' %}">登出({{ user.username }})</a></li>
{% else %}
<li><a href="{% url 'login' %}">登入</a></li>
{% endif %}
</ul>
</nav>
{# 訊息提示 #}
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{# 主要內容區 #}
<main class="container">
{% block content %}{% endblock %}
</main>
{# 頁尾 #}
<footer>
<p>© {% now "Y" %} MyBlog. All rights reserved.</p>
</footer>
<script src="{% static 'js/main.js' %}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
{% block %} 定義可覆蓋區域
{% block %} 標籤定義了一個具名區塊,子模板可以選擇性地覆寫它。幾個設計要點:
- block 命名要語義化:使用
title、content、extra_css、sidebar等有意義的名稱 - 提供預設內容:
{% block title %}我的部落格{% endblock %}讓未覆寫的子模板也能正常顯示 - 預留擴充點:
extra_css和extra_js讓子模板可以載入額外的資源
{% extends %} 繼承父模板
子模板使用 {% extends %} 宣告繼承關係,並只覆寫需要變動的 block:
{# templates/blog/post_list.html #}
{% extends "base.html" %}
{% load static %}
{% block title %}文章列表{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/blog.css' %}">
{% endblock %}
{% block content %}
<h1>所有文章</h1>
<div class="post-grid">
{% for post in posts %}
<article class="post-card">
<h2><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h2>
<p class="meta">{{ post.created_at|date:"Y年m月d日" }} | {{ post.author }}</p>
<p>{{ post.content|truncatewords:30 }}</p>
</article>
{% empty %}
<p>目前沒有文章。</p>
{% endfor %}
</div>
{% endblock %}
繼承規則提醒:
{% extends %}必須是模板的第一個標籤,前面不能有任何內容(包括空白行)- 子模板中只有
{% block %}內的內容會被渲染,block 之外的內容會被忽略 - 使用
{{ block.super }}可以保留父模板該 block 的原始內容,再追加新內容
{# 使用 block.super 保留父模板內容 #}
{% block extra_css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/post_detail.css' %}">
{% endblock %}
多層繼承
實務上,我們常會設計三層繼承結構來區分不同的頁面佈局(Layout):
base.html → 全站 HTML 骨架(head、navbar、footer)
├── blog_base.html → 部落格佈局(含側邊欄)
│ ├── post_list.html
│ └── post_detail.html
└── page_base.html → 單頁佈局(全寬,無側邊欄)
├── about.html
└── contact.html
{# templates/blog/blog_base.html — 第二層:部落格佈局 #}
{% extends "base.html" %}
{% block content %}
<div class="blog-layout">
<div class="blog-main">
{% block blog_content %}{% endblock %}
</div>
<aside class="blog-sidebar">
{% block sidebar %}
{% include "partials/sidebar.html" %}
{% endblock %}
</aside>
</div>
{% endblock %}
{# templates/blog/post_detail.html — 第三層:文章詳情頁 #}
{% extends "blog/blog_base.html" %}
{% block title %}{{ post.title }}{% endblock %}
{% block blog_content %}
<article>
<h1>{{ post.title }}</h1>
<time>{{ post.created_at|date:"Y年m月d日 H:i" }}</time>
<div class="post-body">
{{ post.content|safe }}
</div>
</article>
{% endblock %}
建議不要超過三層繼承,過多層次會讓模板的 block 追蹤變得困難。
{% include %} 引入可重用片段
{% include %} 將另一個模板檔案的內容嵌入到當前位置,非常適合重複使用的 UI 元件(Component):
{# templates/partials/navbar.html #}
<nav class="navbar">
<a href="{% url 'home' %}" class="logo">MyBlog</a>
<ul>
<li><a href="{% url 'post_list' %}">文章</a></li>
<li><a href="{% url 'about' %}">關於</a></li>
</ul>
</nav>
{# templates/partials/sidebar.html #}
<div class="sidebar">
<h3>熱門文章</h3>
<ul>
{% for post in popular_posts %}
<li><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></li>
{% endfor %}
</ul>
<h3>標籤</h3>
<div class="tag-cloud">
{% for tag in tags %}
<a href="{% url 'tag_posts' slug=tag.slug %}" class="tag">{{ tag.name }}</a>
{% endfor %}
</div>
</div>
{# templates/partials/footer.html #}
<footer>
<p>© {% now "Y" %} MyBlog. All rights reserved.</p>
</footer>
在 base.html 中使用 {% include %} 引入這些片段:
<body>
{% include "partials/navbar.html" %}
<main class="container">
{% block content %}{% endblock %}
</main>
{% include "partials/footer.html" %}
</body>
你也可以透過 with 關鍵字傳遞變數給被引入的模板:
{# 傳遞變數給 include 的模板 #}
{% include "partials/sidebar.html" with popular_posts=top_posts tags=all_tags %}
自訂模板標籤(Custom Template Tags)
當內建的標籤無法滿足需求時,你可以建立自訂模板標籤(Custom Template Tags)。
建立 templatetags 目錄
首先,在你的 app 目錄下建立 templatetags 套件:
myapp/
├── templatetags/ # 建立此目錄
│ ├── __init__.py # 必須有此檔案,讓 Python 識別為套件
│ └── blog_tags.py # 自訂標籤模組
├── models.py
├── views.py
└── ...
注意:建立 templatetags 目錄後,需要重新啟動開發伺服器,Django 才會偵測到新的模組。
simple_tag – 簡單標籤
simple_tag 接受參數並回傳一個值,可以直接輸出或存入模板變數:
# myapp/templatetags/blog_tags.py
from django import template
from myapp.models import Post, Tag
register = template.Library()
@register.simple_tag
def total_posts():
"""回傳已發布的文章總數"""
return Post.objects.filter(is_published=True).count()
@register.simple_tag(takes_context=True)
def user_post_count(context):
"""回傳當前登入使用者的文章數"""
user = context['request'].user
if user.is_authenticated:
return Post.objects.filter(author=user).count()
return 0
在模板中使用:
{% load blog_tags %}
{# 直接輸出 #}
<p>目前共有 {% total_posts %} 篇文章</p>
{# 存入變數再使用 #}
{% total_posts as post_count %}
{% if post_count > 100 %}
<p>我們已經累積超過 100 篇文章了!</p>
{% endif %}
{# 帶 context 的標籤 #}
<p>你發表了 {% user_post_count %} 篇文章</p>
inclusion_tag – 包含標籤
inclusion_tag 會渲染一個子模板並將結果嵌入,最適合可重用的 UI 元件:
# myapp/templatetags/blog_tags.py
@register.inclusion_tag('partials/popular_posts.html')
def show_popular_posts(count=5):
"""顯示熱門文章元件"""
posts = Post.objects.filter(
is_published=True
).order_by('-view_count')[:count]
return {'popular_posts': posts}
@register.inclusion_tag('partials/tag_cloud.html')
def show_tag_cloud():
"""顯示標籤雲元件"""
tags = Tag.objects.all()
return {'tags': tags}
{# templates/partials/popular_posts.html #}
<div class="popular-posts">
<h3>熱門文章</h3>
<ul>
{% for post in popular_posts %}
<li><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></li>
{% endfor %}
</ul>
</div>
{# 在任何模板中使用 #}
{% load blog_tags %}
{% show_popular_posts 3 %}
{% show_tag_cloud %}
自訂過濾器(Custom Template Filters)
過濾器(Filter)用於對模板變數進行轉換處理,使用管線符號 | 串接:
# myapp/templatetags/blog_filters.py
from django import template
from django.utils.safestring import mark_safe
import markdown as md
register = template.Library()
@register.filter(name='reading_time')
def reading_time(content):
"""估算文章閱讀時間(假設每分鐘閱讀 500 個中文字)"""
char_count = len(content)
minutes = max(1, round(char_count / 500))
return f'{minutes} 分鐘'
@register.filter(name='markdown')
def markdown_to_html(value):
"""將 Markdown 文字轉換為 HTML"""
return mark_safe(md.markdown(value, extensions=['extra', 'codehilite']))
@register.filter(name='truncate_chars_zh')
def truncate_chars_zh(value, length=50):
"""中文友善的字元截斷"""
if len(value) <= length:
return value
return value[:length] + '...'
在模板中使用:
{% load blog_filters %}
<p>閱讀時間:{{ post.content|reading_time }}</p>
<div class="post-body">{{ post.content|markdown }}</div>
<p>{{ post.content|truncate_chars_zh:100 }}</p>
靜態檔案處理(Static Files)
靜態檔案(Static Files)是指 CSS、JavaScript、圖片等不會隨請求變動的資源。Django 提供了完善的靜態檔案管理機制。
{% load static %} 與 {% static %} 標籤
在模板中引用靜態檔案,必須先載入 static 標籤庫:
{% load static %}
{# 引用 CSS #}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
{# 引用 JavaScript #}
<script src="{% static 'js/app.js' %}"></script>
{# 引用圖片 #}
<img src="{% static 'images/logo.png' %}" alt="Logo">
{% static %} 標籤會根據 settings.py 中的 STATIC_URL 設定自動生成正確的 URL 路徑,讓你不需要寫死路徑。
STATICFILES_DIRS 設定
在 settings.py 中配置靜態檔案的搜尋路徑:
# settings.py
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
# 靜態檔案的 URL 前綴
STATIC_URL = '/static/'
# 額外的靜態檔案搜尋目錄(開發時使用)
STATICFILES_DIRS = [
BASE_DIR / 'static', # 專案根目錄下的 static 資料夾
BASE_DIR / 'assets', # 也可以加入其他資料夾
]
# 正式環境:collectstatic 收集靜態檔案的目標目錄
STATIC_ROOT = BASE_DIR / 'staticfiles'
推薦的靜態檔案目錄結構:
my_django_project/
├── static/ # 全站共用的靜態檔案
│ ├── css/
│ │ └── main.css
│ ├── js/
│ │ └── main.js
│ └── images/
│ └── logo.png
├── myapp/
│ └── static/ # App 專屬的靜態檔案
│ └── myapp/ # 加一層 app 名稱避免命名衝突
│ ├── css/
│ │ └── blog.css
│ └── js/
│ └── blog.js
└── ...
collectstatic 命令
部署到正式環境(Production)時,需要執行 collectstatic 命令,將所有靜態檔案收集到 STATIC_ROOT 指定的目錄中:
# 收集所有靜態檔案到 STATIC_ROOT
python manage.py collectstatic
# 輸出範例:
# 128 static files copied to '/path/to/staticfiles'.
收集後的檔案通常由 Nginx 或 CDN(內容傳遞網路,Content Delivery Network)等專門的靜態檔案伺服器提供服務,而不是由 Django 處理,這樣能大幅提升效能。
Jinja2 整合簡介
Django 預設使用自己的模板引擎 DTL(Django Template Language),但也支援整合 Jinja2 – 另一個功能更強大的 Python 模板引擎。Jinja2 允許在模板中執行更多 Python 表達式,適合需要複雜模板邏輯的場景。
在 settings.py 中同時配置兩個模板引擎:
# settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
{
'BACKEND': 'django.template.backends.jinja2.Jinja2',
'DIRS': [BASE_DIR / 'jinja2_templates'],
'APP_DIRS': True,
'OPTIONS': {
'environment': 'myapp.jinja2_env.environment',
},
},
]
建立自訂的 Jinja2 環境設定:
# myapp/jinja2_env.py
from jinja2 import Environment
from django.contrib.staticfiles.storage import staticfiles_storage
from django.urls import reverse
def environment(**options):
env = Environment(**options)
env.globals.update({
'static': staticfiles_storage.url,
'url': reverse,
})
return env
大多數 Django 專案使用 DTL 就已足夠。如果你的團隊已經熟悉 Jinja2,或者需要在模板中執行複雜的表達式運算,再考慮整合 Jinja2。
完整範例:具有導航列、內容區、側邊欄的部落格模板
讓我們綜合以上所有知識,建立一個完整的部落格模板架構。
專案結構
templates/
├── base.html # 第一層:全站骨架
├── blog/
│ ├── blog_base.html # 第二層:部落格佈局(含側邊欄)
│ ├── post_list.html # 第三層:文章列表頁
│ └── post_detail.html # 第三層:文章詳情頁
└── partials/
├── navbar.html # 導航列元件
├── footer.html # 頁尾元件
└── sidebar.html # 側邊欄元件
第一層:base.html
{# templates/base.html #}
{% load static %}
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}首頁{% endblock %} | MyBlog</title>
<link rel="stylesheet" href="{% static 'css/main.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
{% include "partials/navbar.html" %}
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
<main>
{% block content %}{% endblock %}
</main>
{% include "partials/footer.html" %}
<script src="{% static 'js/main.js' %}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
第二層:blog_base.html
{# templates/blog/blog_base.html #}
{% extends "base.html" %}
{% load blog_tags %}
{% block content %}
<div class="blog-layout">
<section class="blog-main">
{% block blog_content %}{% endblock %}
</section>
<aside class="blog-sidebar">
{% block sidebar %}
{% show_popular_posts 5 %}
{% show_tag_cloud %}
{% endblock %}
</aside>
</div>
{% endblock %}
第三層:post_list.html
{# templates/blog/post_list.html #}
{% extends "blog/blog_base.html" %}
{% load blog_tags blog_filters %}
{% block title %}文章列表{% endblock %}
{% block blog_content %}
<h1>所有文章</h1>
{% total_posts as count %}
<p>共 {{ count }} 篇文章</p>
{% for post in posts %}
<article class="post-card">
<h2><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h2>
<div class="post-meta">
<time>{{ post.created_at|date:"Y年m月d日" }}</time>
<span>{{ post.content|reading_time }} 閱讀</span>
</div>
<p>{{ post.content|truncate_chars_zh:150 }}</p>
</article>
{% empty %}
<p>目前沒有任何文章。</p>
{% endfor %}
{# 分頁 #}
{% if is_paginated %}
<nav class="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">上一頁</a>
{% endif %}
<span>第 {{ page_obj.number }} 頁,共 {{ page_obj.paginator.num_pages }} 頁</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">下一頁</a>
{% endif %}
</nav>
{% endif %}
{% endblock %}
第三層:post_detail.html
{# templates/blog/post_detail.html #}
{% extends "blog/blog_base.html" %}
{% load blog_filters %}
{% block title %}{{ post.title }}{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/highlight.css' %}">
{% endblock %}
{% block blog_content %}
<article class="post-detail">
<header>
<h1>{{ post.title }}</h1>
<div class="post-meta">
<span>作者:{{ post.author.get_full_name|default:post.author.username }}</span>
<time>{{ post.created_at|date:"Y年m月d日 H:i" }}</time>
<span>{{ post.content|reading_time }} 閱讀</span>
</div>
</header>
<div class="post-body">
{{ post.content|markdown }}
</div>
<div class="post-tags">
{% for tag in post.tags.all %}
<a href="{% url 'tag_posts' slug=tag.slug %}" class="tag">{{ tag.name }}</a>
{% endfor %}
</div>
</article>
{% endblock %}
{% block extra_js %}
<script src="{% static 'js/highlight.js' %}"></script>
{% endblock %}
總結
本文從 DRY 原則 出發,系統性地介紹了 Django 模板系統的進階功能。透過 {% extends %} 與 {% block %} 建立 模板繼承 架構,用 {% include %} 拆分可重用的 UI 元件,再以自訂 Template Tags 和 Template Filters 擴充模板的表達能力。在靜態檔案處理方面,我們學會了使用 {% static %} 標籤引用資源、配置 STATICFILES_DIRS 管理搜尋路徑,以及透過 collectstatic 命令為正式環境做準備。
掌握這些進階模板技巧後,你就能設計出結構清晰、高度可維護的模板架構 – 從三層繼承的佈局系統到模組化的 UI 元件,讓團隊中的前端設計師和後端開發者都能高效率地協作。在下一篇文章中,我們將進入 Django Forms 的世界,學習如何處理表單的建立、驗證與渲染。