抱歉,您的瀏覽器無法訪問本站
本頁面需要瀏覽器支持(啟用)JavaScript
了解詳情 >

這篇教學透過 Django 實作建立一個陽春部落格。透過 Django Framework 指令快速構建網站框架。
透過簡單的專案體驗 Django 的基本功能。

版本需求
Python 3.12
Django 4.2 LTS
Conda 24.5.0

1. 建立虛擬環境

第 1/3 步: 更新 conda 套件

1
conda update conda

小提示
當然也可使用 conda update --all 進行更新,相對時間會比較久。

第 2/3 步: 建立虛擬環境

使用以下指令建立虛擬環境 (Python 會自動使用 conda 預設版本)
Note: 當虛擬環境名稱重複時,將會刪除就舊的環境並重新建立

1
2
# conda create --name (虛擬環境名稱) python=(python 版本)
conda create --name django_env python=3.12

注意
當初現以下提示訊息時,表示您的 conda 尚未初始化 (conda init)

1
2
3
4
CommandNotFoundError: Your shell has not been properly configured to use 'conda activate'.
To initialize your shell, run

$ conda init <SHELL_NAME>

第 3/3 步: 啟用虛擬環境

啟用虛擬環境,之後下載 Python 套件都會存放於對應的虛擬環境之下,避免套件相依性的干擾問題。

1
2
# conda activate (虛擬環境名稱)
conda activate django_env

不確定虛擬環境名稱時,可以透過以下指令進行查詢:

1
conda env list

當成功進入虛擬環境時會顯示虛擬環境名稱在指令前綴,如:

1
(django_env) C:\Users\wycha>

小提示
離開虛擬環境的指令如下:

1
2
# conda deactivate
conda deactivate

檢查虛擬環境內的 Python 路徑位置:

1
where python

輸出狀況 (檢查預設 Python 路徑指向 ...\anaconda3\env\(虛擬環境)\python.exe):

1
2
3
(django_env) C:\Users\wycha>where python
C:\Users\wycha\anaconda3\envs\django_env\python.exe
C:\Users\wycha\AppData\Local\Microsoft\WindowsApps\python.exe

完成

2. 虛擬環境安裝 Django 套件

以下開始的指令需要進入虛擬環境執行 (conda activate (虛擬環境名稱))。

透過 pip 安裝套件:

1
2
# pip install (套件名稱)(版本條件運算子)(版本號碼)
pip install django==4.2

您需要知道
conda 本身也具有套件管理的功能,但是下載 packages 的來源地址由 anaconda 維護。
使用 conda 下載套件的版本資訊可以在 https://anaconda.org/conda-forge/repo 查詢,
pip 套件管理則由 PyPA 維護,套件資訊可在 https://pypi.org/ 查詢。

簡而言之,conda 環境打包出來的 packages 是適用於 conda 環境 (conda 可以建構非 python 語言環境或其他 shell scripts),pip 的 packages 適用於安裝套件於 python 環境,兩種套件管理器的出發目的不一樣,所以 packages 提供的版本也不盡相同。又 conda 可以建立 python 環境所以 pip 可以針對 conda 中 python 進行安裝。因此使用上 conda 與 pip 是不衝突的。

按照 django 狀況為例, conda 提供的版本為 4.1,pip 提供的版本為 4.2.*,因此採用 pip 進行套件安裝。

執行結果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(django_env) C:\Users\wycha>pip install django==4.2
Collecting django==4.2
Using cached Django-4.2-py3-none-any.whl.metadata (4.1 kB)
Collecting asgiref<4,>=3.6.0 (from django==4.2)
Using cached asgiref-3.8.1-py3-none-any.whl.metadata (9.3 kB)
Collecting sqlparse>=0.3.1 (from django==4.2)
Using cached sqlparse-0.5.0-py3-none-any.whl.metadata (3.9 kB)
Collecting tzdata (from django==4.2)
Using cached tzdata-2024.1-py2.py3-none-any.whl.metadata (1.4 kB)
Using cached Django-4.2-py3-none-any.whl (8.0 MB)
Using cached asgiref-3.8.1-py3-none-any.whl (23 kB)
Using cached sqlparse-0.5.0-py3-none-any.whl (43 kB)
Using cached tzdata-2024.1-py2.py3-none-any.whl (345 kB)
Installing collected packages: tzdata, sqlparse, asgiref, django
Successfully installed asgiref-3.8.1 django-4.2 sqlparse-0.5.0 tzdata-2024.1

3. django-admin 初始化專案

第 1/4 步: 把命令提示字元的目標移動到桌面

請根據自己的桌面路徑執行以下程式碼:

1
cd %HOMEDRIVE%%HOMEPATH%\Desktop

確保專案資料夾建立於桌面

第 2/4 步: 使用 django-admin 建立程式碼框架

1
2
# django-admin startproject (專案名稱)
django-admin startproject djangoBLOG

第 3/4 步: 使用 VSCode 開啟專案

1
2
# code (專案資料夾的路徑)
code djangoBLOG

第 4/4 步: 設定專案用的 .vscode/settings.json

方便後續開發流程,讓 VScode 開啟時可以自動載入虛擬環境。

  1. 在檔案總管的空白處新增資料夾 .vscode 並且在該資料夾內建立 settings.json

  2. 新增以下程式碼至 settings.json

    1
    2
    3
    4
    5
    { 
    "python.terminal.activateEnvironment": true,
    "python.defaultInterpreterPath": "~\\anaconda3\\envs\\django_env\\python.exe",
    "terminal.integrated.defaultProfile.windows": "Command Prompt",
    }
  3. Ctrl + Shift + P 輸入 Developer: Reload Window 按下 Enter 重新啟動 VSCode

4. 管理程式 manage.py 構建 app

