almost 4 years ago

在本篇中要講述一些有關於template(模版)的進階技巧。學習了相關的知識之後,讀者們可以寫出架構更好的頁面,同時也會發現,我們能省下大量的時間。


  • 主題1-重複利用模板
    • 匯入模板
    • 模板繼承
      • 模板繼承的步驟
      • 模板繼承的策略
      • 模板繼承的原則
      • 利用模板繼承更新我們的網站
  • 主題2-RequestContext與Context處理器
    • Context處理器
    • 使用render_to_response
    • 使用render
    • Django預設的處理器
  • 主題3-自定義過濾器
    • 前置作業
    • 過濾器函數
    • 註冊過濾器
    • 載入過濾器
    • 使用裝飾器

主題1-重複利用模版

當我們花了時間撰寫了一個模版後(尤其是寫了一堆html標籤累的要死),卻無法重複利用他,這可是會讓人心情沮喪的。好在模板的設計本身就是一個可再利用的資源,透過使用同一個template,可以讓相似度高的頁面(視圖)共用同一個模板。但是我們難道沒辦法利用已經寫好的template作為新template的一部分嗎?

如果你還不能明白我的意思,我就講的再直接一點:封裝、匯入、繼承...
這些在程式設計上耳熟能詳的代名詞有沒有讓你有一些想法呢?

其實撰寫一個模版我們就運用了封裝的技巧,透過參數化的方式,使得普通的html頁面變成能接受context填寫而重複利用的樣板。

那匯入或是繼承呢?

匯入模版

使用{% include %}標籤就可以將模板匯入,以餐廳王的首頁來說:

mysite/templates/index.html
<html>
    <head>
        <title>Index</title>
        <meta charset="utf-8">
    </head>
    <body>
        <h2>歡迎來到餐廳王</h2> <p><a href="/accounts/register/">註冊</a></p>
        {% if request.user.is_authenticated %}
            <p>
                {{request.user}} 您已經登入囉~
                <a href="/accounts/logout/">登出</a>
            </p>
            <a href="/restaurants_list/">餐廳列表</a>
        {% else %}
            <p>您尚未登入喔~<a href="/accounts/login/">登入</a></p>
        {% endif %}
    </body>
</html>

我們可以將之拆解為:

index.html
<html>
    <head>
        <title>Index</title>
        <meta charset="utf-8">
    </head>
    <body>
        {% include 'body.html' %}
    </body>
</html>

body.html
<h2>歡迎來到餐廳王</h2> <p><a href="/accounts/register/">註冊</a></p>
    {% if request.user.is_authenticated %}
        <p>
            {{request.user}} 您已經登入囉~
            <a href="/accounts/logout/">登出</a>
        </p>
        <a href="/restaurants_list/">餐廳列表</a>
    {% else %}
        <p>您尚未登入喔~<a href="/accounts/login/">登入</a></p>
    {% endif %}

當然我們也可以考慮反向的匯入,也就是說,在index.html中保留<body>的部份而include<head>等其他內容。
這完全看哪一部分的html代碼是能重複利用的。

我們將{% include %}標籤的完整用法整理如下:

{% include '模版名稱(路徑)' %}

模版名稱(路徑)是一個字串,所以記得用單引號或是雙引號括起來,這個名稱可以是一個模板目錄下的template file,也可以是一個在模板目錄下包含了其子目錄在內的路徑。

如果模板名稱(路徑)有誤,在除錯模式下會顯示TemplateDoesNotExist錯誤,在一般模式下則會略過。

模板繼承

不過使用者深入去思考後會發現,{% include %}標籤其實並不是那麼好用,怎麼說呢?

我們舉個例子,假設我們有一個要套用到所有頁面的樣式如下:

<!doctype html>
<html>
    <head>
        <title>(頁首標題)</title>
    </head>
    <body>
        網頁選單
        <h1>(頁面標題)</h1>
        <h1>說明</h1>
        <p>(內容)</p>
        頁尾
    </body>
