Django -- 從平凡到超凡

第 6 章    範本與靜態檔案

(1) 以 HTML 格式回覆資料

∗ 以長字串儲存 HTML 資料

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

▸ 可以直接將 HTML 寫在程式裡回覆給使用者,例如將 main/veiws.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
✶ 註:亦可利用 CLI 建立目錄:
$ cd <project>/main
$ mkdir templates templates/main

▸ 建立範本main.html

Right click folder main/templates/main --> New --> File --> File name: main.html --> Finish
內容如下:
<!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> }}, 變數名稱與左右大括號之間各有一空格
✶ 範本變數的值在 view 中設定後送至範本,範本引擎會以變數值取代變數:
範本變數
✶ 測試:
django讚

∗ 練習:在 bookstore 專案的首頁顯示現在時間

提示:

▸ 建立 main app,並寫好 URL mapping、 main() 函式、及 main.html

▸ 在 main() 函式裡匯入時間模組:import datetime

▸ 利用 now = datetime.datetime.now() 取得現在時間

▸ 將變數 now 以範本變數方式傳到範本

▸ 在範本中接收 now 範本變數並且在頁面顯示出來

∗ 摘要:新增 app 的步驟如下

▸ 右鍵點選專案 --> Django --> Create Application --> 輸入 app 名稱 --> OK

▸ 在專案的 settings.pyINSTALLED_APPS 中登記此 app

▸ 在專案的 urls.py 中設定 app 的 URL mapping (Namespace)

▸ 在 app 的 urls.py 中設定 URL mapping (Name)

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

▸ 建立範本

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

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

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

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

1. 在 main/urls.py 加入以下 URL 對應:
url(r'^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)

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

▸ 在 main/templates/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>
    ...

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

main/urls.py
...
urlpatterns = [
    url(r'^$', views.main, name='main'),
    url(r'^about/$', views.about, name='about'),
]
r'^about/$':URL 對應的格式為 main/about/^ 表示字串開頭, $ 表示字串結尾,亦即 about/ 後面不能再有任何字元

▸ 在 main/views.py 最後加入 about view, 回覆 about.html 網頁

...
def main(request):
    ...


def about(request):
    '''
    Render the about page
    '''
    return render(request, 'main/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 的範本引擎自動產生連結

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

首頁 --> 關於

(4) 範本標籤

範本標籤 (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 <itemList> %}
  ...
{% endfor %}

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

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

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

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

▸ 如果沒有 itemListitemList 為空, 則執行 {% empty %} 部份:

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

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

{% for <item> in <itemList> %}
   <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>' %}:匯入其他範本

∗ DRY原則

▸ 目前 main.htmlabout.html 都有以下相同的程式片段,違反DRY原則:

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

▸ 解決方案:

✶ 新增 main/menu.hmtl 新檔案,並將相同的程式片段移過來:
<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
  ...
  <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' %}
  ...
--> 測試:與原先結果一樣

(5) 靜態檔案

∗ 靜態檔案 (Static file)

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

▸ 因靜態檔案內容不變,處理靜態檔案的方式與處理動態網頁不同

▸ 通常將所有靜態檔案集中在某個目錄底下,方便靜態檔案伺服器存取

▸ 在 settings.py 裡有 STATIC_URL = '/static/' 指令, 亦即在系統部署時,將所有靜態檔案都複製並集中放在 /static/ 目錄底下:

static/
   main/
      css/
      img/
   article/
      css/
      img/
   ...

▸ 另外一種靜態檔案是在系統上線後由使用者上載的檔案,通常稱為「媒體檔案」(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 樣式

▸ 影像與 CSS 檔案都屬「靜態檔案」(內容不會變),Django 將所有靜態檔案都置於 app 底下的 static/ 目錄

▸ 建立 main/static/main/static/main 兩個目錄,在此目錄中再建立以下目錄:

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

▸ 註:靜態檔案目錄名稱必須是 static,此為 Django 預設,不可使用其他名稱

main.css 內容如下:

html, body {
  margin: 0;
  height: 100%;
}

body {
  padding: 20px;
  background: url(../img/background.jpg) fixed;
  background-repeat: repeat;
}

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

ul#menu li {
  display: inline-block;
}
html, body: 無邊距,高度與瀏覽器同高
body: 內填充 20px,背景影像涵蓋全部,不重複、xy 方向都至中、位置固定 (不隨捲軸移動)
ul:設定下邊距,文字靠右
li:設定行內區塊顯示,讓連結並列一行

▸ 在 main/templates/main/main.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">

▸ 註:加入 {% static ... %} 後可能需要重新啟動伺服器

▸ 在 about.html 範本亦加入 {% load ... %}<link ...> 資料

▸ 測試結果:所有頁面均顯示背景圖片並依樣式顯示

連結首頁     連結關於

(6) 發表文章功能

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

▸ Right click project --> Django --> Create Application --> Name: article --> OK

▸ 在 blog/settings.py 裡登記

INSTALLED_APPS = [
    ...
    'django.contrib.staticfiles',
    'article',
    'main',    
]

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

urlpatterns = [
    ...
    url(r'^admin/', include(admin.site.urls)),
    url(r'^article/', include('article.urls', namespace='article')),
    url(r'^main/', include('main.urls', namespace='main')),    
    url(r'^.*', include('main.urls')),
]
✶ URL 格式若為 article/..., 則匯入 article.urls 進一步做 URL mapping
注意url(r'.*', ...) 必須置於最後一行,否則 HTTP request 會由 main 處理

▸ 建立 article/urls.py 檔案,內容如下:

from django.conf.urls import url
from article import views


urlpatterns = [
    url(r'^$', views.article, name='article'),
]

▸ 編輯 article/views.py

from django.shortcuts import render


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

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

<!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>

▸ 在 menu.html 加入「部落格」連結:

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

▸ 測試:localhost:8000/article/

連結關於

(7) 小結

∗ 上推專案到 Github

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

∗ 練習

▸ 在 bookstore 專案中:

✶ 亦將原先回覆 Hello World! 字串方式改為回覆範本方式,範本首頁內容為「歡迎光臨本書店」
✶ 建立有兩個連結的 menu.html,一個是「首頁」,另一個是「聯絡我們」(Contact), ,並連結到有書局內容、聯絡方式資訊的頁面
✶ 加入動態資料:在首頁判斷
# 如果現在時間是上午 (中午 12:00 以前),首頁顯示「早安」
# 如果現在是下午 (中午 12:00 以後),首頁顯示「午安」
✶ 利用 CSS 設定第一行文字「歡迎光臨本書店」的字體大小、顏色、陰影,背景圖片等
✶ 在頁面適當地方插入自選的書本影像 (<img src="..." alt="...">)

上一章       下一章