跳轉到

樣板進階

開始之前

任務目標

在這個章節中,我們會完成:

  • 了解 Template 繼承與 Include
  • 建立專案級別的 Templates 資料夾
  • 實作多重繼承架構

Template 繼承

Template 繼承(Template Inheritance)是 Django 樣板系統最強大的功能之一,讓你可以建立一個基礎樣板,然後在其他樣板中繼承它。

為什麼需要繼承?

假設你的網站有多個頁面,每個頁面都有相同的導覽列、頁尾,只有中間的內容不同。如果每個頁面都重複寫一次相同的 HTML,會有以下問題:

  • 程式碼重複,難以維護
  • 修改導覽列時需要更新所有頁面
  • 容易出錯或遺漏

使用 Template 繼承,你可以:

  • 將共用的部分寫在基礎樣板中
  • 在子樣板中只需要定義不同的部分
  • 修改基礎樣板時,所有繼承它的頁面都會自動更新

基本概念

Template 繼承使用兩個主要的標籤:

  • {% block %} - 定義可以被子樣板覆寫的區塊
  • {% extends %} - 繼承父樣板

建立專案級別的 Templates

在開始實作之前,我們先建立專案級別的 templates 資料夾。

為什麼需要專案級別的 Templates?

  • App 級別的 templates:放在 app/templates/ 中,只給該 App 使用
  • 專案級別的 templates:放在專案根目錄的 templates/ 中,可以被所有 App 共用

專案級別的 templates 適合放置:

  • 網站的基礎樣板(base.html)
  • 共用的元件(導覽列、頁尾等)
  • 錯誤頁面(404.html、500.html)

建立 templates 資料夾

在專案根目錄(與 manage.py 同層)建立 templates 資料夾:

mkdir templates

目錄結構:

django-playground/
├── blog/
├── core/
├── practices/
├── templates/          # 專案級別的 templates
├── manage.py
└── db.sqlite3

設定 Django

修改 core/settings.py,告訴 Django 去哪裡找專案級別的 templates:

core/settings.py
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [
            BASE_DIR / "templates",  # (1)!
        ],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]
  1. 加入專案級別的 templates 路徑

DIRS vs APP_DIRS

  • DIRS:指定專案級別的 templates 資料夾
  • APP_DIRS:設為 True 時,Django 會在每個 App 的 templates/ 資料夾中尋找樣板

Django 的搜尋順序:

  1. 先在 DIRS 指定的資料夾中尋找
  2. 再到各個 App 的 templates/ 資料夾中尋找

實作基礎樣板

建立專案級別的 base.html

templates/ 資料夾中建立 base.html

templates/base.html
<!DOCTYPE html>
<html lang="zh-Hant">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Django Playground" />
    <meta name="keywords" content="Django, Playground" />

    <title>
      {% block title %}
        Django 大冒險
      {% endblock title %}
    </title>

    {% block extra_head %}
    {% endblock extra_head %}
  </head>
  <body>
    <header>
      <nav>
        <h1>
          Django 大冒險
        </h1>
      </nav>
    </header>

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

    <footer>
      <p>
        Django Playground
      </p>
    </footer>

    {% block extra_scripts %}
    {% endblock extra_scripts %}
  </body>
</html>

這個基礎樣板定義了四個 block:

Block 用途
title 頁面標題
extra_head 額外的 CSS 或 meta 標籤
content 主要內容區域
extra_scripts 額外的 JavaScript

在 App 中使用基礎樣板

建立 blog/templates/blog/article_list.html 檔案:

blog/templates/blog/article_list.html
{% extends "base.html" %}

{% block title %}
  文章列表 - Django 大冒險
{% endblock title %}

{% block content %}
  <h2>
    文章列表
  </h2>
  <ul>
    {% for article in articles %}
      <li>
        {{ article.title }} - {{ article.author.name }}
      </li>
    {% empty %}
      <li>
        目前沒有文章
      </li>
    {% endfor %}
  </ul>
{% endblock content %}

建立對應的 view:

blog/views.py
from django.shortcuts import render

from blog.models import Article


def article_list(request):
    articles = Article.objects.all()
    return render(request, "blog/article_list.html", {"articles": articles})

設定 URL:

blog/urls.py
from django.urls import path

from blog import views

urlpatterns = [
    path("articles/", views.article_list, name="article_list"),
]
core/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("practices/", include("practices.urls")),
    path("blog/", include("blog.urls")),
]