</html>

我們可以將整個頁面樣式區分為所有頁面共有的部分和自定義的部份:

共有部分:<html><head><body>等標籤,網頁選單、頁尾等
自定義部分(以小括號括起來的部分):頁首標題、頁面標題、內容

若使用{% include %}我們必須選定上述兩部分其中一個當做基礎,再包含進其他的部份。

舉例來說,我們可以產生以下幾個html(為了讓大家了解,我就暴力法拆解給大家看吧):

A.html
<!doctype html>
<html>
    <head>
B.html
</head>
<body>
    網頁選單
C.html
    <h1>說明</h1>
D.html
頁尾
    </body>
</html>
final.html
{% include 'A.html' %}
        <title>(頁首標題)</title>
{% include 'B.html' %}
        <h1>(頁面標題)</h1>
{% include 'C.html' %}
        <p>(內容)</p>
{% include 'D.html' %}

我們發現產生一個頁面需要花上5個html檔,且檔案零碎。

這使得include的動作異常複雜,當共有部分和自定義部分交錯的越嚴重時,這會讓需要include的數量大幅提昇(這還不打緊,反而是撰寫每一個被include進來的頁面才痛苦,不但要寫超多個html檔,還要忍受這些零碎的痛苦。)

問題就在於這些被重複利用的部份必須完全被切割!
所以我們比較鼓勵讀者使用Django的模版繼承。

怎麼做呢,我們直接來看一個例子,我們首先要制定一個基礎的模版,他應該要包含所有共同部份:

base.html
<!doctype html>
<html>
    <head>
        <title>{% block title %}{% endblock %}</title>
    </head>
    <body>
        網頁選單
        <h1>{% block pagetitle %}{% endblock %}</h1>
        <h1>說明</h1>
        <p>{% block content %}{% endblock %}</p>
        頁尾
    </body>
</html>

記得,可以在每個需要自定義的地方,使用模板區塊。
一個模板區塊以{% block BLOCKNAME %}為開頭,以{% endblock %}為結尾。可以填入內容也可以留白,這個區塊的內容會讓繼承此基礎模板的子模板覆寫,這與物件導向中關於類別的繼承與覆寫很類似。

接著使用{% extends %}標籤讓子模板去繼承基礎模板:

final.html
{% extends 'base.html' %}
{% block title %}(頁首標題){% endblock %}
{% block pagetitle %}(頁面標題){% endblock %}
{% block content %}(內容){% endblock %}

{% extends TEMPLATENAME %}標籤的參數TEMPLATENAME可以是一個字面的字串,也可以是一個變量。當使用變量的時候,可以動態地更換繼承的模板達到一些特殊的效果。當然,不論用字串或是變量,該參數所指名的模板應該要位於任何我們能夠存取到的模板目錄中。

若我們有重新定義某區塊的內容,則基礎模板中該區塊的內容會被我們覆寫,否則使用基礎模板中的內容。

模板繼承的步驟

我們會發現,經過繼承設計的模板,更簡潔,更完整。我們在下面整理一下關於模板設計的幾個要點:

1. 定義基礎模板,並在其中使用{% block BLOCKNAME %}和{% endblock %}來定義模板區塊。
2. 利用{% extends BASE_TEMPLATE_NAME %}讓子模板繼承基礎模板。
3. 使用模板區塊來覆寫(填寫)基礎模板的內容,未被覆寫的區塊將會採用原基礎模板的內容。

模板繼承的策略

至於繼承的策略我們大致在這裡分析一下,常見的設計手法是一種三層式的設計:

第一層: 撰寫一個全站的base模板,應該包含全站等級的頁首/頁尾和選單。
第二層: 針對幾個特定的功能集合撰寫不同的區域模板(繼承base模板),應該包含特定功能的選單及功能。
第三層: 每一個特定功能的模板實作,應該要根據分類繼承區域模板。

