2019年2月23日 星期六

Python 學習筆記 : Django 2 測試 (一) : 請求與回應處理

Django 是以 Python 語言所設計, 用來架設中大型網站的框架, 源自 2003 年於美國堪薩斯州 Lawrence 新聞集團旗下報紙 Journal-World 服務的 Adrian Holovaty 與 Simon Willison, 用來管理該集團的新聞網站. 兩位開發者於 2005 年將 Django 以 BSD 授權開放原始碼, 並以比利時吉他手 Django Reinhardt 之名命名, 並於 2008 年成立 Django 軟體基金會來管理開發工作, 目前已有至少 1000 名貢獻者, 同時擁有超過 3000 個專為 Django 設計的套件.

Django 創建的目的是讓資料庫驅動的網站開發變簡單, 它不僅可與傳統關聯式資料庫一起運作, 也能與 NoSQL 資料庫如 MongoDB 等一起運作. Django 採用了一套設計原則讓開發者能快速打造出一個資料庫驅動的網站系統, 可說是最有生產力的架站框架. 其主要精神是 :
  1. 元件可重用 (DRY 原則-Don't Repeat Yourself )
  2. 堅持明確性 (Being explicit)
  3. 弱耦合結構 (Loosely coupled structure)
  4. 敏捷開發 (Agile)
明確性是指 Django 中不論處理請求或將運算結果回應給模版都使用明確的模式, 這樣對於學習者或維護者而言成本都最低; 反觀採用隱含模式 (implicit) 的 Ruby, 雖然程式碼看起來簡短, 但對整個架構還不十分了解的初學者來說學習曲線較陡; 而且很多系統互動都是在背後自動進行, 使得追蹤錯誤較為困難, 雖然隱含模式開發速度更快, 但可能耗費更多時間在維護上.

Django 的 MTV 架構是一種弱耦合架構, 各部件之間沒有很強的相依性, HTML 模版, 商業邏輯方法, 以及資料庫模型是分拆開來的部件.

參考 :

https://zh.wikipedia.org/wiki/Django

Django 使用 MTV (相對於 MVC) 架構將視圖 (Template) 與資料庫模型 (Model) 以及控制邏輯分割開來, 因此前端網頁設計師與後端程式設計師便可真正分工. 其次, 通訊協定細節被封裝起來, 資料庫也透過模型化與抽象化使存取更方便. 總之, 只要熟悉 django 結構便能快速建構一個專業的網站.

MVC 是一種軟體架構的理念, 並非一種技術, 其架構源自挪威奧斯陸大學教授 Trygve Reenskaug 於 1979 年在全錄 Xerox 實驗室做訪問研究時為 GUI 軟體設計所提出的設計模式, 最初用在程式語言 Smalltalk 的軟體架構上以實現動態的程式設計, 目的是可簡化軟體未來之維護與擴充以及重複利用程式碼並使專案開發更有效率. 參考 :

https://zh.wikipedia.org/wiki/MVC
什麼是MVC?What's Model-View-Controller?

在網頁設計中, MVC 架構如下圖所示 :


視圖負責產出使用者介面, 模型負責資料庫介接, 而控制器負責實作商業邏輯. 在網站系統中, 視圖用來生成網頁代碼以便讓瀏覽器渲染成像. Django 的設計基本上遵循 MVC 的功能分工精神, 不過, 在 Django 中 MVC 被稱為 MTV 架構, 兩者的對應如下 :


 MVC 架構 MTV 架構 說明
 M=Model (模型) M=Model (模型) 負責資料存取
 V=View (視圖) T=Template (模版)  負責資料呈現
 C=Controller (控制器) V=View (視圖) 負責訊息處理


這裡容易讓人搞混的是, Django 的 View (視圖) 是控制器功能 (具體而言即 views.py 的函數), 而資料呈現是由模版負責 (具體而言即 template 資料夾下的 HTML 檔案), 模型部分則意義相同. 總之, MVC 與 MTV 裡面的 V 指的是完全不同的功能. MTV 架構如下圖所示 :




Model 指的是抽象化的資料庫模型, 將資料庫操作抽象化的好處是不需要直接去操作各種資料庫, 而是以統一的方式操作模型. Template 是呈現給使用者的視覺畫面, 具體而言基本上就是 HTML 與 CSS 組成, 尚待控制器填空的 html 檔, 這部分被抽離出來有利於前端網頁設計師獨立作業. View 負責控制與整合, 即操作模型, 取出資料經運算後填入 Template 後回應給客戶端. 注意, 視圖也是可以不經過模版 Template 直接透過伺服器輸出網頁, 但這樣比較麻煩且變化性不大.

本篇測試參考了下面幾本書中的範例加以改寫 :
  1. It's django - 用 Python 迅速打造 Web 應用 (楊孟穎, 袁克倫, 碁峰)
  2. Python 新手使用 django 架站的 16 堂課 (何敏煌, 博碩)
  3. Python 新手使用 django 架站技術實作 (何敏煌, 林亮昀, 博碩)
  4. Python 網頁程式交易 App 實作 (林萍珍, 博碩)
  5. Beginning Django- (Daniel Rubio, Apress)
  6. 一次搞定所有 Python Web 框架開發百科全書 (佳魁, 劉長龍) 
  7. 科班出身的MVC網頁開發 : 使用Python + Django (佳魁, 王友釗)
以上書籍除了 3 外都是針對 1.x 版 (3 是 2 的第二版, 納入 django 2.0), Django 2 的中文書籍較少, 英文書可參考下列網站 :

Best Django Books (2019)

另外下列網站也是學習 Django 2.x 的好教材 :

Django 2 By Example 全书翻译、踩坑及教程 (電子書)
iT 幫鐵人賽 : From Django 1.11 to Django 2.1 系列

本篇則使用最新版的 Django 2 進行測試, Django 2.x 版不再支援 Python 2, 最低必須是 Python 3.4+ 版, 安裝前要先用 python --version 指令查詢電腦內所安裝的 Python 版本 :

C:\Users\User>python --version 
Python 3.7.2 

Django 與所支援與搭配的 Python 版本如下表 :


 Django 版本 搭配之 Python 版本
 1.11 2.7, 3.4, 3.5, 3.6
 2.0 3.4, 3.5, 3.6
 2.1, 2.2 3.5, 3.6, 3.7


Django 2 與 1.x 版有許多用法差異, 有部分用法不相容, 若在 2.x 版上沿用舊法可能導致錯誤, 參考 :

What is the difference between Django 1.11 and Django 2?

其中最主要的差異是 urls.py 中指派路由的寫法, 在 Django 1.x 使用正規表達式來解析網址; 而 Django 2.0 以後改用路由字串來解析, 但仍保留正規表達式用法, 但解析 URL 的函數已經改為 re_path() 而非 url() 了, 參考 :


Django 使用 URLconf 模組負責解析網址, 它會在路由程式  urls.py 中的  urlpatterns 串列變數裡搜尋網址所對應之 views.py 處理函數. 當我們以 django-admin startproject mysite 建立一個 mysite 專案後, 在第二層專案目錄下的設定檔 settings.py 中就會被設定為 mysites.urls, 表示路由指派程式預設為 urls.py :

ROOT_URLCONF = 'mysite.urls'

在 Django 1.x 版的 urls.py 中需從 django.conf.urls 模組匯入 url() 函數, 然後利用呼叫 url() 並且使用正規表示法來規範 URL 的格式來建立 urlpatterns 串列, 例如 :

from django.conf.urls import url
from mysite imort views
urlpatterns = [
url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive), ]