Block 的預設內容

Block 可以有預設內容,子樣板可以選擇是否覆寫:

templates/base.html
<!DOCTYPE html>
<html lang="zh-Hant">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Django Playground" />
    <meta name="keywords" content="Django, Playground" />

    <title>
      {% block title %}
        Django 大冒險
      {% endblock title %}
    </title>

    {% block extra_head %}
    {% endblock extra_head %}
  </head>
  <body>
    <header>
      <nav>
        <h1>
          Django 大冒險
        </h1>
      </nav>
    </header>

    <main>
      {% block content %}
        <p>
          歡迎來到 Django 大冒險!
        </p>
      {% endblock content %}
    </main>

    <footer>
      <p>
        Django Playground
      </p>
    </footer>

    {% block extra_scripts %}
    {% endblock extra_scripts %}
  </body>
</html>

如果子樣板沒有覆寫 content block,就會顯示預設的「歡迎來到 Django 大冒險!」。

使用 block.super

有時候你想要保留父樣板的內容,並在其基礎上添加新內容,可以使用 {{ block.super }}

blog/templates/blog/article_detail.html
{% extends "base.html" %}

{% block title %}
  {{ article.title }} - {{ block.super }}
{% endblock title %}

{% block content %}
  <article>
    <h2>
      {{ article.title }}
    </h2>
    <p>
      作者:{{ article.author.name }}
    </p>
    <p>
      發布時間:{{ article.created_at }}
    </p>
    <div>
      {{ article.content }}
    </div>
  </article>
{% endblock content %}

建立對應的 view:

blog/views.py
from django.shortcuts import render

from blog.models import Article


def article_list(request):
    articles = Article.objects.all()
    return render(request, "blog/article_list.html", {"articles": articles})


def article_detail(request, article_id):
    article = Article.objects.get(id=article_id)
    return render(request, "blog/article_detail.html", {"article": article})

設定 URL:

blog/urls.py
from django.urls import path

from blog import views

urlpatterns = [
    path("articles/", views.article_list, name="article_list"),
    path("articles/<int:article_id>/", views.article_detail, name="article_detail"),
]

接著訪問 http://localhost:8000/blog/articles/1/,會看到網站 title 是:文章標題 - Django 大冒險

Warning

上方的網址範例可能會沒有編號 2 的文章,如果出現 404 的頁面,請修正網址的 ID 部分,例如 http://localhost:8000/blog/articles/2/ 之類的

Template Include

除了繼承,Django 還提供 {% include %} 來引入其他樣板片段。

Include vs Extends

功能 Extends Include
用途 繼承父樣板的結構 引入樣板片段
數量限制 一個樣板只能 extends 一次 可以 include 多次
Block 可以覆寫 block 無法覆寫 block
使用場景 頁面整體結構 可重複使用的元件

Include 範例

建立一個共用的文章卡片元件:

blog/templates/blog/components/article_card.html
<div class="article-card">
  <h3>
    {{ article.title }}
  </h3>
  <p>
    {{ article.author.name }} | {{ article.created_at }}
  </p>
  <p>
    {{ article.content|truncatewords:30 }}
  </p>
  <a href="{% url 'article_detail' article.id %}">閱讀更多</a>
</div>

在 Template 中產生 URL

{% url %} 是 Django 內建的 template tag,用來根據 URL name 產生對應的網址。

語法:{% url 'url_name' arg1 arg2 ... %}

使用 {% url %} 的好處是:

  • 不需要硬編碼網址,避免修改 URL 時要到處更新
  • 自動處理參數,例如 {% url 'article_detail' article.id %} 會生成 /blog/articles/1/
  • 如果 URL pattern 改變,只要 name 不變,所有使用 {% url %} 的地方都會自動更新

如果想要知道有哪些內建的 template tag 可以使用可以參考:https://docs.djangoproject.com/en/5.2/ref/templates/builtins/

在其他樣板中使用:

blog/templates/blog/article_list.html
{% extends "base.html" %}

{% block title %}
  文章列表 - Django 大冒險
{% endblock title %}

{% block content %}
  <h2>
    文章列表
  </h2>
  <div class="article-grid">
    {% for article in articles %}
      {% include "blog/components/article_card.html" %}
    {% empty %}
      <p>
        目前沒有文章
      </p>
    {% endfor %}
  </div>
{% endblock content %}