模板繼承的原則

那設計的原則也有以下兩點:

  1. 基礎模板中的{% block %}區塊越多越好,雖然並不是每個區塊都要求繼承他的子模板來覆寫,但是預留更多可變得位置,會使得模板設計上更靈活。
  2. 如果發現有大量重複的代碼在存在於各個模板中,這些應該考慮抽取出來放置在基礎模板。這與OOP的精神相同。

接下來我們便使用前面介紹的模板繼承概念來整頓一下我們的餐廳王網站吧!

利用模板繼承更新我們的網站

由於我們目前的網站結構還算簡單,我們只採用兩層的繼承架構。
那第一步便是為整個網站製作一個基礎模板base.html:

mysite/templates/base.html
<!doctype html>
<html>
    <head>
        <title>{% block title %}{% endblock %}</title>
        <meta charset="utf-8">
    </head>
    <body>
        <nav>
            | <a href="/restaurants_list/">餐廳列表</a> |
            {% if request.user.is_authenticated %}
                {{ request.user }}
                <a href="/accounts/logout/">登出</a>
            {% else %}
                <a href="/accounts/login/">登入</a> /
                <a href="/accounts/register/">註冊</a>
            {% endif %}
        </nav>
        <h2>{% block h2 %}{% endblock %}</h2>
        {% block content %}{% endblock %}
    </body>
</html>

這個基礎模板包含了頭尾的一些基礎標籤以及一個全站通用的nav bar(網站選單),和幾個待填空的模板區塊。
這裡總共用了四個模板區塊,有在瀏覽器的title block,各頁面的標題h2 block和內容content block。

在這裡我們也利用{% request.user %}對已登入的使用者顯示他們的username。

接著,利用繼承base.html,我們來整頓其他的頁面,我們以首頁做個示範,至於其他的頁面,就留待讀者自行更動調整:

mysite/templates/index.html
{% extends 'base.html' %}

{% block title %} 首頁 {% endblock%}
{% block h2 %} 餐廳王 {% endblock %}

{% block content %}
    <p>歡迎來到餐廳王</p>
{% endblock %}

是不是清爽許多了呢?
我們會發現,透過全站統一繼承一個基礎模板,不但使的各頁面的代碼更簡短清晰,更可以簡單的共用統一的風格跟選單。
讀者們還會發現,當下一次要新增一個模板時,會輕鬆不少。

主題2-RequestContext與Context處理器

還記得在第三篇模板中我們曾經詳細地介紹過使用模板的方式嗎?
那該篇筆記中,我們最後得出來的結論是:使用render_to_response函式來完成創建模板到填寫模板到回應的動作。
如果讀者們對於基礎的觀念已經有點淡忘了,可以回到該篇筆記查查,因為我們要利用最基礎的手法來闡述RequestContext的用途。

最原始的模板處理步驟,我們首先得用django.template.Template類別來創造一個模板。
接著使用djanog.template.Context來設置一些待填的變量,Context是一個純然由使用者打造的類字典物件,缺點很明顯,每一次要使用時都必須要手動設置。

解決這個問題就得靠django.template.RequestContext這個物件,他與Context類似,但比Context多了兩項資訊,其一是HttpRequest,其二是Context處理器,這讓我們得以使用一個已經設置好若干訊息(透過HttpRequest和Context處理器)的Context來填充模板,而不需要每次都手動建造(即便locals函數已經能夠幫我們省下很多時間了,但仍然不足)。

我們來舉個例子:

from django.template import loader, Context

def view1(request):
    ...
    t = loader.get_template('template1.html')
    c = Context({
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'View 1'
    })
    return t.render(c)

def view2(request):
    ...
    t = loader.get_template('template2.html')
    c = Context({
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'View 2'
    })
    return t.render(c)

