2020年11月19日 星期四

Python 學習筆記 : Django 3 測試 (六) : 應用程式

距離年初學習 Django 已近一年, Django 也從 2 版躍升到 3 版, 最近因為要所借的書被預約了必須在還書前看完, 所以暫時重回 Django 學習軌道, 希望這次能一舉搞定 Django. 以下測試改用最新版的 Django 3, 其實新版最大的亮點是支援非同步, 整體架構並無改變, 只有專案設定檔的絕對路徑用法改變而已. 

本系列之前的筆記參考 : 


Django 2 官方文件參考 :

在之前的測試中, 我們使用 django-admin startproject mysite 指令自動建立了一個名為 mysite 的 Django 網站, 其基本結構如下 :

mysite (上層專案目錄)

     |____ manage.py
     |____ mysite  (下層案目錄)
                   |_____ __init__.py
                   |_____ settings.py
                   |_____ urls.py
                   |_____ wsgi.py

接著在上層專案目錄下添加模版目錄 (templates) 與靜態檔案目錄 (static), 這個 templates 目錄就是 Django MTV 架構中的 T (Template) :

(venv) D:\django\venv\mysite>mkdir templates 
(venv) D:\django\venv\mysite>mkdir static   
(venv) D:\django\venv\mysite>cd static   
(venv) D:\django\venv\mysite\static>mkdir images    
(venv) D:\django\venv\mysite\static>mkdir css     
(venv) D:\django\venv\mysite\static>mkdir js     

然後在下層專案目錄下添加處理呼叫與回應處理程式 views.py (對應 Django MTV 架構中的 V), 最後修改路由器程式 urls.py 即可讓網頁應用程式網站運作起來, 完整之檔案結構如下 : 

mysite (上層專案目錄)
     |____ manage.py
     |____ templates                 (模版目錄)
     |____ static                        (靜態檔案)
     |             |____ css              (存放 CSS 樣式檔)
     |             |____ js                (存放 Javascript 檔)
     |             |____ images        (存放圖檔)
     |____ mysite                       (下層專案目錄)
                   |____ __init__.py
                   |____ settings.py
                   |____ urls.py         (專案路由器)
                   |____ wsgi.py
                   |____ views.py      (HTTP 請求處理器)

但是直接將網頁應用程式建在專案目錄下的結構不符合軟體工程的要求, 因為由 urls.py 與 views.py 組成的服務控制邏輯與專案緊密結合, 使得應用程式無法重複使用於其他專案. 完整的 Django 網站架構是由是由上層的專案 (Project) 與下層的多個應用程式 (App) 組成, 專案負責設定, App 負責特定之服務邏輯, 且與抽象化的 ORM 資料庫存取機制結合以建立動態網站. 

App 是 Django 專案中具有獨立功能的可插拔元件, 亦即可將網頁專案的服務邏輯按照功能切割為數個 App 以符合軟體工程中元件可重用 (reusable) 之要求, 每一個 App 可以在最小程度的調整後移植到其他專案中使用. 例如一個購物網站的功能可以由會員登入, 商品列表, 購物車以及結帳等 App 組成. 

建立一個 App 可在第一層專案目錄下輸入如下指令 : 

python manage.py startapp <AppName>

例如 :

(venv) D:\django\venv\mysite>python manage.py startapp helloworld
(venv) D:\django\venv\mysite>dir  
 D:\django\venv\mysite 的目錄

2020/11/18  下午 04:47    <DIR>          .
2020/11/18  下午 04:47    <DIR>          ..
2020/11/14  下午 10:55                 0 db.sqlite3
2020/11/16  下午 03:25    <DIR>          helloworld
2020/11/14  下午 08:52               684 manage.py
2020/11/14  下午 10:55    <DIR>          mysite
2020/11/18  下午 04:48    <DIR>          static
2020/11/18  下午 04:47    <DIR>          templates

注意, App 與 static 與 templates 是第一層專案目錄下平行的子目錄. 此外, 建立 App 會在第一層專案目錄下建立一個 db.sqlite3 檔案與一個 helloworld 目錄, 前者為 Django 預設使用的單一檔案關聯式資料庫; 後者則是 App 的目錄, 其底下檔按結構為 : 




可見建立 App 時會自動在該 App 目錄自動產生 views.py 程式檔, 而以前直接在專案下建立應用程式時則須自行在第二層專案目錄 mysite 底下手動建立 views.py, 使用 App 架構的話就不需要了. App 目錄底下的檔案與子目錄之用途如下 :


 App 檔案目錄 說明
 migrations 紀錄與資料庫版本與資料模型變更相關之檔案 (用來同步模型與資料庫)
 __init__.py 空檔案, 使 App 目錄形式上符合 Python 套件要求
 admin.py 用來註冊資料模型以便能於管理網頁中管理資料表
 apps.py 儲存與此 App 相關之設定
 models.py 儲存 App 資料模型定義以及資料間之關係
 test.py 儲存測試程式碼
 views.py 為 urls.py 處理 HTTP 要求與回應之程式檔