但在 2.x 版則改為呼叫新增的 django.urls.path() 方法且使用比較簡化的 URL 描述方式, 因此要從 django.urls 模組匯入 path() 函數 :

from django.urls import path
from mysite imort views
urlpatterns = [
path('articles/<int:year>/', views.year_archive), ]

Django 2.x 版的網址解析寫法使用尖括號來取得 URL 中的參數, 這種用法比較寬鬆, 且包含了資料類型自動轉換, 不需要再呼叫 int() 或 str() 等函數 (如果沒有指定類型預設為字串), 比較直觀易用.

Django 2.x 保留了正規表達式路由解析法, 但它是另外提供 django.urls.re_path() 函數來支援傳統的正規表達式用法, 參考 :

URL dispatcher
django2笔记:路由path语法

路由解析程式 urls.py 可同時使用新舊版解析方式, 這時要同時匯入 re_path() 與 path() 兩個函數 :

from django.urls import path
from django.urls import re_path
from mysite imort views

urlpatterns = [
  re_path(r'^$', views.home),
  path('articles/<int:year>/', views.year_archive),
  ]

注意, 使用正規表達式解析首頁 (即 "/") 時要用 r'^$' 而非 r'^/$', 在開頭 ^ 與結尾 $ 符號中間多一個斜線 / 反而會出現錯誤.

