Django -- 從平凡到超凡

第 6 章    範本與靜態檔

(1) 以 HTML 格式回覆資料

∗ 以長字串儲存 HTML 資料

▸ 目前的系統僅顯示 Hello World! 字串,一般而言, 網站所回覆的資料很多,因此應該回覆一個完整的 HTML 網頁

▸ 可以直接將 HTML 寫在程式裡回覆給使用者,例如將 main() 函式內容刪除,並改為以下內容:

main/views.py
from django.http import HttpResponse


def main(request):
    '''
    Render the main page
    '''
    html = '''
    <!doctype html>
    <html>
    <head>
    <title>部落格</title>
    <meta charset="utf-8">
    </head>
    <body>
    <p>這是 HTML 版的 Hello world!</p>
    </body>
    </html>
    '''
    return HttpResponse(html)
利用 Python 的長字串 (由三個引號包住) 來儲存 HTML 資料

▸ 測試 localhost:8000/

htmlHelloWorld
✶ 檢視原始檔可見完整的 HTML 網頁資料:
<!doctype html>
<html>
<head>
<title>部落格</title>
<meta charset="utf-8">
</head>
<body>
<p>這是 HTML 版的 Hello world!</p>
</body>
</html>

▸ 但以上寫法並不好,將程式與資料混雜違反 Views 應該和 Templates 分開的原則 (MTV), 而且利用長字串來儲存網頁太不方便了

▸ 解決方案:使用 HTML 檔案

(2) 使用範本系統

∗ 回覆資料最佳方法是使用 HTML 檔案

▸ Django 稱 HTML 檔案為「範本」 (Template,或稱為「模板」),亦即使用固定樣式的範本, 再加上資料的變動,即可產生動態文件 (Dynamic document)

▸ Django 的範本系統 (Template system)

✶ 範本裡包含 HTML 碼,也可以有範本變數 (Template variable) 或範本語言 (Django template language, DTL), 均由 Django 的範本引擎 (Template engine) 來處理
✶ 透過範本變數及範本語言可以產生動態網頁
範本變數的值可以改變,範本語言執行結果也可以改變 HTML 的內容

▸ Django 範本目錄的結構:範本目錄 templates 置於 app 目錄之下, 在範本目錄底下還需要再建立 <app> 目錄, 然後該 app 所屬的範本都儲存在 <templates>/<app> 目錄底下:

<app>/
   templates/
      <app>/
         <template1>.html
         <template2>.html
         ...

▸ 以本專案為例,main app 的範本目錄結構應該如下:

main/
   templates/
      main/
         main.html

▸ 範本目錄名稱必須是 templates,此為 Django 預設,不可使用其他名稱

▸ 為何在 templates/ 目錄下還要有一個 main 目錄?

✶ 在範本檔案前面冠上 app 名稱是個好方式,因為 render() 函式回覆 HTTP 請求時,就可以使用 main/main.html 的格式, 清楚的顯示 main.html 是屬於 main app
✶ 要移植整份範本時,目錄架構仍能保持,程式不需變動結構
HTML 文件類型

HTML 文件有以下幾種類型:

∗ 以範本呈現頁面資料:修改 view 程式

main/views.py

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


def main(request):
    '''
    Render the main page
    '''
    context = {'like':'Django 很棒'}
    return render(request, 'main/main.html', context)

▸ 改用 render 函式,不再使用 HttpResponse

▸ 範本變數的功能是用來將變動資料從 views 程式送到範本 (亦即網頁),一個範本變數由變數名稱與變數值配對組成, 格式為 '<varName>':<varValue>

▸ Django 使用 Python 的字典資料結構 (Dictionary,以大括號包住) 來儲存多個範本變數, 此範本字典的名稱習慣上使用 context,但也可使用其他變數名稱

▸ Python 字典是由「鍵」(Key,即變數名稱) 與「值」(Value,即變數值) 的配對組成,以上述 views 程式為例, Key'like'Value'Django 很棒'

因此 context 範本字典即為 {'like':'Django 很棒'}