其中有需要動手去修改的只有 admin.py, models.py, 以及 views.py 這三個檔案. 加入 App 後完整的 Django 網站之檔案結構如下 : 

mysite                                      (上層專案目錄)
     |____ manage.py                (管理程式)
     |____ templates                  (模版目錄)
     |____ static                         (靜態檔案)
     |             |____ css               (存放 CSS 樣式檔)
     |             |____ js                 (存放 Javascript 檔)
     |             |____ images         (存放圖檔)
     |____ mysite                        (下層專案目錄)
     |             |____ __init__.py   (形成套件)
     |             |____ settings.py    (專案設定檔)
     |             |____ urls.py           (專案路由器)
     |             |____ wsgi.py         (伺服器佈署設定檔)
     |____ myapp                         (應用程式目錄)
                   |____ migrations     (資料庫同步)
                   |____ __init__.py    (形成套件)
                   |____ admin.py       (註冊資料模型)
                   |____ apps.py         (App 設定檔)
                   |____ models.py     (資料模型定義)
                   |____ test.py           (測試用程式)
                   |____ views.py        (HTTP 請求處理器)

注意, 此結構是所有 App 都使用專案路由器來將 URL 映射到請求處理器, 

建立 App 後須先如下設定與程式修改才能正常運行 :


1. 修改 settings.py 設定檔 :   

建立 App 後須先修改第二層專案目錄下的 settings.py (專案設定檔), 需要改的地方基本上有四個 :
  • INSTALLED_APPS : 註冊所建立之 App 名稱
  • LANGUAGE_CODE : 將預設英文 en-us 改為繁體中文的 zh-Hant
  • TIME_ZONE : 將預設 UTC 改為 Asia/Taipei
  • TEMPLATES : 指定網頁模板路徑
INSTALLED_APPS 是一個串列變數, 其中 django.contrib 開頭的是 Django 內建的應用程式, 自訂的 App 名稱就直接加到最後面即可 : 

# Application definition
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'helloworld',
]

在 INSTALLED_APPS 中註冊 helloworld 這個 App 後, Django 才會載入這個應用程式. 

其次是修改 LANGUAGE_CODE 與 TIME_ZONE 變數, 修改如下 : 

LANGUAGE_CODE = 'zh-Hant'
TIME_ZONE = 'Asia/Taipei'

修改模版變數 TEMPLATES, 把原先空白的 'DIR' 屬性設為 templates 子目錄之絕對路徑 :

'DIRS': [BASE_DIR / 'templates')],

最後在 STATIC_URL 變數底下添加一行指定靜態目錄之絕對路徑 : 

STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static'),]   

注意, 在 Django 2.x 的 settings.py 中使用 os.path.dirname() 來取得網站根目錄絕對位址, 然後用 os.path.join() 組合出檔案之絕對路徑, 但在 Django 3 已經不需要這麼做了, 直接用 BASE_DIR 串接即可, 參考 :


最後將 settings.py 存檔即完成設定.  


2. 修改路由器 urls.py : 

第二層專案目錄下的 urls.py 負責處理 HTTP 請求的路由 (routing), 將來自客戶端的 URL 請求指配給 App 目錄下的 views.py 處理並做出回應. 建立專案後預設的 urls.py 內容如下 :

from django.contrib import admin
from django.urls import path

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

這個預設的 urls.py 已經幫我們匯入兩個內建 App : 後台管理模組 admin 以及路由指派模組 django.urls 的 path(), 後台管理程式的 URL 在本機為 127.0.0.1/admin, 對應到 path() 的第一參數 'admin/', 它被指派給 admin.site.urls 這個內建 App 處理. 

對於我們新建的應用程式 helloworld 來說, 只要在串列變數 urlpatterns 中為此 App 添加路由指派即可, 例如 :

from django.contrib import admin
from django.urls import path
from helloworld.views import helloworld

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

首先從 helloworld 目錄下的 views.py 匯入其 helloworld() 函數, 然後將 URL 'helloworld/' 指派給 helloworld() 函數去處理 (即回應此請求), 如果在本機要請求此 App 服務需使用 127.0.0.1:8000/helloworld 這個網址. 


3. 修改請求處理程式 views.py : 

開啟 App 目錄底下的 views.py 檔, 其預設內容如下 : 

from django.shortcuts import render

# Create your views here.