Include 的變數傳遞

Include 的樣板可以存取父樣板的所有變數。在上面的範例中,article_card.html 可以使用 article 變數,因為它在 for 迴圈中。

你也可以使用 with 明確傳遞變數:

{% include "components/article_card.html" with article=featured_article %}

多重繼承架構

在大型專案中,我們通常會建立多層的樣板繼承結構。

架構說明

graph TD
    A[templates/base.html<br/>專案基礎樣板] --> B[blog/templates/blog/base.html<br/>App 基礎樣板]
    B --> C[blog/templates/blog/article_list.html<br/>文章列表頁]
    B --> D[blog/templates/blog/article_detail.html<br/>文章詳細頁]
  • 專案級 base.html:定義整個網站的共用結構(導覽列、頁尾)
  • App 級 base.html:繼承專案級 base.html,定義該 App 的共用結構
  • 頁面樣板:繼承 App 級 base.html,只需要定義頁面特有的內容

實作 App 級基礎樣板

建立 blog/templates/blog/base.html

blog/templates/blog/base.html
{% extends "base.html" %}

{% block content %}
  <div class="blog-container">
    <aside class="sidebar">
      <h3>
        文章管理
      </h3>
      <ul>
        <li>
          <a href="{% url 'article_list' %}">文章列表</a>
        </li>
      </ul>
    </aside>

    <div class="main-content">
      {% block blog_content %}
      {% endblock blog_content %}
    </div>
  </div>
{% endblock content %}

這個 App 級樣板:

  1. 繼承專案級的 base.html
  2. 覆寫 content block,加入側邊欄
  3. 定義新的 blog_content block 給子樣板使用

使用 App 級基礎樣板

修改文章列表頁,改為繼承 App 級樣板:

blog/templates/blog/article_list.html
{% extends "blog/base.html" %}

{% block title %}
  文章列表 - Django 大冒險
{% endblock title %}

{% block blog_content %}
  <h2>
    文章列表
  </h2>
  <div class="article-grid">
    {% for article in articles %}
      {% include "blog/components/article_card.html" %}
    {% empty %}
      <p>
        目前沒有文章
      </p>
    {% endfor %}
  </div>
{% endblock blog_content %}

現在這個頁面會:

  1. 繼承 blog/base.html(App 級)
  2. App 級樣板會繼承 base.html(專案級)
  3. 最終頁面會包含:導覽列、側邊欄、文章列表、頁尾

繼承鏈

Django 會按照繼承鏈組合所有樣板:

article_list.html (blog_content)
blog/base.html (content + sidebar)
base.html (整體結構)

每一層都可以覆寫或擴充上一層的內容。

常見問題

為什麼我的樣板找不到?

檢查以下幾點:

  1. 確認 TEMPLATESDIRS 設定正確
  2. 檢查樣板檔案路徑是否正確
  3. 確認 App 已經加入 INSTALLED_APPS
  4. 重新啟動開發伺服器

Block 名稱重複怎麼辦?

如果多個樣板定義了相同名稱的 block,最後繼承的會生效。建議:

  • 專案級樣板使用通用名稱(contenttitle
  • App 級樣板使用帶前綴的名稱(blog_contentblog_sidebar

可以繼承多個樣板嗎?

不行,一個樣板只能 {% extends %} 一個父樣板。但可以透過多層繼承來達成類似效果:

A.html ← B.html ← C.html

Include 會影響效能嗎?

Include 會增加額外的檔案讀取,但 Django 有樣板快取機制。在正式環境中影響很小,在開發環境中可能會稍微慢一點。

只要不過度使用(例如在迴圈中 include 大量樣板),效能影響可以忽略。

任務結束

完成!

恭喜你完成了這個章節!現在你已經:

  • 了解 Template 繼承與 Include
  • 建立專案級別的 Templates 資料夾
  • 實作多重繼承架構

補充說明

Template 繼承是 Django 樣板系統的核心功能,掌握它可以:

  • 大幅減少重複的程式碼
  • 讓樣板結構更清晰易維護
  • 建立一致的使用者介面

在實際專案中,建議:

  • 規劃好樣板繼承結構
  • 合理劃分 block,不要太細也不要太粗
  • 使用有意義的 block 名稱
  • 將可重複使用的元件抽出成獨立檔案