另外一個語法上的改變是使用者認證函數 user.is_authenticated() 與 user.is_anonomous() 改為 user.is_authenticated 與 user.is_anonomous 屬性, 參考 :

https://www.youtube.com/watch?v=rMJDPFox-XI

Django 2 官方教學文件參考 :

https://docs.djangoproject.com/en/2.1/

Python 學習筆記索引參考 :

Python 學習筆記索引


以下從建立一個預設專案網站, 其次簡單輸出 Hello World, 最後使用 templates 生成網頁逐步測試 Django 的基本用法 :


一. 建立一個預設專案網站 : 

1. 安裝 django 套件 :

使用 Django 架站首先用 pip 安裝 django 套件, 直接用 pip install django 會安裝最新版, 我安裝時最新是 2.1.7 版 :

D:\Python\test>pip3 install django
Collecting django
  Downloading https://files.pythonhosted.org/packages/c7/87/fbd666c4f87591ae25b7bb374298e8629816e87193c4099d3608ef11fab9/Django-2.1.7-py3-none-any.whl (7.3MB)
Requirement already satisfied: pytz in c:\python37\lib\site-packages (from django) (2018.9)
Installing collected packages: django
Successfully installed django-2.1.7

可見 django 有一個相依模組 pytz.

也可以指定 Django 版本, 指令如下 :

D:\Python\test>pip3 install django==2.1.7 

如果已經安裝過 Django 想要更新為最新版, 就在指令後面加上 -U 參數 :

pip3 install django -U

在樹莓派上安裝結果如下 :

pi@raspberrypi:~ $ pip3 install django
Collecting django
  Downloading https://files.pythonhosted.org/packages/c7/87/fbd666c4f87591ae25b7bb374298e8629816e87193c4099d3608ef11fab9/Django-2.1.7-py3-none-any.whl (7.3MB)
Collecting pytz (from django)
  Downloading https://files.pythonhosted.org/packages/61/28/1d3920e4d1d50b19bc5d24398a7cd85cc7b9a75a490570d5a30c57622d34/pytz-2018.9-py2.py3-none-any.whl (510kB)
Installing collected packages: pytz, django
Successfully installed django-2.1.7 pytz-2018.9

安裝好可用下列指令檢視 Djangle 版本 :

C:\Users\User>python -m django --version 
2.1.7 

或在 IDLE 中查詢 :

>>> import django
>>> django.VERSION
(2, 1, 7, 'final', 0)

參考 :

The Complete Guide To Install Django And Configure It (Linux & Windows)


2. 使用 django-admin startproject 建立網頁專案 : 

指令 : django-admin startproject 專案名稱

以建立網頁專案 mysite 為例 :

D:\Python\test>django-admin startproject mysite

此指令執行後會在目前資料夾下建立一個 mysite 資料夾 (上層專案目錄=網站根目錄), 其下有一個 manage.py 與另一個 mysite 資料夾 (下層專案目錄=全站設定), 結構如下 :


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


這些自動建立的網頁檔案只是一個基本架構, 預設是用來呈現一個 demo 網頁, 實際開發專案是在此架構基礎上進行增修以建構較複雜之網頁, 這些手動作業主要包括 :
  1. 在下層專案目錄下添加一個 views.py 來處理視圖之呈現
  2. 修改 urls.py 指派 views.py 中對應之處理函數
  3. 在上層專案目錄下添加一個 templates 目錄來存放網頁模版
  4. 在上層專案目錄下添加一個 static 目錄來存放靜態檔案 (css/js/images)
最後之檔案結構如下所示 :


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