這兩個視圖函數都會使用Context來填寫各自的模版,我們發現,'user'和'ip_address'兩項資訊根本一模一樣,卻必須要在兩個view function裡面都手動設置,這其實非常累贅。

Context處理器

一個解決的辦法便是使用Context processor(Context處理器),Context處理器其實只是一個function,這個函數以HttpRequest物件為參數,並會回傳一個包含了Context共有資訊的字典。我們看看上面的範例要怎麼用Context處理器加以改良:

from django.template import loader, RequestContext

def context_proc(request):
    return {
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
    }

def view1(request):
    ...
    t = loader.get_template('template1.html')
    rc = RequestContext(request, {'message': 'View 1'}, processors=[context_proc])
    return t.render(rc)

def view2(request):
    ...
    t = loader.get_template('template2.html')
    rc = RequestContext(request, {'message': 'View 2'}, processors=[context_proc])
    return t.render(c)

我們透過context_proc將view1和view2中Context共有的部分:'user'和'ip_address'提取出來,並且在兩個視圖函式中改用RequestContext,如此便可以省去不少需要手動設置的功夫了!

同時,我們也觀察到RequestContext的結構:

RequestContext(HttpRequest, 字典(非共有資訊), processors=[處理器1,處理器2,...])

第一個參數要給定一個HttpRequest物件,第二個參數是非共有資訊集合成的字典,另外還有一個名為processors的參數,這是一個處理器清單(list),裡面擺置用來處理共有資訊的處理器們。

使用render_to_response

但光是這樣我們還不滿足,我們之前的快捷手段:render_to_response難道派不上用場了嗎?當然不!
只需要多給予一個context_instance的參數就可以囉:

from django.template import loader, RequestContext
from django.shortcuts import render_to_response

def context_proc(request):
    return {
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
    }

def view1(request):
    ...
    return render_to_response(
        'template1.html',
        {'message': 'View 1'}, 
        context_instance = RequestContext(request, processors=[context_proc])
    )

def view2(request):
    ...
    return render_to_response(
        'template2.html',
        {'message': 'View 2'}, 
        context_instance = RequestContext(request, processors=[context_proc])
    )

不過看起來,代碼並沒有減少多少,這是因為我們將原本設置Context資訊的功夫花在設置Context處理器了。
難道Context處理器並非一個好的選擇?
不,這依然是個優秀的處理手段,因為Django允許我們在settings.py中設置processors,這樣就不需要每次都要填寫processrs了,很棒吧。

實際的作法非常簡單,在settings.py中設置TEMPLATE_CONTEXT_PROCESSORS元組,將想要加入的處理器路徑加入即可,這會導致任何一個RequestContext物件都會含有該些處理器所提供的資訊。

比如說處理器context_proc位於mysite/mysite/views.py中,我們只需:

mysite/mysite/settings.py
...
from django.conf import global_settings
...
TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + 
                             ("mysite.views.context_proc",)

就可以省略RequestContext中處理器的設置,視圖函數可簡化為:

from django.template import loader, RequestContext
from django.shortcuts import render_to_response

def context_proc(request):
    return {
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
    }

def view1(request):
    ...
    return render_to_response(
        'template1.html',
        {'message': 'View 1'}, 
        context_instance = RequestContext(request)
    )

def view2(request):
    ...
    return render_to_response(
        'template2.html',
        {'message': 'View 2'}, 
        context_instance = RequestContext(request)
    )

這邊稍微來解釋一下settings.py中的作法,這邊給讀者一個概念,並非所有的設定值都會出現在專案的settings.py中,很多的設定值會被寫在django裡面的global_settings.py,只要使用者沒有自行在自己的settings.py中覆寫這些參數,Django會使用global_settings.py的預設值。

我們現在想要設置TEMPLATE_CONTEXT_PROCESSORS,這是一個元組,在global_settings.py中早就有若干Context處理器被設置在其中,若我們貿然將TEMPLATE_CONTEXT_PROCESSORS覆寫掉可能會造成不少錯誤,為了避免這一點,我們得先將原始的處理器元組匯入,再串接上新的處理器。