▸ 最後的 render() 函式將 main/main.html 裡所有的範本變數置換成為其值,產生最後的 HTML 結果,然後回覆結果網頁給使用者

∗ 建立範本

▸ 在 main app 目錄下建立 templatestemplates/main 目錄:

✶ 在 Eclipse 建立目錄:
Right click app main New Folder Folder name: templates Finish
Right click folder main/templates New Folder Folder name: main Finish

▸ 建立範本main.html

Right click folder main/templates/main New File File name: main.html Finish
內容如下:
main/templates/main/main.html
<!doctype html>
<html>
<head>
<title>部落格</title>
<meta charset="utf-8">
</head>
<body>
<h2>Django 說 -- Hello world!</h2>
<p>{{ like }}</p>
</body>
</html>
✶ Django 的範本變數 (Template variable) 在範本中以兩個大括號包含:
{{ <variable> }}, 變數名稱與左右大括號之間各有一空格,以本例而言即為 {{ like }}
✶ 範本變數的值在 view 中設定後送至範本,範本引擎會以變數值取代變數:
範本變數
✶ 測試:
django讚

(3) 範本標籤

範本標籤 (Template tag) 是在範本中由 {%%} 包住的 範本指令{% 之後與 %} 之前必須要有空格

▸ 範本變數 (Template variable) 是在範本中由 {{}} 包住的變數,{{ 之後與 }} 之前也要有空格

▸ 如果範本變數在範本標籤裡面,則不需要大括號

∗ if 標籤

{% if <condition> %}
  ...
{% endif %}

{% if <condition> %}
  ...
{% else %}
  ...
{% endif %}

{% if <condition> %}
  ...
{% elif <condition> %}
  ...
{% else %}
  ...
{% endif %}

▸ 關係運算子:>, >=, <, <=, ==, !=

▸ 邏輯運算子:and, or, not

▸ 所有運算子前後都要有空格,例如:

{% if course == 'Python' and numStudents >= 30 %}

∗ for 標籤

{% for <item> in <items> %}
  ...
{% endfor %}
<items> 是一個序列,每個 for 迴圈依序取用序列裡的元素並指派給迴圈變數 <item>

▸ 執行 10 次 (以 10 個字元的字串當作迴圈的依據):

{% for i in '1234567890' %}
  ...
{% endfor %}

▸ 執行 100 次 (因數值較大,需從 view 程式建立串列再傳到範本):

view:
render(..., ..., {'range100':range(100)})
template:
{% for i in range100 %}
  ...
{% endfor %}

▸ 如果沒有 items 範本變數或 items 為空序列, 則執行 {% empty %} 部份:

{% for <item> in <items> %}
    ...
{% empty %}
    ...
{% endfor %}

▸ 每個迴圈循環使用 {% cycle %} 內的項目 (<tr> 元素每個迴圈輪流設定類別,亦即單數圈 class='odd', 雙數圈 class='even'):

{% for <item> in <items> %}
   <tr class="{% cycle 'odd' 'even' %}">
        ...
{% endfor %}

for 迴圈內可使用的變數

forloop.counter0:迴圈計數,從 0 開始
forloop.counter:迴圈計數,從 1 開始
forloop.first:如果是第一圈,就是 True
forloop.last:如果是最後一圈,就是 True
forloop.parentloop:巢狀迴圈的外圈計數

∗ 其他範本標籤

{% block <blockName> %}{% endblock %}: 區塊 (其內容可置換的區塊)

{% url '<urlNamespace>:<urlName>' %}: 利用 urls.py 裡的具名 URL 轉成實際 URL

{% load staticfiles %}:載入靜態檔案

{% static '<filePath>' %}: 利用 settings.py 裡的 STATIC_URL 值來設定檔案的路徑

{% extends '<parentTemplate>' %}:繼承範本

{% include '<otherTemplate>' %}:匯入其他範本

(4) 加入「關於」連結:說明網站內容

∗ 從一個網頁連結到另一個網頁:思考與規劃