注意, urls.py 與 views.py 並非強制的, 可以改用其他檔名, 但要做好配套. 如果不用 urls.py 要改用例如 routers.py, 則必須修改 settings.py, 將設定項目 ROOT_URLCONF 從 'mysite.urls' 改為 'mysite.router'; 若不用 views.py 要改用例如 renders.py, 則在路由設定程式 (預設 urls.py) 裡面就要匯入 renders.py 而非 views.py. 良心的建議是 : 用預設的就好, 別自找麻煩.

首先來測試這個預設之 demo 網頁.


3. 啟動 django 伺服器 :

先用 cd mysite 指令切換至上層專案目錄 (因為那裏才有 manage.py 程式), 再執行 runserver 啟動內建之開發伺服器, 注意, 此伺服器預設開啟 8000 埠 :

D:\Python\test>cd mysite 

D:\Python\test\mysite>python manage.py runserver 
Performing system checks...

System check identified no issues (0 silenced).

You have 15 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.
February 23, 2019 - 23:16:49
Django version 2.1.7, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000

可見此網頁伺服器預設監聽 8080 埠. 開啟瀏覽器連線 localhost:8000 或 127.0.0.1:8000 即可看到此預設網頁:




Bingo! 第一個 django 網站完成了!

上面這個 127.0.0.1 只能用在本機而已, 如果要讓區網內其他主機也可存取網頁, 則必須在啟動 Django 內建的開發伺服器時指定本機在區網中的 IP 位址, 也可以指定埠號, 不一定要用預設之 8000 埠, 例如 :

python manage.py runserver 192.168.2.105 8080 

首先用 ipconfig 查詢本機 IP, 得到例如 192.168.2.105, 然後修改專案主目錄 (此處為第二層 mysite) 下的 settings.py, 將此 IP 以字串格式放入 ALLOWED_HOST 這個串列中, 例如 :

ALLOWED_HOSTS = ['192.168.2.105'] 

這樣就可以啟動伺服器了 :

D:\Python\test\mysite>python manage.py runserver 192.168.2.105:8080 
Performing system checks...

System check identified no issues (0 silenced).

You have 15 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.
February 28, 2019 - 16:18:37
Django version 2.1.7, using settings 'mysite.settings'
Starting development server at http://192.168.2.105:8080/
Quit the server with CTRL-BREAK.

這樣在瀏覽器網址列就要輸入 192.168.2.105:8080 才會連線到此伺服器了.


二. Django 專案的結構 :

接下來進一步研究上面 django 所建立的檔案與目錄結構 :


mysite  (網站根目錄)
     |____ manage.py
     |____ mysite  (全站設定)


Django 的基本檔案架構由兩層專案目錄組成, 上層專案目錄是網站根目錄, 預設包含一個管理程式 manage.py 與下層專案目錄, 以及未來將來要建立的 app 目錄. 下層專案目錄則用來存放全站設定, 預設包括網站的設定檔 settings.py 與路由配置檔 urls.py.

上層專案目錄下的 manage.py 是這個專案的管理程式, 用途為管理網站組態與接收處理命令列指令, 其主要功能與指令如下所示 :
  1. 啟動 Django 的測試伺服器或 Shell 介面 :
    python manage.py runserver
    python manage.py shell
  2. 建立應用程式 (App) :
    python manage.py startapp <APPNAME>
  3. 建立資料庫架構與版本的 migration 資料檔 :
    python manage.py makemigrations <APPNAME>
  4. 同步模型與資料庫 :
    python manage.py migrate
例如上面就用了 python manage.py runserver 啟動伺服器, 而建置應用程式則使用 python manage.py startapp app_name. 此程式主要是執行用, 不需要修改.

管理程式 manage.py 的內容如下 :

#!/usr/bin/env python
import os
import sys

if __name__ == '__main__':
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)

可見它主要是從 django 套件的核心引入 execute_from_command_line 模組.

專案內容主要是放在下層專案目錄 mysite 資料夾下, 預設會建立四個檔案, 其用途如下 :



 下層專案目錄預設檔案 說明
 __init.py__ 空檔案, 使下層專案目錄成為一個套件 (package)
 settings.py 專案的設定檔 (網站功能設定)
 ursl.py URL 路由配置檔 (網址與處理函數的對應)
 wsgi.py 網頁伺服器與 Django 之介面設定黨, Django 的入口