Ctrl + ` 開啟 VSCode 內的終端機,先檢查終端機是否自動進入虛擬環境,

即終端機輸入開頭出現 (django_env)

接著輸入以下指令建立新的 web app:

1
2
# python manage.py startapp (APP 的名字)
python manage.py startapp blog_post

接著,啟動【測試】伺服器:

1
2
# python manage.py startapp (APP 的名字)
python manage.py runserver

小提示
在終端機介面按下 Ctrl + C 停止測試伺服器

開啟瀏覽器連接上 http://127.0.0.1:8000/ 將會看到 django 初始化面,如下圖:

Django Runserver

而按照指令所產生的資料夾結構如下:

djangoBLOG 資料夾結構
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
│  db.sqlite3
│ manage.py

├─.vscode
│ settings.json

├─blog_post
│ │ admin.py
│ │ apps.py
│ │ models.py
│ │ tests.py
│ │ views.py
│ │ __init__.py
│ │
│ └─migrations
│ __init__.py

└─djangoBLOG
asgi.py
settings.py
urls.py
wsgi.py
__init__.py

5. 修改 Django 框架的設定

djangoBLOG/settings.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# line:28
ALLOWED_HOSTS = ['*']

# line:106
LANGUAGE_CODE = "zh-hant"

# line:108
TIME_ZONE = "Asia/Taipei"

# line:40
INSTALLED_APPS = [
# ... (略) ...
'blog_post',
]
  • Line 28: ALLOWED_HOSTS 設定允許連線的來源
  • Line 106: LANGUAGE_CODE 網站的預設語言。
  • Line 108: TIME_ZONE 網站的時區
  • Line 40: INSTALLED_APPS 啟用的 APP 清單

    注意
    這邊的 INSTALLED_APPS 是一個 python list 的型態,因此每個元素需要加上逗點分隔。

6. 定義資料模型 Models

構建一個 CMS(Content Management System) 系統,最重要的部分就是將資訊內容儲存於資料庫裡,方便後續維戶與更新。而 Django 框架提供 ORM(Object Relational Manager) 方式將資料庫邏輯構建成物件導向式的程式碼型態,可以節省開發人員維護資料庫結構的時間。

  1. 接下來我們真的 APP 裡面 models.py 程式碼進行修改:
專案資料夾\blog_post\models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.db import models

class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.CharField(max_length=200)
content = models.TextField()
pub_date = models.DateTimeField(auto_now_add=True)

class Meta:
ordering = ['-pub_date']

def __str__(self):
return self.title

  1. 讓資料模型生效需要執行以下指令:

    1
    2
    python manage.py makemigrations
    python manage.py migrate
    執行結果
    執行結果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    (django_env) C:\Users\wycha\OneDrive\桌面\djangoBLOG>python manage.py makemigrations
    Migrations for 'blog_post':
    blog_post\migrations\0001_initial.py
    - Create model Post
    (django_env) C:\Users\wycha\OneDrive\桌面\djangoBLOG>python manage.py migrate
    Operations to perform:
    Apply all migrations: admin, auth, blog_post, contenttypes, sessions
    Running migrations:
    Applying contenttypes.0001_initial... OK
    Applying auth.0001_initial... OK
    Applying admin.0001_initial... OK
    Applying admin.0002_logentry_remove_auto_add... OK
    Applying admin.0003_logentry_add_action_flag_choices... OK
    Applying contenttypes.0002_remove_content_type_name... OK
    Applying auth.0002_alter_permission_name_max_length... OK
    Applying auth.0003_alter_user_email_max_length... OK
    Applying auth.0004_alter_user_username_opts... OK
    Applying auth.0005_alter_user_last_login_null... OK
    Applying auth.0006_require_contenttypes_0002... OK
    Applying auth.0007_alter_validators_add_error_messages... OK
    Applying auth.0008_alter_user_username_max_length... OK
    Applying auth.0009_alter_user_last_name_max_length... OK
    Applying auth.0010_alter_group_name_max_length... OK
    Applying auth.0011_update_proxy_permissions... OK
    Applying auth.0012_alter_user_first_name_max_length... OK
    Applying blog_post.0001_initial... OK
    Applying sessions.0001_initial... OK

7. 啟用管理介面並新增資料

Django 框架有提供後台管理介面給開發者使用,除了有使用者帳號模組以外,也包含資料庫管理模組於其中。啟用 admin 功能時需要建立超級使用者的使用者帳號密碼。

  1. 透過指令建立超級使用者

    1
    python manage.py createsuperuser

    按照提示訊息進行輸入:

    1
    2
    3
    4
    5
    6
    7
    使用者名稱 (leave blank to use 'wycha'): admin
    電子信箱: admin@django_example.com
    Error: 請輸入有效的電子郵件地址。
    電子信箱: admin@djangoexample.com
    Password:
    Password (again):
    Superuser created successfully.

    注意

    1. 使用者名稱為登入後台的帳號。
    2. 電子信箱可以使用假信箱作為替代,不過如果後須會使用到 email 發信功能時,請輸入正常使用中的 email 地址
    3. 終端機的系統互動介面 Password 欄位,基於安全性在輸入的時候不會有任何反應。
    4. 當然這些資料都可以在後台進行修改
  2. 將 APP 納入管理後台的程式中

    1
    2
    3
    4
    5
    6
    7
    from django.contrib import admin
    from blog_post.models import Post

    # Register your models here.

    admin.site.register(Post)

    • Line 2: 表示將會使用到 APP.models 中的 Post Class。
    • Line 6: 使用 django.contrib 中的 admin Class,並把託管的 model 以參數形式傳入。
  3. 進入後台並嘗試操作資料庫管理介面

    第 1/7 步: 啟動測試伺服器

    1
    python manage.py runserver

    第 2/7 步: 連上後台管理程式

    1. 打開瀏覽器
    2. 輸入 http://localhost:8000/admin

    第 3/7 步: 輸入帳號密碼並點選登入

    新增文章 1.

    第 4/7 步: 對著 APP Model 的 Posts 按下新增按鈕

    新增文章 2.

    第 5/7 步: 在新增 post 介面中按照欄位填入資訊並按下儲存

    新增文章 3.

    注意
    Slug 欄位是作為文章的網址的資料,此欄位資料不重複、限定英文小寫與減字號 (-)、底線 (_) 符號,其餘字元盡量不要使用,特別是中文。

    第 6/7 步: 按下新增 POST 按鈕繼續新增下一篇文章

    新增文章 4.

    第 7/7 步: 新增文章直到 Posts Table 裡免有 5 筆資料

    新增文章 5.

    完成: 接續下一步實作

  4. 讓 admin 介面中的顯示其他欄位於總表上 (修改 )

    專案資料夾/blog_post/admin.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    from django.contrib import admin
    from blog_post.models import Post

    # Register your models here.


    class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'slug', 'pub_date']


    admin.site.register(Post, PostAdmin) # 修改

    • Line 8: 新增 PostAdmin Class 繼承自 admin.ModelAdmin 並定義屬性 list_display
    • Line 11: 將 PostAdmin Class 作為第二參數傳入 admin.site.register 方法內

    重新整理 http://127.0.0.1:8000/admin/blog_post/post/ 畫面
    新增文章 5.

8. 修改首頁並讀取資料庫內的資料

這節次開始我們開始嘗試把資料庫的資料讀取並顯示於指定網址。

  1. 編輯 APP 的 views.py

    專案資料夾/blog_post/views.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from django.shortcuts import render
    from django.http import HttpResponse
    from blog_post.models import Post

    # Create your views here.


    def index(requests):
    posts = Post.objects.all()
    post_lists = list()
    for count, post in enumerate(posts):
    post_lists.append("#{}: {}<br><hr>".format(str(count), str(post)))
    return HttpResponse(post_lists)

    • views.py 為這個 APP 的視圖集合,裡面定義頁面呈顯方式,其中:
      • Line 3: 預先載入資料模型 Post Class。
      • Line 8: 定義視圖方法。作為視圖方法必須帶有 requests 參數。
      • Line 9: 使用 model 的查詢方法 Post.objects.all
      • Line 13: 使用 HttpResponse 將資料回傳
  2. 定義網址對應執行的視圖方法

    專案資料夾/DjangoBLOG/urls.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    from django.contrib import admin
    from django.urls import path
    from blog_post.views import index

    urlpatterns = [
    path('', index),
    path("admin/", admin.site.urls),
    ]

    • urls.py 這一支程式存在於 settings.py 同一層資料夾內,也表示一個專案裡面只有一個「根」 urls.py,當伺服器接收到請求時,會優先使用這個 urls.py 比對請求網址與對應的執行方法。
    • Line 3: 對應的視圖方法使用時要先載入
    • Line 6: 定義網址為 '' 時對應執行的方法。 '' 表示 base_url 後面沒有其他內容,即 http://localhost:8000/
    • Line 7: 定義網址為 admin/ 時對應執行的方法,即 http://localhost:8000/admin/。這邊為了維護方便採用嵌套路由作為對應執行方法,後續章節會詳細講解其原理。
  3. 當修改完上面兩個部份的程式碼後打開瀏覽器並連上 http://localhost:8000/ 畫面如下:
    編輯首頁 1.

  4. 顯示文章內容 ➞ 修改 blog_post/views.py::index()

    專案資料夾/blog_post/views.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    from django.shortcuts import render
    from django.http import HttpResponse
    from blog_post.models import Post

    # Create your views here.


    def index(requests):
    posts = Post.objects.all()
    post_lists = list()
    for count, post in enumerate(posts):
    post_lists.append("<h2>#{}: {} </h2><br><hr>".format(str(count), str(post))) # 修改
    post_lists.append("<small> {} </small><br><br>".format(str(post.content))) # 修改
    return HttpResponse(post_lists)

    • 修改 Line 12: 加入 <h2><hr> HTML 標籤讓標題格式化。
    • 修改 Line 13: 用 post.content 取得資料庫文章內容插入 <small> 標籤中
      編輯首頁 2.

9. 將頁面模板化

在上一個節次我們將資料渲染出來,但實務上頁面組成可能還會包含其他區塊,如:標題、橫幅、側欄、頁尾…等,如果全部把它放在 views.py 內會讓程式碼顯得雜亂,因此我可以運用 Django Views 與 Templates 架構進行程式碼拆分,讓 View 專注於資料儲存、邏輯操作, Templates 負責根據資料渲染畫面。

9.1. 在 Django 使用模板 (Template) 設定

第 1/2 步: 建立 templates 資料夾

1
mkdir templates

第 2/2 步: 修改 專案資料夾/DjangoBLOG/settings.pyTEMPLATES 變數

專案資料夾/DjangoBLOG/settings.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TEMPLATES = [ # 約 54 ~ 55 行
{
"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",
],
},
},
]

完成設定

9.2. 在指定 Templates 資料夾新增模板 index.html

專案資料夾/templates/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
<meta charset="utf-8">
<title> Django CMS</title>
</head>
<body>
<h1> Welcome to My Blog </h1>
<hr>
{{ posts }}
<hr>
<h3> 現在時間: {{now}} </h3>
</body>
</html>
  • Line 9 與 11 中的 {{}} 為渲染記號,Django 會填充對應的變數資料於指定的位置 (記號內為變數名稱)。

9.3. 修改 views.py 並指定使用的模板

專案資料夾/blog_post/views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
from django.shortcuts import render
from django.http import HttpResponse
from blog_post.models import Post
from datetime import datetime

# Create your views here.


def index(requests):
posts = Post.objects.all()
now = datetime.now()
return render(requests, "index.html", locals())

  • Line 4: 使用 datetime package 取的當前伺服器時間。
  • Line 11: 新增 now 變數儲存當前時間字串。
  • Line 12: 使用 render() 將資料與模板進行渲染。
    • render() 第一個參數為 requests 來自傳進方法的 requests
    • render() 第二個參數為使用的模板路徑 + 檔案名稱。
    • render() 第三個參數為模板渲染的字典型態的資料。
    • locals() 會將當前方法內之變數與值打包成字典。
渲染結果

編輯首頁 3.

9.4. QuerySet 渲染與排版調整

從 9.3 的渲染結果來看,<body>{{ posts }} 是以 QuerySet 的物件顯示在渲染標記處。而 QuerySet 是由 ORM 產生的查詢結果。因此我們需要在模板內使用特定的模板語言進一步處理 QuerySet。

專案資料夾/templates/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<html>
<head>
<meta charset="utf-8">
<title> Django CMS</title>
<style>
.post_title {
font-family: 微軟正黑體;
font-size: 16pt;
font-weight: bold;
}
.post_body {
font-family: 微軟正黑體;
font-size: 10pt;
letter-spacing: 1pt;
}
.post_date {
font-family: 微軟正黑體;
font-size: 10pt;
}
</style>
</head>
<body>
<h1> Welcome to My Blog </h1>
<hr>
{% for post in posts %}
<p class="post_title" >{{ post.title }}</p>
<p class="post_body" >{{ post.content }}</p>
<p class="post_date" >{{ post.pub_date }}</p>
{% endfor %}
<h3> 現在時間: {{now}} </h3>
</body>
</html>
  • Line 25 ~ 29: {% for %} ... {% endfor %} 是模板語言裡的迴圈,會重複「渲染」迴圈內的 html 標籤。

    • 模板內使用 {% ... %} 表示使用模板語法,語法使用與範例將會在後面教學文章進行示範。
  • Line 6 ~ 19: 為 CSS 語法,將 <p> 依照不同的 class 進行格式調整。

渲染結果

編輯首頁 4.

10. 精簡首頁內容並製作超連結

一般來說首頁通常不會顯示太多內容,而是會做成類似入口的形式,提供使用者按照自己所需瀏覽資訊。
因此我們可以只保留標題,並將其作為超連結。

10.1. 精簡首頁為超連結

專案資料夾/tempaltes/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<!-- ... (略) ... -->
<body>
<h1> Welcome to My Blog </h1>
<hr>
{% for post in posts %}
<a href="/post/{{post.slug}}">
<p class="post_title" >{{ post.title }}</p>
</a>
{% endfor %}
<h3> 現在時間: {{now}} </h3>
</body>
</html>
  • Line: 6 ~ 10: 將 <p class="post_title"> 以外的標籤移除,並且以 <a> 標籤包起來。
  • Line 7: 其中 <a>href 內使用 {{post.slug}} 作為超連結各文章的專屬位址。

10.2 新增 post.html 渲染單一文章內容

新增模板 post.html專案資料夾/templates/ 底下,內容如下:

專案資料夾/tempaltes/post.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<html>
<head>
<meta charset="utf-8">
<title> Django CMS</title>
<style>
.post_title {
font-family: 微軟正黑體;
font-size: 16pt;
font-weight: bold;
}
.post_body {
font-family: 微軟正黑體;
font-size: 10pt;
letter-spacing: 1pt;
}
.post_date {
font-family: 微軟正黑體;
font-size: 10pt;
}
</style>
</head>
<body>
<h1> {{post.title}} </h1>
<hr>
<p class="post_content" >{{ post.content }}</p>
<p class="post_date" >{{ post.pub_date }}</p>
<hr>
<h3><a href='/'> 回首頁 </a></h3>
</body>
</html>
  • 內容基本上與 index.html 差不多。
  • Line 22 ~ 28: 原本是用 {% for %} 將 QuerySet 渲染,這邊改寫成直接將資料渲染。

10.3. 修改 urls.pyslug 作為 url 的參數進行傳遞

定義超連結的路由資訊:

專案資料夾/DjangoBLOG/urls.py
1
2
3
4
5
6
7
8
9
from django.contrib import admin
from django.urls import path
from blog_post.views import index, showPost

urlpatterns = [
path("", index),
path("admin/", admin.site.urls),
path("post/<slug:slug>/", showPost),
]
  • Line 3: 加入新的 showPost 方法 (尚未實現因此可能會出現錯誤)。
  • Line 8: 定義新的連結 post/<slug:slug>/,其中 <slug:slug> 表示參數該字段是來自超連結的參數。
    • <slug:slug> 中的第一個 slug 是參數的型態。
    • <slug:slug> 中的第二個 slug 是參數的名稱。

10.4. 新增 showPost 方法於 blog_post\views.py

專案資料夾\blog_post\views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.shortcuts import render, redirect
from django.http import HttpResponse
from blog_post.models import Post
from datetime import datetime
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned

# Create your views here.
# ... (略) ...

def showPost(requests, slug):
try:
post = Post.objects.get(slug=slug)
except ObjectDoesNotExist:
return redirect('/')
except MultipleObjectsReturned:
return redirect('/')
return render(requests, "post.html", locals())

  • Line 1: 引入 django 內建的重新導向方法 redirect
  • Line 5: 引入 django 例外處理的 Class
    • ObjectDoesNotExist 當 model 查詢不存在的 keys 時會回傳此例外。
    • MultipleObjectsReturned 當 model 查詢的 keys 大於一筆時,回傳此例外。
  • Line 9 ~ 16: showPost 主方法
    • 其中以 try ... excepy... 語法實現。
    • 只要接收到錯誤時就將頁面轉址回首頁。
渲染結果

編輯首頁 5.

11. 共用模板

觀察 index.htmlpost.html 程式碼部分只有在 <body> 裡面有些微的差異,因此我們可以將重複性高的區塊獨立出來,進一步使用模板繼承的技巧,抽象化成單一文件方便後續維護與使用。

模板共用 1.

  • 以上圖為例子,index.html 中可以拆解成 header.htmlfooter.htmlnavbar.html … 等區塊。這些區塊用單一檔案進行撰寫與維護。
  • 模組與模組之前可以使用 include 將程式碼導入。
  • 當然也可以透過 include 方式組成 scaffolds (鷹架),如上圖的 base.html
  • 而內容差異不大的頁面則採用 extends 方式將資料填充到 scaffolds 形成一個新的頁面 page。

11.1. 在 templates 資料夾按照功能性建立資料夾

1
2
3
mkdir templates\components
mkdir templates\scaffolds
mkdir templates\pages
  • components 存放組件,如:header.htmlfooter.htmlnavbar.html
  • scaffolds 存放組合出來的架構,提供給 pages 繼承。
  • pages 則用來收納透繼承而產生的檔案,提供給 views 渲染模板。

11.2 在 components 資料夾內新增 header.css.html

post.htmlindex.html 的 CSS 區塊獨立成單一檔案。

專案資料夾\templates\components\header.css.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<style>
.post_title {
font-family: 微軟正黑體;
font-size: 16pt;
font-weight: bold;
}
.post_body {
font-family: 微軟正黑體;
font-size: 10pt;
letter-spacing: 1pt;
}
.post_date {
font-family: 微軟正黑體;
font-size: 10pt;
}
.blog-footer {
font-family: 微軟正黑體;
}
</style>
專案資料夾\templates\components\footer.html
1
2
3
4
5
{% if now %}
<p class="blog-footer">現在時刻:{{ now }}</p>
{% else %}
<p class="blog-footer">本文內容取自網路,如有侵權請來信通知下架...</p>
{% endif %}

11.4 在 scaffolds 資料夾內新增 base.html

專案資料夾\templates\components\base.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE>
<html>
<head>
<meta charset='utf-8'>
<title>
{% block title %} {% endblock %}
</title>
{% include 'components/header.css.html' %}
</head>
<body>
<div class="post_title"><h3>{% block headmessage %}{% endblock %}</h3></div>
<hr>
{% block content %}{% endblock %}
<hr>
{% include 'components/footer.html' %}
</body>
</html>
{% block js_script %}{% endblock %}
  • Line 6、11、13與18: {% block title %} ... {% endblock %} ,這邊使用到模板語言的 block 意思為,我們在 base.html 保留一個插槽讓,繼承的檔案進行區塊填充,而 block 模板語言的第二參數為區塊名稱。
  • Line 8、15: {% include 'components/footer.html' %} 使用 include 將指定路徑檔案的程式碼,複製並填充到 include 所在的區塊。

11.5 在 pages 資料夾內新增 index.htmlpost.html

專案資料夾\templates\pages\index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{% extends 'scaffolds/base.html' %}
{% block title %} Django CMS {% endblock %}

{% block headmessage %} Welcome to My Blog {% endblock %}

{% block content %}
{% for post in posts %}
<a href="/post/{{post.slug}}">
<p class="post_title" >{{ post.title }}</p>
</a>
{% endfor %}
{% endblock %}

{% block js_script %}{% endblock %}
專案資料夾\templates\pages\index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
{% extends 'scaffolds/base.html' %}
{% block title %} Django CMS {% endblock %}

{% block headmessage %} {{post.title}} {% endblock %}

{% block content %}
<p class="post_content" >{{ post.content }}</p>
<p class="post_date" >{{ post.pub_date }}</p>
<hr>
<h3><a href='/'> 回首頁 </a></h3>
{% endblock %}

{% block js_script %}{% endblock %}
  • 仔細比較兩個檔案與 base.html,不難看出 base.html 是用 {% block %} 預留區塊,而被繼承下來的 index.htmlpost.html 檔案組就就是針對預留區塊進行賦值。

11.6 修改 blog_post/views.pyindex()showPost()

專案資料夾\blog_post\views.py
1
2
3
4
5
6
7
8
9
def index(requests):
# ... (略) ...
return render(requests, "pages/index.html", locals())


def showPost(requests, slug):
# ... (略) ...
return render(requests, "pages/post.html", locals())

  • 修改 render() 中的模板位址,讓 Django 使用 templates/pages 資料夾內的模板進行渲染。

12. 靜態檔案處理

通常一個網站包含許多圖片內容豐富我們的版面,當然也會根據專案不同會設計自己的樣式如 css 檔案,甚至在應用程式有特定的 UI 操作流程需要撰寫 javascript 讓使用者可以在應用程式中進行互動。上述這些檔案實際上不會因為使用者不同,檔案內容而有所改變,而這些檔案就可以視為靜態檔案。

後續實作將會使用到一張圖片,請先自行準備,圖檔的用途作為 logo 使用。
Logo Maker: https://www.designevo.com/tw/logo-maker/

12.1. 設定 static 資料夾

1
2
3
4
5
6
mkdir static
mkdir static/images
mkdir static/scripts
mkdir static/styles
mkdir public
mkdir public/assets

12.2. 建立檔案用於測試靜態文件載入

  1. 將 logo 拖曳至 專案資料夾\static\images\ 底下並重新命名為 logo.jpg (附屬檔名按照影像的類型格式)。
  2. 建立 main.js專案資料夾\static\scripts\ 底下,檔案內容維持空白。
  3. 建立 main.css專案資料夾\static\styles\ 底下,檔案內容維持空白。
  4. 建立 .gitkeep專案資料夾\public\assets 底下,檔案內容維持空白。

12.3. 修改 settings.pystatic 路徑寫入設定檔中

專案資料夾\DjangoBLOG\settings.py
1
2
3
4
5
6
7
8
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / 'public/assets'
STATICFILES_DIRS = [
BASE_DIR / 'static'
]
  • STATIC_URL 表示靜態檔案的路網址。
  • STATIC_ROOT 為當專案部署為正式站的時候,對應的靜態檔案路徑。
  • STATICFILES_DIRS 為當前專案的靜態檔案存放位置。

STATIC_ROOT 實際上在開發環境(python manage.py runserver)時是沒有功能的,但是如果將專案託管於 Apache 或是 Nginx 伺服器時,會將靜態檔案的網址指向伺服器內某個資料夾路徑。伺服器在處理網址與路徑時根據資料安全的等級會限制使用者獲取特定資料夾內的資料,而靜態資料夾(public/*)存取權限相對寬鬆。
當部署到正式環境時,需要額外執行以下指令,彙整靜態檔案至指定資料夾內。

1
python manage.py collectstatic

12.4 改寫 base.html 嘗試使用靜態檔案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE>
<html>
<head>
<meta charset='utf-8'>
<title>
{% block title %} {% endblock %}
</title>
{% load static %}
{% include 'components/header.css.html' %}
<link href="{% static 'styles/main.css' %}" rel="stylesheet">
<script src="{% static 'scripts/main.js' %}"></script>
</head>
<body>
<img src="{% static "images/logo.png" %}">
<div class="post_title"><h3>{% block headmessage %}{% endblock %}</h3></div>
<hr>
{% block content %}{% endblock %}
<hr>
{% include 'components/footer.html' %}
</body>
</html>
{% block js_script %}{% endblock %}
  • Line 8: 使用 {% load static %} 模板語言宣告該頁面會使用到靜態檔案。
  • Line 10: 使用 main.css 靜態檔案。
  • Line 11: 使用 main.js 靜態檔案。
  • Line 14: 使用靜態檔案的圖片。
測試方式

靜態檔案 1.

對著瀏覽器 Crtl + F12 進入 DevTools (以 Chrome 為例),切換到 Network 頁籤並按下 F5 重新整理,觀察 main.jsmain.css 是否有載入(Status = 200),載入失敗的靜態檔案將會是以紅字 (Status = 404),而圖片則會直接顯示於頁面上。

13. 導入 Bootstrap 5 UI 框架

這一章節開始,將學習如何使用前端框架 Bootstrap 5 將版面進行美化。為了貼近實務這章節會開始大量編寫 template 模板,並適當的將區塊獨立成單一文件格式,即「切版」。接下來的實現過程較為繁瑣,再請注意檔案是否缺漏,否則會 Django 會拋出 templates not found 等異常狀態。

13.1. 下載 AdminLTE 4.0 Bootstrap5 框架

AdminLTE 4.0 是一套由 Bootstrap 5 為基礎的 UI 前端框架,在 https://adminlte.io 有完整頁面示範,提供我們快速開發網站 APP。該框架主要有兩個部分 cssjavascript,要使用該 AdminLTE 框架,我們需要將 AdminLTE 所提供的程式碼拖曳至靜態資料夾按照類型存放。

第 1/3 步: 進入 Gist 複製程式碼

第 2/3 步: 複製 adminlte.min.css

新增檔案 adminlte.min.css專案資料夾/static/styles/ 底下, 程式碼內容則將 Gist 的 adminlte.min.css 區塊內複製貼上。

第 3/3 步: 複製 adminlte.min.js

新增檔案 adminlte.min.js專案資料夾/static/scripts/ 底下, 程式碼內容則將 Gist 的 adminlte.min.js 區塊內複製貼上。

接續下個小節修改 components

13.2. 建立 components

專案資料夾/templates/components 分別建立以下檔案,程式碼請參考下方內容:

第 1/7 步: 建立 components/pages.script.lib.html

專案資料夾/templates/compoents/pages.script.lib.html
1
2
3
4
5
6
7
8
9
10
{% load static %}
<script src="https://unpkg.com/overlayscrollbars@2.3.0/browser/overlayscrollbars.browser.es6.min.js" integrity="sha256-H2VM7BKda+v2Z4+DRy69uknwxjyDRhszjXFhsL4gD3w=" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha256-whL0tQWoY1Ku1iskqPFvmZ+CHsvmRWx/PIoEvIeWh4I=" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap@5.3.2/dist/js/bootstrap.min.js" integrity="sha256-YMa+wAM6QkVyz999odX7lPRxkoYAan8suedu4k2Zur8=" crossorigin="anonymous"></script>
<script src="{% static "scripts/adminlte.min.js" %}">
<script src="https://unpkg.com/sortablejs@1.15.0/Sortable.min.js" integrity="sha256-ipiJrswvAR4VAx/th+6zWsdeYmVae0iJuiR+6OqHJHQ=" crossorigin="anonymous"></script>
<script src="https://unpkg.com/apexcharts@3.37.1/dist/apexcharts.min.js" integrity="sha256-+vh8GkaU7C9/wbSLIcwq82tQ2wTf44aOHA8HlBMwRI8=" crossorigin="anonymous"></script>

<script src="https://unpkg.com/jsvectormap@1.5.3/dist/js/jsvectormap.min.js" integrity="sha256-/t1nN2956BT869E6H4V1dnt0X5pAQHPytli+1nTZm2Y=" crossorigin="anonymous"></script>
<script src="https://unpkg.com/jsvectormap@1.5.3/dist/maps/world.js" integrity="sha256-XPpPaZlU8S/HWf7FZLAncLg2SAkP8ScUTII89x9D3lY=" crossorigin="anonymous"></script>

第 2/7 步: 建立 components/pages.style.lib.html

專案資料夾/templates/compoents/pages.style.lib.html
1
2
3
4
5
6
7
{% load static %}
<link rel="stylesheet" href="https://unpkg.com/@fontsource/source-sans-3@5.0.12/index.css" integrity="sha256-tXJfXfp6Ewt1ilPzLDtQnJV4hclT9XuaZUKyUvmyr+Q=" crossorigin="anonymous"><!--end::Fonts--><!--begin::Third Party Plugin(OverlayScrollbars)-->
<link rel="stylesheet" href="https://unpkg.com/overlayscrollbars@2.3.0/styles/overlayscrollbars.min.css" integrity="sha256-dSokZseQNT08wYEWiz5iLI8QPlKxG+TswNRD8k35cpg=" crossorigin="anonymous"><!--end::Third Party Plugin(OverlayScrollbars)--><!--begin::Third Party Plugin(Bootstrap Icons)-->
<link rel="stylesheet" href="https://unpkg.com/bootstrap-icons@1.11.0/font/bootstrap-icons.min.css" integrity="sha256-Qsx5lrStHZyR9REqhUF8iQt73X06c8LGIUPzpOhwRrI=" crossorigin="anonymous"><!--end::Third Party Plugin(Bootstrap Icons)--><!--begin::Required Plugin(AdminLTE)-->
<link rel="stylesheet" href="https://unpkg.com/apexcharts@3.37.1/dist/apexcharts.css" integrity="sha256-4MX+61mt9NVvvuPjUWdUdyfZfxSB1/Rf9WtqRHgG5S0=" crossorigin="anonymous"><!-- jsvectormap -->
<link rel="stylesheet" href="https://unpkg.com/jsvectormap@1.5.3/dist/css/jsvectormap.min.css" integrity="sha256-+uGLJmmTKOqBr+2E6KDYs/NRsHxSkONXFHUL0fy2O/4=" crossorigin="anonymous">
<link rel="stylesheet" href="{% static "styles/adminlte.min.css" %}">

第 3/7 步: 建立 components/sidebar.html

專案資料夾/templates/compoents/sidebar.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{% load static %}
<aside class="app-sidebar bg-body-secondary shadow" data-bs-theme="dark">
<!-- Logo 小橫幅 -->
<div class="sidebar-brand">
<a href="/" class="brand-link"> <!--begin::Brand Image-->
<img src="{% static "images/logo.png" %}" alt="Logo" class="brand-image opacity-75 shadow"> <!--end::Brand Image--> <!--begin::Brand Text-->
<span class="brand-text fw-light">我的記事本</span>
</a>
</div>

<!-- Menu -->
<div class="sidebar-wrapper">
<nav class="mt-2">
<ul class="nav sidebar-menu flex-column" data-lte-toggle="treeview" role="menu" data-accordion="false">
<li class="nav-item"> <a href="admin" class="nav-link"> <i class="nav-icon bi bi-speedometer"></i>
<p>管理後台</p>
</a> </li>
<li class="nav-item"> <a href="admin" class="nav-link"> <i class="nav-icon bi bi-speedometer"></i>
<p>功能新增中</p>
</a> </li>
</ul>
</nav>
</div>
</aside>

第 4/7 步: 建立 components/navbar.html

專案資料夾/templates/compoents/navbar.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<nav class="app-header navbar navbar-expand bg-body">
<div class="container-fluid">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-lte-toggle="sidebar" href="#" role="button">
<i class="bi bi-list"></i>
</a>
</li>
<li class="nav-item d-none d-md-block">
<a href="#" class="nav-link">Home</a>
</li>
<li class="nav-item d-none d-md-block">
<a href="#" class="nav-link">Contact</a>
</li>
</ul>
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" data-widget="navbar-search" href="#" role="button">
<i class="bi bi-search"></i>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-lte-toggle="fullscreen">
<i data-lte-icon="maximize" class="bi bi-arrows-fullscreen"></i>
<i
data-lte-icon="minimize"
class="bi bi-fullscreen-exit"
style="display: none"
></i>
</a>
</li>
<li class="nav-item dropdown user-menu">
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown">
<img
src="https://upload.wikimedia.org/wikipedia/commons/1/12/User_icon_2.svg"
class="user-image rounded-circle shadow"
alt="User Image"
/>
<span class="d-none d-md-inline">User</span>
</a>
<ul class="dropdown-menu dropdown-menu-lg dropdown-menu-end">
<li class="user-header text-bg-primary">
<img
src="https://upload.wikimedia.org/wikipedia/commons/1/12/User_icon_2.svg"
class="rounded-circle shadow"
alt="User Image"
/>
<p>
User
</p>
</li>
<li class="user-footer">
<a href="#" class="btn btn-default btn-flat">Profile</a>
<a href="#" class="btn btn-default btn-flat float-end">Sign out</a>
</li>
</ul>
</li>
</ul>
</div>
</nav>

第 5/7 步: 建立 components/page.header.html

專案資料夾/templates/compoents/page.header.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="app-content-header">
<div class="container-fluid">
<div class="row">
<div class="col-sm-6">
<h3 class="mb-0">{{ pageTitle|default:"首頁"}}</h3>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-end">
{% if hasPageTitle is True %}
<li class="breadcrumb-item"><a href="/">首頁</a></li>
<li class="breadcrumb-item active" aria-current="page">
{{ pageTitle|default:"首頁"}}
</li>
{% else %}
<li class="breadcrumb-item active">首頁</li>
{% endif %}

</ol>
</div>
</div>
</div>
</div>

第 6/7 步: 建立 components/article.card.html

專案資料夾/templates/compoents/article.card.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div class="row pb-3">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title display-1">{{ post.title }}</h3>
<div class="card-tools"> <button type="button" class="btn btn-tool" data-lte-toggle="card-collapse" title="Collapse"> <i data-lte-icon="expand" class="bi bi-plus-lg"></i> <i data-lte-icon="collapse" class="bi bi-dash-lg"></i> </button> <button type="button" class="btn btn-tool" data-lte-toggle="card-remove" title="Remove"> <i class="bi bi-x-lg"></i> </button> </div>
</div>
<div class="card-body">
{{ post.content }}
{% if showReadMore %}
<br>
<a href="post/{{post.slug}}"> 閱讀更多... </a>
{% endif %}
</div> <!-- /.card-body -->
<div class="card-footer"> 發表時間:{{ post.pub_date }}</div> <!-- /.card-footer-->
</div>
</div>
</div>

第 7/7 步: 修改 components/footer.html

專案資料夾/templates/compoents/footer.html
1
2
3
4
5
6
7
<footer class="app-footer">
<div class="float-end d-none d-sm-inline">Djagno CMS Demo</div><strong>
Copyright &copy; 2023-2024&nbsp;
<a href="/" class="text-decoration-none">My Notebook</a>.
</strong>
All rights reserved.
</footer>

接續下個小節修改 base.html 與繼承頁面

13.3. 修改 base.html 與其繼承頁面

一共三個頁面需要進行處理,分別是 base.html 以及繼承其模板的 index.htmlpost.html

第 1/3 步: 修改 base.html

專案資料夾/templates/scaffolds/base.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<html>
<head>
<meta charset='utf-8'>
<title>
{% block title %} {% endblock %}
</title>
{% load static %}
{% include 'components/header.css.html' %}
{% include 'components/pages.style.lib.html' %}
<link href="{% static 'styles/main.css' %}" rel="stylesheet">
<script src="{% static 'scripts/main.js' %}"></script>
</head>
<body class="layout-fixed sidebar-expand-lg bg-body-tertiary">
<div class="app-wrapper">
{% include 'components/navbar.html'%}
{% include 'components/sidebar.html'%}
<main class="app-main">
{% block content %}{% endblock %}
</main>

{% include 'components/footer.html' %}
</div>
</body>
</html>
{%include 'components/pages.script.lib.html' %}
<script>
const SELECTOR_SIDEBAR_WRAPPER = ".sidebar-wrapper";
const Default = {
scrollbarTheme: "os-theme-light",
scrollbarAutoHide: "leave",
scrollbarClickScroll: true,
};
document.addEventListener("DOMContentLoaded", function() {
const sidebarWrapper = document.querySelector(SELECTOR_SIDEBAR_WRAPPER);
if (
sidebarWrapper &&
typeof OverlayScrollbarsGlobal?.OverlayScrollbars !== "undefined"
) {
OverlayScrollbarsGlobal.OverlayScrollbars(sidebarWrapper, {
scrollbars: {
theme: Default.scrollbarTheme,
autoHide: Default.scrollbarAutoHide,
clickScroll: Default.scrollbarClickScroll,
},
});
}
});
</script>
{% block js_script %}{% endblock %}

第 2/3 步: 修改 index.html

專案資料夾/templates/pages/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
{% extends 'scaffolds/base.html' %}
{% block title %} Django CMS {% endblock %}

{% block content %}
{% include 'components/page.header.html'%}
<div class="app-content">
{% for post in posts %}
{% include "components/article.card.html" with post=post showReadMore=True %}
{% endfor %}
</div>
{% endblock %}

{% block js_script %}{% endblock %}

第 3/3 步: 修改 post.html

專案資料夾/templates/pages/post.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{% extends 'scaffolds/base.html' %}
{% with hasPageTitle=True pageTitle="FUCK" %} {% endwith %}
{% block title %} Django CMS {% endblock %}


{% block content %}
{% include 'components/page.header.html' with hasPageTitle=True pageTitle="貼文"%}
<div class="app-content">
{% include "components/article.card.html" with post=post %}
<h3><a href='/'> 回首頁 </a></h3>
</div>
{% endblock %}

{% block js_script %}{% endblock %}

13.4. 調整 AdminLTE 的 CSS

這邊採用 AdminLTE 的 Card 作為顯示文章的框架,但是 Card Title 的字體大小不夠明顯,我們可以修改 static/styles/main.css 達到我們想要的字體大小。

專案資料夾/static/styles/main.css
1
2
3
4
.card-title {
color: red;
font-size: 1.5rem!important;
}

13.5. 整體完成效果

套用 AdminLTE 後的版面

14. 過濾器 (filter)

從上一個章節的結果觀察,目前的版面在首頁會一次性把文章內容全部渲染,就如同之前章節所提,作為首頁會希望貼文僅顯示部分內容,盡可能再不捲動畫面的狀態下,顯示多筆貼文。
此時,我們可以使用模板的過濾器 (filter) 進行截斷。語法範例如下 :

1
{% data | [filter_command] %}

14.1. 使用 filter 截斷貼文中多餘的字元

修改 專案資料夾\templates\components\article.card.html 檔案中 <div class="card-body">標籤內容:

專案資料夾\templates\components\article.card.html
1
2
3
4
5
6
7
8
9
10
11
12
<!-- ... (略) ... -->
<div class="card-body">
{% if showReadMore %}
{{ post.content | truncatechars:100 }}
<br>
<a href="post/{{post.slug}}"> 閱讀更多... </a>
{% else %}
{{ post.content }}
{% endif %}
</div>
<div class="card-footer"> 發表時間:{{ post.pub_date }}</div>
<!-- ... (略) ... -->
  • 用模板語言 {% if %} 判斷參數 showReadMore 變數值。
  • {{ post.content | truncatechars:100 }} 在渲染資料處加上過濾器 truncatechars
  • truncatechars:100 過濾器為刪除資料第 100 字元之後的字串進行渲染。
  • 過濾器指令與參數用 : 區隔,但參數前面不得有空白字元存在。

專案資料夾\templates\pages\index.html

1
{% include "components/article.card.html" with post=post showReadMore=True %}

需要在 {% include "components/article.card.html" %} with 加上 showReadMore 變數與資料。同樣要注意變數與資料間不得有空白字元存在,但使用 = 將變數與值區隔開來。

套用 Filter 的渲染結果

Filter 前處理資料 01

14.2. Filter 的 linebreaks 功能

在先前新增貼文時,透過 Django 內建後台進行新增,如果資料有使用斷行處理時,資料會將斷行字元轉換成 \n 進行儲存,而將資料渲染時 HTML 會將 \n 以空白字元進行處理,畫面結果如下圖:
Filter 前處理資料 02

我可以過 filter 中的 linebreaks\n 轉換成 <br>,轉換後畫面如下:
Filter 前處理資料 03

修改 專案資料夾\templates\components\article.card.html:

專案資料夾\templates\components\article.card.html
1
2
3
4
5
6
7
8
9
10
11
12
<!-- ... (略) ... -->
<div class="card-body">
{% if showReadMore %}
{{ post.content | truncatechars:100 }}
<br>
<a href="post/{{post.slug}}"> 閱讀更多... </a>
{% else %}
{{ post.content | linebreaks }} <!-- 修改這處 -->
{% endif %}
</div>
<div class="card-footer"> 發表時間:{{ post.pub_date }}</div>
<!-- ... (略) ... -->
  • 程式碼僅需要在 {{ post.content }} 加上 linebreaks 過濾器。

14.3. 讓貼文內容可以顯示 HTML 語法

請先登入後台並新增一篇貼文,內容如下:

  • Title: Show HTML Image

  • Slug: dispaly-html-with-img

  • Content:

    1
    2
    3
    4
    <blockquote class="imgur-embed-pub" lang="en" data-id="a/oFFsavC" data-context="false">
    <a href="//imgur.com/a/oFFsavC"></a>
    </blockquote>
    <script async src="//s.imgur.com/min/embed.js" charset="utf-8"></script>

回到首頁並展開 Show HTML Image 貼文頁面中。

Show HTML Image 貼文

Filter 前處理資料 04

從渲染畫面結果顯示可以發現,程式碼片段在渲染過程中以字串的形式呈現,在安全性考量下 Django Template 引擎在渲染會先將資料以字串型態進行儲存,避免資料內嵌有惡意程式碼。因此如果要渲染帶有程式碼的資料時,需要加上過濾器 safe 表示這段資料的程式碼內容是安全的。

修改 專案資料夾\templates\components\article.card.html:

專案資料夾\templates\components\article.card.html
1
2
3
4
5
6
7
8
9
10
11
12
<!-- ... (略) ... -->
<div class="card-body">
{% if showReadMore %}
{{ post.content | truncatechars:100 }}
<br>
<a href="post/{{post.slug}}"> 閱讀更多... </a>
{% else %}
{{ post.content | safe | linebreaks }} <!-- 修改這處 -->
{% endif %}
</div>
<div class="card-footer"> 發表時間:{{ post.pub_date }}</div>
<!-- ... (略) ... -->
帶有 safe 過濾器的渲染結果

Filter 前處理資料 05

為何 Django 需要設計這個安全機制
例如在貼文內容加上這段程式碼後,只要展開貼文內容,就會將網站重新轉址至目標網站。

1
<script>document.location.href="https://keoinn.github.io";</script>

14.4. 自訂過濾器渲染 Markdown 語法

如同上個章節的步驟,請先登入後台並新增一篇貼文,內容如下:

Show Markdown Content 貼文

Filter 前處理資料 06

先前的步驟裡,貼文如果是 HTML 語法與 JavaScript 程式碼可以透過 safe 標籤允許 Django Templates 引擎渲染顯示,但是如果是特定語法的 Markdown 標籤時,就需要自定義渲染規則與標籤。實現步驟如下:

第 1/5 步: 進入虛擬環境安裝 markdown 套件

注意

  1. 記得進入虛擬環境執行程式碼,即命令提示字元前面需要出現 (django_env)
  2. 如果測試伺服器是正在運行的狀態,安裝套件錢記得先停止伺服器,使用 Ctrl + C 終止程式。
1
pip install markdown

第 2/5 步: 建立 libraries 資料夾

在專案資料夾底下透過以下指令建立資料夾,librariestemplates 在同一個層級。接著在 libraries 底下建立 filter 資料夾。

1
2
mkdir libraries
mkdir libraries/filter

第 3/5 步: 在 libraries\filter 底下建立 markdown_filter.py

專案資料夾\libraries\filter\markdown_filter.py
1
2
3
4
5
6
7
8
9
10
11
from django import template
from django.template.defaultfilters import stringfilter

import markdown as md

register = template.Library()

@register.filter()
@stringfilter
def markdown(value):
return md.markdown(value, extensions=['markdown.extensions.fenced_code'])

第 4/5 步: 修改 settings.py 加入自定義標籤

專案資料夾\djangoBLOG\settings.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
TEMPLATES = [ ## 約 55 行處
{
"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",
],
"libraries": { # 加入這幾行
"markdown_tag": "libraries.filter.markdown_filter", # 加入這幾行
} # 加入這幾行
},
},
]

第 5/5 步: 修改 article.card.html 為渲染記號加上 markdown 過濾器

專案資料夾\templates\components\article.card.html
1
2
3
4
5
6
7
8
9
10
11
12
<!-- ...(略)... -->
<div class="card-body">
{% if showReadMore %}
{{ post.content | truncatechars:100 }}
<br>
<a href="post/{{post.slug}}"> 閱讀更多... </a>
{% else %}
{% load markdown_tag %} <!-- 加入這行 -->
{{ post.content | markdown | safe | linebreaks }} <!-- 修改此處 -->
{% endif %}
</div>
<!-- ...(略)... -->

完成後結果

Filter 前處理資料 07

15. 專案部署準備

最後一個部分,再完成專案後要準備部署所使用的環境與程式碼。並撰寫簡單的部署指令,之後可以利用第三方工具實現自動化部署。

15.1. PIP Requirement List

將專案部署於伺服器上,需要設定與安裝執行環境,例如:我們這個專案執行於 Django 4.2 Package、Markdown 3.6 這些套件資訊可以在開發環境將套件資訊與版本號碼進行打包,在部署環境時可以透過打包資訊一次性安裝相依套件,而不再是一行一行指令進行安裝。因此我們可以使用 pip 指令進行打包並將資訊記錄於 requirments.txt

1
pip freeze > requirements.txt

您需要知道
同樣地在執行紀錄套件資訊與版本號碼階段,要進入 Python 虛擬環境,否則紀錄的套件版本資訊將會是當前環境的資訊,在部署階段可能會發生 module not found 等問題。
透過以下指令進行一次性安裝 (同樣需要注意當前的虛擬環境):

1
pip install -r requirements.txt
requirements.txt 套件資訊輸出結果參考
專案資料夾/requirements.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
asgiref==3.8.1
astroid==3.2.2
black==24.4.2
click==8.1.7
colorama==0.4.6
dill==0.3.8
Django==4.2
flake8==7.0.0
isort==5.13.2
Markdown==3.6
mccabe==0.7.0
mypy-extensions==1.0.0
packaging==24.0
pathspec==0.12.1
platformdirs==4.2.2
pycodestyle==2.11.1
pyflakes==3.2.0
setuptools==69.5.1
sqlparse==0.5.0
tomlkit==0.12.5
tzdata==2024.1
wheel==0.43.0

15.2. 編寫 wsgi.py 檔案

接下來將會把專案託管於 Apache 透過 WSGI (Web Server Gateway Interface) module 處理 Djagno Framework 的請求,而 WSGI 則會處理 Web Request 並與 Python Application 執行與通訊。

先決條件

  1. 伺服器租用 GCP VM。
  2. 用自己的 Google 帳號租用虛擬機 (用自己的帳號租用時,會在虛擬機自動建立名為自己帳號的資料夾作為個人資料夾)。
  3. 專案部署於個人資料夾底下。
  4. Python 虛擬環境名稱為 django_env,且 Python 版本為 3.12。
專案資料夾\djangoBLOG\wsgi.py
1
2
3
4
5
6
7
import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "DjangoBLOG.settings")

application = get_wsgi_application()

(GCP帳號) 請替換成自己的帳號即 Gmail 地址移除 (@gmail.com) 部分。

15.3. 上傳 db.sqlite3 至雲端服務

一般來說我們會把資料庫跟專案區分開來。實務上也會根據將資料庫獨立於專案之外,最常見的例子就是獨立建置資料庫伺服器,連線方式則使用 socket 進行連接。在這架構之下也容易實現資料鏡像備份,進行測試時只需替換資料庫連結地址,將正式站與測試站資料區隔。而教學文章中採用 sqlite3 作為資料庫的儲存軟體,又該資料庫是以檔案方式進行儲存,我們可以將資料庫的檔案上傳至雲端儲存空間進行備援。

1
python-upload@key-component-424608-b2.iam.gserviceaccount.com

15.4. 建立 Git 版控資訊並存儲至 Github

第 1/@@ 步: 建立 .gitignore

在專案資料夾下建立 .gitignore 讓暫存檔案與資料庫不進行版本追蹤。

.gitignore 程式碼
專案資料夾\.gitignore
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# Django #
*.log
*.pot
*.pyc
__pycache__
db.sqlite3
media

# Backup files #
*.bak

# If you are using PyCharm #
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# AWS User-specific
.idea/**/aws.xml

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# File-based project format
*.iws

# IntelliJ
out/

# JIRA plugin
atlassian-ide-plugin.xml

# Python #
*.py[cod]
*$py.class

# Distribution / packaging
.Python build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.whl
*.egg-info/
.installed.cfg
*.egg
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
.pytest_cache/
nosetests.xml
coverage.xml
*.cover
.hypothesis/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery
celerybeat-schedule.*

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# mkdocs documentation
/site

# mypy
.mypy_cache/

# Sublime Text #
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
*.sublime-workspace
*.sublime-project

# sftp configuration file
sftp-config.json

# Package control specific files Package
Control.last-run
Control.ca-list
Control.ca-bundle
Control.system-ca-bundle
GitHub.sublime-settings

# Visual Studio Code #
# .vscode/*
# !.vscode/settings.json
# !.vscode/tasks.json
# !.vscode/launch.json
# !.vscode/extensions.json
.history

# Virtual Envirment
django_env
django_env/*

# public static
public/assets/*
!public/assets/.gitkeep

留言