▸ HTML 連結其他網頁的標籤格式:<a href=...>...</a> (a: anchor,定錨)

▸ 假設將在 main app 中加入一個 about.html (「關於」) 範本,用來介紹網站,預計步驟如下:

1. 在 main/urls.py 加入以下 URL 對應:
path('about/', views.about, name='about'),
2. 在 main.html 裡建立連到「關於」網頁的連結:
<a href="/main/about/">關於</a>

▸ 但以上方式有個缺點:未來如有需要更改 URL 格式時就需要改 2 處:main/urls.pymain.html

違反 DRY 原則:Don't Repeat Yourself.

▸ 解決方案:使用 Djang 的範本標籤 url 與具名 URL 對應格式

<a href="{% url '<urlNamespace>:<urlName>' %}">
✶ 亦即,以具名 URL 來取代實際網址:{% url 'main:about' %}
✶ Django 會先到 blog/urls.py 裡尋找 namespacemain 的項目 ( 找到 main/),然後再到 main/urls.py 裡尋找 nameabout 的項目 ( 找到 about/),最後將兩者組成 main/about/, 結果:<a href="/main/about/">

▸ 程式設計經典諺語:

The road to hell is paved with hard-coded paths.
「通往地獄之路是由寫死的程式所鋪設而成」(引述自 Tango with Django)

∗ 連結主網頁與「關於」網頁:實作

▸ 最後加入 about() 函式,回覆 about.html 網頁

main/views.py
...
def main(request):
    ...


def about(request):
    '''
    Render the about page
    '''
    return render(request, 'main/about.html')

▸ 加上前往「關於」網頁的 URL 對應:利用 Namespace 與 URL name 結合,讓 Django 的範本引擎自動產生 Request 格式

main/urls.py
...
urlpatterns = [
    path('', views.main, name='main'),
    path('about/', views.about, name='about'),
]
'about/':URL 對應的格式為 main/about/

▸ 建立「首頁」與「關於」兩個連結:

main/templates/main/main.html
...
<body>
<ul id="menu">
  <li><a href="{% url 'main:main' %}">首頁</a></li>
  <li><a href="{% url 'main:about' %}">關於</a></li>
</ul>
<h2>Django 說 -- Hello world!</h2>
  ...
✶ 在 ul 標籤中加入 id="menu",以便之後設定 CSS 樣式

▸ 建立 about.html 新檔案,內容如下:

main/templates/main/about.html
<!doctype html>
<html>
<head>
<title>部落格</title>
<meta charset="utf-8">
</head>
<body>
<ul id="menu">
  <li><a href="{% url 'main:main' %}">首頁</a></li>
  <li><a href="{% url 'main:about' %}">關於</a></li>
</ul>
<h2>關於部落格</h2>
<p>歡迎來到我的部落格,您可盡情瀏覽並留言。</p>
</body>
</html>
✶ 「首頁」與「關於」的連結:同樣利用 Namespace 與 URL name 結合,讓 Django 的範本引擎自動產生連結

▸ 測試:點擊「首頁」及「關於」連結均可到達正確頁面

首頁
關於

∗ 三振法則

▸ 相同的程式碼如果在不同地方發生兩次,還可接受,如果有三個地方都有相同 (或極類似) 的程式碼, 那就必須重構 (Refactor),讓程式看來更簡潔、效能更好

三振法則: 若有三個地方有相同程式碼就要重構 (Three strikes and you refactor),就像棒球,三振就要出局

▸ 目前 main.htmlabout.html 都有相同的導航按鈕程式片段,雖然尚未三振,但如果覺得很 *刺眼* 的話,那就重構吧 ;-)

▸ 新增 menu.html 新檔案,並將相同的程式片段移過來:

main/templates/main/menu.html
<ul id="menu">
  <li><a href="{% url 'main:main' %}">首頁</a></li>
  <li><a href="{% url 'main:about' %}">關於</a></li>
</ul>

▸ 在 main.htmlabout.html: 兩檔案中再利用 {% include %} 範本標籤匯入 menu.html