其中 settings.py 與 urls.py 是專案的主角, __init.py__ 是一個空檔案, 目的只是要讓下層專案目錄符合套件形式上的要求而設; wsgi.py 則只有部署到主機上時才會用到.

首先開啟路由配置檔 urls.py, 此檔案負責將伺服器收到的 URL 要求對應到控制器 views.py 中的函數進行處理, 其預設內容如下 :

"""mysite URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/2.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin   
from django.urls import path

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

可見預設路由指派已經有一個 URL, 即 localhost:8000/admin 了, 此網址是 Django 的資料庫後端管理頁面. 當新增一個網頁程式時需在 urlpatterns 串列變數內新增一個 URL 與對應處理函數之 tuple. 

其次開啟 settings.py, 此檔案是專案的系統設定檔, 預設內容如下 :

"""
Django settings for mysite project.

Generated by 'django-admin startproject' using Django 2.1.7.

For more information on this file, see
https://docs.djangoproject.com/en/2.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.1/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '2dwazqn8dsi#9lae!c32%fv(*rk@@ygbwg0c40+d38vb(+oky@'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'mysite.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        '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',
            ],
        },
    },
]

WSGI_APPLICATION = 'mysite.wsgi.application'


# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/

STATIC_URL = '/static/'

此設定檔內都以大寫的識別字做為設定值, 其中比較重要者如下 : 
  1. BASE_DIR : 上層專案目錄 (第一層) 的路徑, 此處為第一層 mysite.
    os.path.abspath(__file__) 會傳回 settings.py 的絕對路徑, 傳給 os.path.dirname() 後會傳回下層專案目錄, 即第二層 mysite, 再傳給 os.path.dirname() 即傳回上層專案目錄, 即第一層 mysite.
  2. DEBUG : 是否啟動偵測模式
    預設為 True, 當專案開發完成, 系統正式上線時務必改為 False 以避免資安疑慮. 
  3. ALLOWED_HOST : 指定 IP 時需以字串放在串列中
  4. INSTALLED_APPS : 已安裝之應用程式
    此即為 Django 所謂應用程式可插拔 (pluggable) 的插頭位置, 當專案新增應用程式時, 必須在此串列尾添加新 APP 名稱. 
  5. ROOT_URLCONF : 根網址設定檔
    設定網址與網頁的配對程式, 此處為 mysite 下的 urls.py. 
  6. DATABASE : 資料庫設定
    預設使用 SQLite3 資料庫 db.sqlite3. 
  7. LANGUAGE_CODE : 語系
    預設為美式英語 en-us, 台灣繁體中文語系要改為 zh-Hant, 在 Django 1.7 以前是用 zh-TW, 這在 2 版行不通. 
  8. TIME_ZONE : 時區
    預設為 UTC, 台灣時區要改為 Asia/Taipei.
以下測試 Django 的請求與回應處理, 這主要是由下層專案目錄下的 urls.py 與 views.py 這兩個程式負責, 它們屬於 MVC 架構中的控制器角色. 在 views.py 中使用 django.http.HttpResponse 類別來處理回應網頁, 而請求處理則是在 urls.py 中使用 django.urls.path 類別來解析 URL 與指配處理函數, 程序如下圖所示 :





三.  用 HttpResponse 輸出 Hello World 頁面 : 

如上所述, Django 是利用 urls.py 指配客戶端要求之 URL 至 views.py 的處理函數以產生回應之網頁, 以下以經典的輸出 Hello World 的網頁為例進行測試. 首先在下層專案目錄 (第二層 mysite) 下新增一個 views.py 如下 :

#views.py
from django.http import HttpResponse

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

此程式從 django.http 模組引入 HttpResponse 類別以處理 HTTP 回應, 然後定義一個自訂函數 helloworld() 來回應客戶端, 此函數有一個傳入參數 request 會從伺服器接收一個封裝了客戶端要求的 HttepRequest 物件, 雖然此例用不到還是要傳入, 因為這是必要參數. 此函數只是簡單地呼叫 HttpResponse() 物件方法傳回字串. 注意, 由於回應中含有中文, 因此須以 utf-8 格式存檔.

接著編輯下層專案目錄下的 urls.py, 先從 mysite.views 模組匯入 helloworld 函數, 然後在 urlpatterns 串列中, 於預設的 admin 底下增加一個 path() 函數呼叫, 將要求之 URL 字串 'hello/' 以及所對應之 helloworld 函數當作參數傳進去 :

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

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