可見它已預先從內建的捷徑模組 django.shortcuts 匯入渲染方法 render(), 但此 render() 方法是用來渲染模版的, 如果只是要回應 Hello World 字串, 其實只要用 django.http.HttpResponse() 方法即可, 因此可將 views.py 修改為 : 

#views.py
from django.shortcuts import render
from django.http import HttpResponse

def helloworld(request): 
    return HttpResponse('<b>Hello World! <i>您好</i></b>')

此處定義一個呼叫 HttpResponse() 方法並傳回其結果的函數 helloworld(), 此函數即專案目錄下的路由器程式 urls.py 中 URL='helloworld' 所對應之處理函數, 當收到這個 URL 請求時就由 helloworld 函數回應. 

這樣就完成 App 的設定了, 將 views.py 以 utf-8 編碼存檔 (因為用到中文) 存檔後用 python manage.py runserver 指令運行測試伺服器, 這時會顯示有 18 個 migration (資料模型變更) 沒有執行, 由於這個簡單的 App 沒有用到資料庫, 所以還不執行變更也沒關係 :

(venv) D:\django\venv\mysite>python manage.py runserver    
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
November 18, 2020 - 23:52:23
Django version 3.1.3, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

在瀏覽器網址列輸入 127.0.0.1:8000/helloworld 即可看到網頁輸出結果 :



以上是在 App 架構下使用 django.http.HttpResponse() 方法輸出網頁的基本步驟, 測試專案壓縮檔存放於 GitHub :




不過當網頁內容複雜時用 HttpResponse() 輸出網頁會顯得很雜亂, 實務上 Django 網站使用捷徑方法 django.shortcuts.render() 搭配模版來輸出網頁, 例如可將上面的應用程式 helloworld 輸出的網頁改成如下可傳遞參數的模版網頁 hello.htm :

<!-- hello.htm -->
<!DOCTYPE html>
<html>
<head>
  <title>Hello</title>
  <meta charset="utf-8">
</head>
<body>
  <p class="msg">Hello, {{ name }}, 現在的時間是 {{ now }}</p>
</body>
</html>

將此模版檔案以 utf-8 編碼存檔於第一層專案目錄下的 templates 子目錄下, 然後修改路由器程式 urls.py, 添加 hello 這個附帶參數 name 的 URL :

#urls.py
from django.contrib import admin
from django.urls import path
from helloworld.views import helloworld, hello

urlpatterns = [
    path('admin/', admin.site.urls),
    path('helloworld/', helloworld),
    path('hello/<name>/', hello),
]

然後修改請求處理程式 views.py, 添加 hello 函式, 利用 django.shortcuts.render() 方法將名字 name 與現在時間 now 兩個參數以字典物件傳給模版網頁 hello.htm, 模版引擎會將參數插入網頁中 :

#views.py
from django.shortcuts import render
from django.http import HttpResponse
from datetime import datetime

def helloworld(request): 
    return HttpResponse('<b>Hello World! <i>您好</i></b>')
    
def hello(request, name):
    now=datetime.now()
    dict1={'name': name, 'now': now}
    return render(request, 'hello.htm', dict1)

然後在網址列輸入 127.0.0.1/hello/trump 結果如下 :



修改後的專案壓縮檔存放於 GitHub :


上面的測試雖然把 views.py 放在 App 目錄下, 但路由器卻統一使用專案路由器 (即第二層專案目錄下的 urls.py), 亦即專案中的每個 App 之路由指派全部都集合在專案路由器內, 如果一個專案包含了 App 很多, 則專案路由器就會顯得很雜亂, 而且削弱了 App 的獨立性. 

為了進一步讓 App 更具有可插拔性, 較好的做法是將各 App 的路由指派放在各自的 App 目錄下, 然後在專案路由器中用 django.urls.include() 將其含括進來, 這樣專案路由器內容就很清爽, 專案內有哪些 App 在此檔案中也一目了然, 當 App 要移植到別的專案中使用時也不需要到原始的專案路由器中複製貼上, 只要將 App 目錄複製過去, 然後於新專案的專案路由器中 include 進來即可, 架構如下 : 




可知此架構下會有兩層路由器檔案 urls.py, 即專案路由器與 App 路由器, 前者位於第二層專案目錄下, 後者則位於各 App 之目錄下, 但這個 App 路由器 urls.py 在建立 App 時並不會自動建立, 必須手動從專案路由器 urls.py 複製一份到 App 目錄下修改. 

其次, 對於 URL 的路徑也要加入 App 這一層, 路徑分配如下所示 :

Protocol://Host address/AppName/AppURL/param1/param2/param3...