還有一個問題是,如果不同的處理器提供了相同名稱的變量怎麼辦?這個時候將會依照處理器被看到的順序設置,後看到的處理器會覆蓋掉前面處理器中相同名稱的變量。

使用render

好了,做到這裡,讀者們可能會覺得這個主題已經告一段落了,但是,難道沒有再更精簡的手法嗎?
其實有的,有個函數比起render_to_response更神奇,那就是render函式:

from django.shortcuts import render

def context_proc(request):
    return {
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
    }

def view1(request):
    ...
    return render(request, 'template1.html', {'message': 'View 1'})

def view2(request):
    ...
    return render(request, 'template2.html', {'message': 'View 2'})

render函式比需要由django.shortcuts中匯入,他跟render_to_response的功能幾乎一樣,除了他強制使用RequestContext作為Context之外,所以我們得出一個結論:

需要用到處理器及HttpRequest資訊的時候用render
否則用render_to_response

render的使用方式如下:

render(HttpRequest, TEMPLATENAME, DICTIONARY)

第一個參數是HttpRequest物件,第二個參數是模板名稱,最後一個是非共有資訊的字典。
讀者若有感悟,應回到前面的筆記,將有使用到RequestContext的地方都改為用render

Django預設的處理器

不過讀者也許會很好奇,在global_settings.py中究竟有哪些預設的處理器,又各提供了哪些有用的資訊(變量)給模板呢?
打開global_settings.py就能夠看到TEMPLATE_CONTEXT_PROCESSORS的預設值:

(
    "django.contrib.auth.context_processors.auth",
    "django.core.context_processors.debug",
    "django.core.context_processors.i18n",
    "django.core.context_processors.media",
    "django.core.context_processors.static",
    "django.core.context_processors.tz",
    "django.contrib.messages.context_processors.messages"
)

有興趣的讀者可以前往Django官網進行詳細的了解。

在結束這個主題之前,我們回憶一下在權限筆記中,我們曾經用過RequestContext來提供perms變量:

mysite/restaurants/restaurants_list.html
...
        {% if perms.restaurants.can_comment %}
            <th>評價</th>
        {% endif %}
...

這就是RequestContext的威力。

主題三-自定義過濾器

自己創造一個過濾器是再簡單也不過的事情了,只要兩個步驟:

1. 寫一個過濾器函數
2. 註冊該函數
3. 載入過濾器

除此之外當然還有一些前置作業要做,就讓我們來一一說明吧!

前置作業

首先我們在restaurants這個APP中新增一個templatetags目錄,這個目錄將會放置我們自定義的過濾器模組或標籤模組,為了使templatetags成為一個python package,大家千萬別忘了建立一個__init__.py

接著我們在該目錄下建立一個python module: myfilters.py,顧名思義就是用來撰寫我們的過濾器函數的地方啦。我們會發現現在的結構為:

mysite-|--manage.py
       |--templates(子結構省略)
       |--mysite(子結構省略)
       |--restaurants--|--__init__.py
                       |--admin.py
                       |--models.py
                       |--tests.py
                       |--views.py
                       |--forms.py
                       |--templatetags--|--__init__.py
                                        |--myfilters.py

過濾器函數

接著便讓我們來撰寫過濾器函數。
讀者要有一個概念就是,過濾器本身是一個函數,我們來看一個簡單的例子,還記得我們的menu模板嗎:

mysite/restaurants/templates/menu.html
<tr>
    <td> {{ food.name }} </td>
    <td> {{ food.price }} </td>
    <td> {{ food.comment }} </td>
    <td> {% if food.is_spicy %} 辣 {% else %} 不辣 {% endif %} </td>
</tr>