路由器 urls.py 也可以用下列寫法 :

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

from mysite import views
#或 from . imports views 亦可

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

差異主要是因為 import 的方式不同, 如果是匯入 views 模組, 則在 path() 內配對的目的地就必須冠上views 模組名稱.

這樣就完成網站更新, Django 開發伺服器會偵測到組態更新, 不須重開伺服器, 於瀏覽器網址列輸入 localhost:8000/hello 就會顯示如下網頁 :



Bingo! 以上便是 Django 輸出簡單網頁的做法.

上面的例子是將欲輸出的網頁直接傳給 HttpResonse(), 如果是較長的網頁內容 views.py 裡面應改用三個引號的長字串變數較易讀, 例如將上面的 views.py 改為如下效果一樣 (路由器 urls.py 不用改) :

#views.py
from django.http import HttpResponse

def helloworld(request):
    html='''
<!DOCTYPE html>
<html>
  <head>
    <title>Hello World</title>
  </head>
  <body>
    <b>Hello World! <i>您好</i></b>
  </body>
</html>
    '''
    return HttpResponse(html)

有了 /hello 這個 URL 後雖然輸入 localhost:8000/hello 會顯示正確的網頁輸出, 但若請求根目錄卻顯示 404 page not found :




這是因為在 urls.py 與對應之 views.py 裡面沒有處理根目錄請求之故. 首先在 views.py 中添加名為 home 的函數來輸出根目錄要求之輸出 :

#views.py
from django.http import HttpResponse

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

def home(request):
    return HttpResponse('<b>歡迎來到我的首頁!</b>') 

函數 home() 只是簡單地輸出一個字串而已. 然後修改 urls.py 如下 :

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

from mysite.views import home, helloworld

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

在 urlpatterns 串列中添加空字串 (表示根目錄) 對應到 views.py 裡面的 home() 函數, 這樣在網址列輸入 localhost:8000 就會顯示 '歡迎來到我的首頁!' 了.

上面的範例處理了路由問題, 但能否傳遞參數呢? 可以的, Django 依循 RESTful 方式傳遞參數, 亦即參數放在 URL 的後面, 格式如下 :

route/param

例如向 Tony 打招呼的 URL 是 /hello/Tony.

在上面已提及, 在 Django 2 的路由器 urls.py 程式中, 從伺服器接收參數的方式已改為使用尖括號並同時完成資料型態轉換, 格式如下 :

path('route/<type:param>/', function_name),

將上面的 urls.py 改為如下 :

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

from mysite.views import home, helloworld

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

此處尖括號內的 str:name 表示以字串格式接收 URL 中的參數, 名稱為 name. 注意, 若無指定資料型態, 預設為字串, 參數的型態格式有下列 5 種, 稱為路徑轉換器 (path converter) :


 參數型態 說明
 int 0 或正整數
 str 非空字串 (預設), 不含路徑分隔字元 "/"
 slug 以 ASCII 字元組成之字串 (含 dash 與 underscore)
 uuid 通用唯一標示碼 uuid 字串 (字母皆小寫)
 path 非空字串 (含路徑分隔字元 "/"), 可表示完整的 URL


參考 :

https://docs.djangoproject.com/en/3.0/topics/http/urls/

因為參數型態預設為 str, 所以上面範例 urls.py 中的 :str 也可以不寫. 而在 views.py 中, 參數 name 則放在第一參數 request 後面, 例如 :

#views.py
from django.http import HttpResponse

def helloworld(request, name):
    return HttpResponse('<b>Hello World! </b>' + name)

def home(request):
    return HttpResponse('<b>歡迎來到我的首頁!</b>')

此處將接收到的第二參數 name 串在 HTML 碼後面回應給客戶端, 在瀏覽器網址列上輸入 localhost:8000/hello/Tony 結果如下 :



可見參數 name=Tony 已被成功地透過 URL 傳遞給伺服器並回應給客戶端. 如果要傳遞多個參數, 則這些參數就在 URL 中串接在路由的後面, 格式為 :

route/param1/param2/....

在 urls.py 的 path() 呼叫中, 依序用倒斜線串接的尖括號來接收這些參數 (可指定轉換成所要之資料型態), 例如 :

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