協定 (Protocol) 為 http 或 https, 主機位址是 IP (例如 127.0.0.1) 或域名 (localhost, www.abc.com 等), 接下來是 App 名稱, App 位址 (在 App 的 urls.py 中映射到 views.py 中的函數), 最後跟著的是可能的參數. 

完整的 URL 會由左至右被一層一層剝掉 (striping), 當 HTTP 伺服器收到請求時, 首先協定與位址會先被伺服器剝除, 剩下 AppName/AppURL/param1/param2/param3... 被送到 Django 的專案路由器處理, 它會在取出 AppName 後將剩下的路徑送給該 App 的路由器, 取出的 AppURL (圖中的 url1) 會被映射到 views.py 中的函數 fun1 處理 (同時將剩下的參數傳入該函數) :




以修改上面的測試 2 為例, 第一步先根據上面的新架構修改 App 的請求處理器 views.y 如下 :

#views.py of App "helloworld"
from django.shortcuts import render
from django.http import HttpResponse
from datetime import datetime

def hello1(request): 
    return HttpResponse('<b>Hello World! <i>您好</i></b>')
    
def hello2(request, name):
    now=datetime.now()
    dict1={'name': name, 'now': now}
    return render(request, 'hello.htm', dict1)

事實上這與上面測試 2 內容一樣, 只是為了讓 URL 路徑更清楚而修改了函數名稱, 原來的 hellowworld() 改為 hello1(); 而 hello() 則改為 hello2() 而已. 

第二步是將第二層專案目錄 mysite 底下的專案路由器程式 urls.py 複製到 App 目錄 helloworld 下, 去除 admin (這屬於專案, App 不需要) 只保留 path 模組即可, 然後定義 App 名稱變數 app_name, 最後在 path() 函數中將 AppURL 映射到 views.py 中的處理函數就完成了, 修改後內容如下 :

#App router : urls.py
from django.urls import path
from helloworld import views

app_name="helloworld"

urlpatterns = [
    path('hello1/', views.hello1),
    path('hello2/<name>/', views.hello2),
]

注意, 此處匯入請求處理器的方式與上面測試 2 不同, 這裡是匯入整個 views 模組而非模組內的函數, 因此映射到函數時前面必須帶上模組名稱. 

最後第三步是修改專案路由器 urls.py, 用 include 把 App 路由器含括進來 :

#Project router: urls.py
from django.contrib import admin
from django.urls import path
from django.urls import include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('helloworld/', include('helloworld.urls')),
]

這樣就全部完成了, 完整的檔案結構如下 :

mysite                                      (上層專案目錄)
     |____ manage.py                (管理程式)
     |____ templates                  (模版目錄)
     |____ static                         (靜態檔案)
     |             |____ css               (存放 CSS 樣式檔)
     |             |____ js                 (存放 Javascript 檔)
     |             |____ images         (存放圖檔)
     |____ mysite                        (下層專案目錄)
     |             |____ __init__.py   (形成套件)
     |             |____ settings.py    (專案設定檔)
     |             |____ urls.py           (專案路由器)
     |             |____ wsgi.py         (伺服器佈署設定檔)
     |____ helloworld                  (應用程式目錄)
                   |____ migrations     (資料庫同步)
                   |____ __init__.py    (形成套件)
                   |____ admin.py       (註冊資料模型)
                   |____ apps.py         (App 設定檔)
                   |____ models.py     (資料模型定義)
                   |____ test.py           (測試用程式)
                   |____ urls.py           (應用程式路由器)
                   |____ views.py        (HTTP 請求處理與回應)


於瀏覽器輸入如下網址 127.0.0.1:8000/helloworld/hello1 結果如下 :



輸入網址 127.0.0.1:8000/helloworld/hello2/trump 結果如下 :



修改後的專案壓縮檔可從 GitHub 下載 :


綜合上述測試總結如下 :
  • Django 3 除了新增非同步功能外, 基本用法與第二版並無差異, 只有在專案設定檔 settings.py 的絕對路徑指派方式不同而已. 
  • 使用 App 在架構上只是將 views.py 從第二層專案目錄移到 App 目錄下, 以及增加資料同步動作而已. 
  • 路由器 urls.py 分離為專案路由器與 App 路由器有助於提升 App 可重用性.
如果是沒有用到資料庫的小型專案, 直接把應用程式寫在專案目錄下 (即不用 App, 控制邏輯 urls.py 與 views.py 都在第二層專案目錄下) 是最簡單的實作方式; 如果要用到資料庫, 則必須使用 App 架構, 功能不複雜只有少數 App 可以直接使用專案路由器; 但若 App 很多且考慮 App 可移植需求的話, 每一個 App 的路由器最好獨立出來, 與專案路由器分開. 

由於篇幅太長, 資料庫存取部分留到下一篇測試. 

沒有留言 :