main/templates/main/main.html
...
<body>
<ul id="menu">
  <li><a href="{% url 'main:main' %}">首頁</a></li>
  <li><a href="{% url 'main:about' %}">關於</a></li>
</ul>
{% include 'main/menu.html' %}
<h2>Django 說 -- Hello world!</h2>
...
main/templates/main/about.html
...
<body>
<ul id="menu">
  <li><a href="{% url 'main:main' %}">首頁</a></li>
  <li><a href="{% url 'main:about' %}">關於</a></li>
</ul>
{% include 'main/menu.html' %}
<h2>關於部落格</h2>
...
測試:與原先結果一樣

(5) 靜態檔案

∗ 靜態檔案 (Static file)

▸ 內容不會變動的檔案,例如影像、音訊、視訊、JavaScript、靜態 HTML、CSS 等,此類檔案隨著專案部署到伺服器

▸ 因靜態檔案內容不變,處理靜態檔案的方式與處理動態網頁不同,而且,通常將所有靜態檔案集中在某個目錄底下,方便靜態檔案伺服器存取

▸ 在 settings.py 裡有 STATIC_URL = '/static/' 設定, 此為指定瀏覽器請求靜態檔案的 url 格式,例如:/static/main/css/main.css/ 即為請求伺服器送出 main app 底下的 static/main/css/main.css 檔案

▸ 另外一種靜態檔案是在系統上線後由使用者上載的檔案,通常稱為「媒體檔案」(Media file)

∗ 網站伺服器架構

▸ 傳統架構:一部伺服器,處理所有請求,包括送出靜態檔案與存取資料庫資料

1個伺服器

▸ 較先進的架構:

✶ 2 種伺服器 (可在同一部電腦或不同電腦)
2個伺服器
# 反向代理伺服器 (Reverse-proxy server,例如 Nginx),功能如下:
- 如果是靜態檔案請求:直接從靜態檔案目錄 (static/media/) 送出檔案,例如: <img src=...>, <link ...>, <script ...>
- 如果是其他請求:轉送請求 (Forward request) 給應用程式伺服器 (Application server)
- 具備負載平衡功能 (Load balancing):在此架構下,通常會同時啟動多部應用程式伺服器,以增加效能,例如某部伺服器正在處理緩慢的 I/O 作業時, 新來的請求就可以轉給其他伺服器
# 應用程式伺服器 (Application server,例如:Gunicorn)
- 負責執行 Web 應用程式
- 可以同時開啟許多伺服器,提昇請求服務效能
✶ 3 種伺服器 (最佳組合):反向代理伺服器 + 應用程式伺服器 + 雲端檔案服務:
3個伺服器
# 除了部署到伺服器的靜態檔案外,其餘檔案都置於雲端儲存服務供應商處,進一步降低伺服器負擔
# 雲端儲存服務 (Cloud storage service),例如:
- Amazon S3
<img src="https://s3-ap-southeast-1.amazonaws.com/<bucketName>/<dirName>/img.png">
- Google Cloud Storage
<img src="https://storage.googleapis.com/<bucketName>/<dirName>/img.png">

∗ 在 main.html 網頁加入背景影像及 CSS 樣式

▸ 因為在 settings.py 裡已設定 STATIC_URL = '/static/', 因此在 app 底下建立 static/ 目錄:建立 main/static/main/static/main 兩個目錄,在此目錄中再建立以下目錄:

css:在此目錄裡新增 main.css 檔案
img:將以下的 background.png 背景影像檔案置於此目錄裡
background.jpg
因此目錄及檔案結構如下:
main/
   static/
      main/
         css/
            main.css
         img/
            background.png

main.css 內容如下:

main/static/main/css/main.css
html, body {
  margin: 0;
  height: 100%;
}

body {
  padding: 20px;
  background: url("/static/main/img/background.png") fixed;
  background-repeat: repeat;
}

ul#menu {
  margin-bottom: 30px;
  text-align: right;
}