from mysite.views import home, helloworld

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

而 views.py 中所對應的處理函數要依序接收所傳進來的參數, 例如 :

#views.py
from django.http import HttpResponse

def helloworld(request, name, message):
    return HttpResponse('<b>Hello World! </b>' + name + ': ' + message)

def home(request):
    return HttpResponse('<b>歡迎來到我的首頁!</b>')

這時在網址列輸入 localhost:8000/tony/How are you 會看到如下網頁 :




視圖處理函數 views.py 中的傳入參數也可以有預設值, 當請求的 URL 中沒有傳參數過來時就會以預設值頂替, 範例如下 :

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

from mysite.views import home, helloworld

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

此處 urls.py 需添加 'hello/<name>/' 這一筆, 否則會找不到 URL 而報錯, 並不會因為有 'hello/<name>/<message>/' 這筆而找到路由 (因為不是用正規式). 相對應的 views.py 中 helloworld() 的 message 要設定預設值如下 :

#views.py
from django.http import HttpResponse

def helloworld(request, name, message='Blablabla ...'):
    return HttpResponse('<b>Hello World! </b>' + name + ': ' + message)

def home(request):
    return HttpResponse('<b>歡迎來到我的首頁!</b>')

注意, Python 函數的傳入參數若有預設值必須全部靠右放置 (即通通放在最後面), 否則執行時會報錯. 在瀏覽器網址列輸入 localhost:8000/tony/ 沒有傳入 message 參數便會以其預設值 "blablabla" 代替 :




以上便是 Django 對 HTTP 請求與回應之基本處理方式.


四. HttpRequest 物件 : 

Django 的請求處理流程是從 settings.py 開始. 當啟動 Django 內建的開發伺服器時, 它會讀取專案目錄下的 settings.py 以解析網站的設定資訊, 其中最重要的是 ROOT_URLCONF, 它指到一個根路由設定檔, 預設為 urls.py, 此檔案告訴 Django 在此網站中會用到哪些 Python 模組以及 URL 對應到 views.py 裡哪些函數.

當 Django 接到頁面請求時, 它會將 HTTP 請求之 metadata 打包成 HttpRequest 物件, 然後在 urls.py 中依序搜尋 URL 樣式, 當找到第一個符合的 pattern 時以 HttpRequest 物件為第一參數呼叫 views.py 中對應的函數, 此函數處理完需傳回一個 HttpResponse 物件, Django 會將其轉成 HTTP 回應給用戶端.

HttpRequest 物件的常用屬性如下 :


 HttpRequest 物件屬性 說明
 method 請求的方法 (字串), 例如 "GET", "POST"
 GET GET 方法所帶之參數 (字典)
 POST POST 方法所帶之參數 (字典)
 user 使用者物件, 可用 is_authenticated() 方法判斷是否登入
 session 連線物件 (字典)
 COOKIES Cookies 物件 (字典)
 META 請求之標頭資訊 (字典), 例如 HTTP_REFERER 等


其中 method 可用來判斷不同請求方法的處理方式, 例如 :

if request.method=="GET":
   ....
elif request.method=="POST"

屬性 user 可用來判斷使用者是否已登入 :

if request.user.is_authenticated():
   ...
else:
   ...

詳細參考 :

https://docs.djangoproject.com/en/2.1/ref/request-response/

例如在 views.py 增加一個 request_test() 的視圖函數 :

#views.py
from django.http import HttpResponse

def helloworld(request, name, message='Blablabla ...'):
    return HttpResponse('<b>Hello World! </b>' + name + ': ' + message)

def home(request):
    return HttpResponse('<b>歡迎來到我的首頁!</b>')

def request_test(request):
    try:
        method=request.method
        http_host=request.META['HTTP_HOST']
        http_user_agent=request.META['HTTP_USER_AGENT']
        remote_addr=request.META['REMOTE_ADDR']
        return HttpResponse('[method]:%s<br>[http_host]:%s<br>\
                            [http_user_agent]:%s<br>[remote_addr]:%s'\
                            %(method, http_host, http_user_agent, remote_addr))
    except e:
        return HttpResponse('Error:%s' &e)

然後在 urls.py 裡面添加一個 request_test 路徑對應 :

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