當初我們為了辣與不辣的顯示採用了if/else標籤,我們現在試著換一個方式,用過濾器來幫我們解決,我們想要利用以下的方式來完成一樣的功能:

mysite/restaurants/templates/menu.html
<tr>
    <td> {{ food.name }} </td>
    <td> {{ food.price }} </td>
    <td> {{ food.comment }} </td>
    <td> {{ food.is_spicy|yes_no:"辣/不辣" }} </td>
</tr>

這裡整理一下這個過濾器的要素:

  1. 名字叫做yes_no
  2. 接受一個布林值的主要參數
  3. 接受一個額外的字串參數Y/N,如果主要參數為真則輸出Y,否則輸出N

要怎麼做呢?
首先打開myfilters.py:

mysite/restaurants/templatetags/myfilters.py
def yes_no(bool_value, show_str):
    if bool_value:
        return show_str.partition('/')[0]
    else:
        return show_str.partition('/')[1] 

我們寫了一個名叫yes_no的函數,並且需要兩個參數,在這裡,函數的名字不需要跟我們自定義的過濾器名稱一樣,但是選擇相同名稱通常是比較好的作法,bool_value是過濾器的第一個參數,他負責接收模板中pipe符號(|)左邊的值;而show_str是第二個參數,會用來接收過濾器中的額外參數,以下是簡單的對應示意:

{{ food.is_spicy|yes_no:"辣/不辣" }}
   ------------- ------ --------
          2.        1.     3.    
def yes_no(bool_value, show_str):
    ------ ----------  --------
       1.       2.        3.

值得注意的是,過濾器函數我們總是要保證他是對的,也就是說,我們不允許該函數能夠拋出例外。這是相當合理的要求,因為任何的例外都會導致網站顯示錯誤,我們應該想辦法對於不可避免的例外採行捕捉,並且回傳一個空白字串好讓事情圓滿(不過這裡我們就不多做示範囉)。

註冊過濾器

下一步我們要將他註冊給django知道,我們利用django.template中的Library來做到這一點:

mysite/restaurants/templatetags/myfilters.py
from django import template
...
register = template.Library()
register.filter('yes_no',yes_no)

在這裡我們使用register的filter函數來幫助我們註冊,filter函數的第一個參數是過濾器的名稱,第二個參數是他對應的過濾器函數(讀者應該明白了為什麼過濾器函數的名稱可以不同於過濾器名稱了吧!)

要注意的是,千萬別自作聰明的這樣寫:

template.Library().filter('yes_no',yes_no)

或是:

reg = template.Library()
reg.filter('yes_no',yes_no)

總之沒有把register(名字也要一模一樣)給建立出來就是會出錯誤!要小心!

當然,為了快捷,django也允許我們使用方便的裝飾器來註冊:

@register.filter(name='yes_no')
def yes_no(bool_value, show_str):
...

透過@register.filter裝飾器可以讓被修飾的函數自動註冊為一個過濾器,該裝飾器的參數name是個可選的參數,用來定義過濾器的名稱,如果使用者不提供該參數,django預設會使用過濾器函數的名稱作為過濾器名稱。

載入過濾器

最後我們可以在模板裡面載入他囉:

mysite/restaurants/templates/menu.html
{% load myfilters %}
...
                <tr>
                    <td> {{ food.name }} </td>
                    <td> {{ food.price }} </td>
                    <td> {{ food.comment }} </td>
                    <td> {{ food.is_spicy|yes_no:"辣/不辣" }} </td>
                </tr>
...

使用{% load %}標籤可以使我們載入myfilter中的過濾器,在這裡,我們只能載入restaurantsapp中的過濾器。

這邊讀者要特別注意,templatetags或是register等等的名稱一定要百分之百跟教學中的一模一樣,至於自定義過濾器的模組名稱可以自由發揮,要多寫幾個模組也無所謂,只要記得使用前要將之載入。

← Django筆記 - 目錄 Python教學投影片 →
 
comments powered by Disqus