ul#menu li {
  display: inline-block;
}
html, body: 無邊距,高度與瀏覽器同高
body: 內填充 20px,固定式背景影像 (不隨捲軸移動),並且重複以貼滿整個頁面
ul:設定下邊距,文字靠右
li:設定行內區塊顯示,讓連結並列一行

▸ 在 main.htmlabout.html 兩範本中均加入以下資料:

main/templates/main/main.html
main/templates/main/about.html
<!doctype html>
{% load staticfiles %}
<html>
<head>
...
<meta charset="utf-8">
<link rel="stylesheet" href="{% static 'main/css/main.css' %}">
</head>
...
✶ HTTP 協定要求 <!doctype html> 必須在第一行, 因此 {% load staticfiles %} 放在第 2 行,讓 Django 載入靜態檔案
✶ Django 會利用 settings.py 中的 STATIC_URL 設定的值, 將 {% static 'main/css/main.css' %} 轉成 /static/main/css/main.css,因此結果標籤會是:
<link rel="stylesheet" href="/static/main/css/main.css">

▸ 註:瀏覽器會將靜態檔案儲存於快取 (Cache) 記憶體中,因此不更新檔案,使用者需要強迫瀏覽器清空快取記憶體,新的資料才會呈現,清空快取指令:

✶ Ubuntu: ctrl-F5
✶ Windows: ctrl-F5
✶ Mac: Command-Shift-R

▸ 測試結果:所有頁面均顯示背景圖片並依樣式顯示 (:有可能需要重啟伺服器才能更新靜態檔案)

連結首頁
連結關於

(6) 發表文章功能

∗ 新增 article app 以處理發表文章相關功能

▸ Right click project Django Create Application Name: article OK

▸ 在設定檔裡登記

blog/settings.py
INSTALLED_APPS = [
    ...
    'django.contrib.staticfiles',
    'article',
    'main',    
]

▸ 加入函式:

article/views.py
from django.shortcuts import render


def article(request):
    '''
    Render the article page
    '''
    return render(request, 'article/article.html')

▸ 建立 URL 對應 (類似 main/urls.py):

article/urls.py
from django.urls import path
from article import views


app_name = 'article'
urlpatterns = [
    path('', views.article, name='article'),
]

▸ 編輯 blog/urls.py 檔案,加上 1 行:

blog/urls.py
...

urlpatterns = [
    path('admin/', admin.site.urls),
    path('article/', include('article.urls', namespace='article')),
    path('main/', include('main.urls', namespace='main')),
    re_path('.*', views.main),
]
✶ URL 格式若為 article/..., 則匯入 article.urls 進行第二階段對應
注意re_path('.*', ...) 永遠都是放在最後一行, 以接受所有不符 URL 對應格式的請求

▸ 建立範本,內容如下(亦包含載入靜態檔案及 CSS 樣式檔連結):

article/templates/article/article.html
<!doctype html>
{% load staticfiles %}
<html>
<head>
<title>部落格</title>
<meta charset="utf-8">
<link rel="stylesheet" href="{% static 'main/css/main.css' %}">
</head>
<body>
{% include 'main/menu.html' %}
<h2>部落格說 -- Hello world!</h2> 
</body>
</html>

▸ 在導航中加入「部落格」連結:

main/templates/main/menu.html
...
  <li><a href="{% url 'main:about' %}">關於</a></li>
  <li><a href="{% url 'article:article' %}">部落格</a></li>
...

▸ 測試:localhost:8000/article/

連結關於

∗ 小結:新增 app 的步驟如下

▸ 右鍵點選專案 Django Create Application

▸ 在專案的 settings.pyINSTALLED_APPS 中登記

▸ 在 app 的 views.py 中建立 views 函式,處理使用者的 HTTP request

▸ 在 app 的 urls.py 中設定 URL mapping 與 name;如果是新的 App,就要在專案的 urls.py 中設定 URL mapping 與 Namespace

▸ 建立相關範本

∗ 上推專案到 Github

Right click project Team Commit Commit message: : Chapter 6 finished Commit and Push

▸ 練習