from mysite.views import home, helloworld, request_test

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

以瀏覽器拜訪 localhost:8000/request_test/ 結果如下 :




五. HttpResponse 物件 : 

HttpRequest 物件是 Django 自動建立的, 但 HttpResponse 物件則需自行呼叫 HttpResponse() 建構子或 render(), render_to_response() 函數, 並傳入一個表示回應頁面內容的 HTML 字串來建立. 每一個 views.py 內的函數都必須傳回一個 HttpResponse 物件以便讓客戶端能看到回應結果.

HttpResponse 物件也可以用檔案串流方式操作, 先建立 HttpResponse 物件, 然後再分批呼叫其 write() 方法寫入串流後再傳回用戶端. 這種方法對設定回應標題很方便.

上面範例中 views.py 裡的 request_test() 也可以這麼寫 :

#views.py
from django.http import HttpResponse

def helloworld(request, name, message='Blablabla ...'):
    return HttpResponse('<b>Hello World! </b>' + name + ': ' + message)

def home(request):
    return HttpResponse('<b>歡迎來到我的首頁!</b>')

def request_test(request):
    response=HttpResponse() 
    try:
        method=request.method
        http_host=request.META['HTTP_HOST']
        http_user_agent=request.META['HTTP_USER_AGENT']
        remote_addr=request.META['REMOTE_ADDR']
        response.write('[method]:%s<br>' % (method))
        response.write('[http_host]:%s<br>' % (http_host))
        response.write('[http_user_agent]:%s<br>' % (http_user_agent))
        response.write('[remote_addr]:%s' % (remote_addr))
        response['Cache-Control']='no-cache'      #設定回應標題
        return response
    except e:
        return response.write('Error:%s' % e)

此例設定回應標題 Cache-Control 為 no-cache, 這可以在 Chrome 按 F12 進入開發者模式, 在 Network/Headers 頁籤內看到 :




HttpResponse 類別也定義了一些好用的子類別, 方便處理重導向或回應 404, 500 等錯誤, 常用 HttpResponse 子類別如下表 :


 HttpResponse 子類別 說明
 HttpResponsePermanentRedirect 永久重導向至指定 URL (狀態碼 301)
 HttpResponseRedirect 重導向至指定 URL (狀態碼 302)
 HttpResponseBadRequest 請求錯誤 (狀態碼 400)
 HttpResponseForbidden 頁面不存在 (狀態碼 403)
 HttpResponseNotFound 頁面禁止  (狀態碼 404)
 HttpResponseNotAllowed 請求不允許 (狀態碼 405)
 HttpResponseServerError  (狀態碼 500)


例如下列重導向範例是在上例的 views.oy 中添加一個 redirect() 函數, 直接傳回 HttpResponseRedirect 物件重導向首頁 :

#views.py
from django.http import HttpResponse
from django.http import HttpResponseRedirect

def helloworld(request, name, message='Blablabla ...'):
    return HttpResponse('<b>Hello World! </b>' + name + ': ' + message)

def home(request):
    return HttpResponse('<b>歡迎來到我的首頁!</b>')

def request_test(request):
    response=HttpResponse()
    try:
        method=request.method
        http_host=request.META['HTTP_HOST']
        http_user_agent=request.META['HTTP_USER_AGENT']
        remote_addr=request.META['REMOTE_ADDR']
        response.write('[method]:%s<br>' % (method))
        response.write('[http_host]:%s<br>' % (http_host))
        response.write('[http_user_agent]:%s<br>' % (http_user_agent))
        response.write('[remote_addr]:%s' % (remote_addr))
        response['Cache-Control']='no-cache'
        return response
    except e:
        return response.write('Error:%s' % e)

def redirect(request):   
    return HttpResponseRedirect("/") 

然後在 urls.py 中匯入此 redirect() 函數並對應到 "redirect/" :

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

from mysite.views import home, helloworld, request_test
from mysite.views import redirect 

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

在網址列輸入 localhost:8000/redirect 果真重導向至首頁.


2020-01-20 補充 :

Django 在 2020 年已推出第三版, 在設定 (例如語言) 上與第二版有些差異, 第二版最後一個版本是 2.2.9 版, 參考 :




What Python version can I use with Django?

沒有留言 :