目錄
Django2實戰示例 第一章 創建博客應用
Django2實戰示例 第二章 增強博客功能
Django2實戰示例 第三章 擴展博客功能
Django2實戰示例 第四章 創建社交網站
Django2實戰示例 第五章 內容分享功能
Django2實戰示例 第六章 追蹤用戶行為
Django2實戰示例 第七章 創建電商網站
Django2實戰示例 第八章 管理支付與訂單
Django2實戰示例 第九章 擴展商店功能
Django2實戰示例 第十章 創建在線教育平台
Django2實戰示例 第十一章 渲染和緩存課程內容
Django2實戰示例 第十二章 創建API
Django2實戰示例 第十三章 上線
第十三章 上線
在上一章,為其他程序與我們的Web應用交互創建了RESTful API。本章將學習如何創建生產環境讓我們的網站正式上線,主要內容有:
- 配置生產環境
- 創建自定義中間件
- 實現自定義管理命令
1創建生產環境
現在該將Django項目正式部署到生產環境中了。我們將按照下列步驟將站點部署到生產環境中:
- 為生產環境配置項目設置
- 使用PostgreSQL數據庫
- 使用uWSGI和NGINX建立web服務器
- 管理靜態資源
- 使用SSL加強站點安全管理
1.1管理用於多個環境的配置
在實際的項目中,很可能要面對不同的環境。一般至少有一個本地開發環境和一個生產環境,也可能有其他環境比如測試環境,預上線環境等。對於不同的環境,有些設置是通用的,有些則因環境而異。讓我們將項目設置為可以適合不同環境,又可以保證項目結構不會被改變。
在educa/educa/
目錄下建立settings
目錄(包),與settings.py
同級,將settings.py
文件重命名為base.py
然后移動到settings
目錄中來,再創建其他文件,setting/
目錄如下所示:
settings/
__init__.py
base.py
local.py
pro.py
這些文件用途如下:
base.py
:基本的設置文件,包含通用的設置,是原來的settings.pylocal.py
:本地環境的自定義設置pro.py
:生產環境的自定義設置
編輯settings/base.py
,找到下列這行:
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
將其替換成下邊這行:
BASE_DIR =
os.path.dirname(os.path.dirname(os.path.abspath(os.path.join(__file__, os.pardir))))
由於我們將settings.py
文件又往下級目錄放了一級,必須讓BASE_DIR
指向正確的路徑,所以使用了os.pardir
指向父目錄,來讓最后的路徑依然是原來的項目根目錄。
編輯settings/local.py
,添加下列代碼:
from .base import *
DEBUG = True
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
這是代表我們本地環境的配置文件。在其中導入了所有base.py
中的設置內容,然后寫了DEBUG
和DATABASES
兩個設置,這兩個設置會覆蓋原來base.py
中的設置,成為本文件中的設置。由於DEBUG
設置和DATABASES
設置在每個配置文件中都會修改,也可以將這兩個設置從base.py
中刪除。
再來編輯settings/pro.py
,如下所示:
from .base import *
DEBUG = False
ADMINS = (
('Antonio M', 'email@mydomain.com'),
)
ALLOWED_HOSTS = ['*']
DATABASES = {
'default': {
}
}
這是生產環境的配置文件,來詳細看一下其中的內容:
DEBUG
:設置DEBUG
為False
是生產環境的強制要求。如果不關閉,會將錯誤跟蹤和敏感配置信息泄露給所有人。ADMINS
:當DEBUG
設置為False
的時候,如果一個視圖拋出異常,所有信息會以郵件形式發送到ADMINS
配置中列出的所有人。需要將其中的信息改成自己的名字和郵箱(還需要配置SMTP服務器)。ALLOWED_HOSTS
:Django只會向這個設置中的地址或者主機名稱提供Web服務。這是一個安全手段。我們使用了通配符*表示可以用於所有主機名稱或者IP地址。在稍后的配置中會更詳細的作出限制。DATABASES
:生產環境的數據庫設置,現在留空,后邊會進行該設置。由於生產環境的數據庫和非生產環境的數據庫一般是隔離的,甚至生產環境數據庫只有處於生產環境才能訪問。所以該項需要單獨配置。
在需要面對多種環境時,建立一個基礎配置文件並為每種環境編寫單獨的配置文件。用於具體環境的配置文件繼承基礎配置並重寫與環境相關的配置即可。
由於我們現在沒有把配置文件放在原來settings.py
所在的位置,所以無法運行manage.py
,必須為其指定settings
模塊的所在路徑,即使用--settings
參數或者設置環境變量DJANGO_SETTINGS_MODULE
。
打開系統命令行窗口輸入:
export DJANGO_SETTINGS_MODULE=educa.settings.pro
這條命令會為當前的會話窗口設置DJANGO_SETTINGS_MODULE
環境變量。如果不想每次運行shell都執行一遍,可以把這條命令加入到shell配置文件如.bashrc
或者.bash_profile
中。
如果不想對系統進行任何設置,那么在啟動站點的時候必須加上--settings
參數,如下:
python manage.py migrate --settings=educa.settings.pro
現在我們就為多環境做好了基礎設置。
1.2使用PostgreSQL數據庫
在整本書中,我們大部分都使用了Python自帶的SQLite數據庫,只要在博客全文檢索的時候推薦使用了PostgreSQL數據庫。SQLite輕量而且易於使用,但對於生產環境而言太過簡陋,必須需要一個更強力的數據庫比如PostgreSQL和MySQL或者Oracle。PostgreSQL的安裝在第三章中已經介紹過,不再贅述。
讓我們為我們的應用創建一個PostgreSQL用戶,打開系統命令行輸入如下命令:
su postgres
createuser -dP educa
系統會提示輸入用戶密碼和權限。輸入密碼並且給予用戶權限,然后使用下列命令建立一個新的數據庫:
createdb -E utf8 -U educa educa
這樣就建立好了一個新的數據庫並且將其分配給educa用戶,之后編輯settings/pro.py
,修改數據庫的設置如下:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'educa',
'USER': 'educa',
'PASSWORD': '',
}
}
將密碼部分替換成為educa用戶設置的密碼。由於新數據庫是空的,運行:
python manage.py migrate
然后創建一個超級用戶:
python manage.py createsuperuser
譯者注,安裝PostgreSQL遠沒有這么簡單,尤其是通過第三方程序遠程管理PostgreSQL,需要修改PostgreSQL的配置文件,將認證方式修改為md5或者trust,然后啟用允許訪問的IP,建議查看官方文檔和各種安裝教程進行配置。
1.3部署前檢查
Django提供了一個check
命令,可以在任何時候檢查項目。通常檢查過程包括檢查所有注冊的應用,輸出所有錯誤和警告信息。如果包含--deploy
參數,還會額外執行針對生產環境的檢查。
打開系統終端然后輸入如下命令進行檢查:
python manage.py check --deploy
譯者注:作者這里遺漏了配置文件的路徑,應該寫成python manage.py check --deploy --settings=educa.settings.*
,其中*為base,local或pro
如果站點編寫正確的話,會看到沒有錯誤輸出,但是會有一些警告信息。這說明站點通過了檢查,但這些警告信息應該得到處理,以讓站點更加安全。本書不會深入這里的內容,但是要記得在正式部署之前一定要進行部署前檢查。
1.4通過WSGI程序提供Django服務
Django的主要部署平台就是WSGI,WSGI是Web Server Gateway Interface的簡稱,是基於Python的程序提供Web服務的標准格式。由於Django也是Python程序,也需要通過WSGI對外提供服務。
當通過startproject
命令新建一個項目的時候,Django會在項目目錄內新建一個wsgi.py
。這個文件包含了一個WSGI可調用函數,為我們的Django應用提供了一個接口。無論是我們之前采用本機8000端口的開發服務器,還是正式生產環境,都需要通過這個接口。關於WSGI的詳細知識可以看https://wsgi.readthedocs.io/en/latest/及Python的PEP333。
1.5安裝WSGI
直到本節之前,我們的所有開發都是在django在本地環境運行的開發服務器上進行的。在生產環境中,需要一個真正的web服務器才能部署django服務。
uWSGI是一個非常快的Python應用程序WSGI服務器,使用WSGI標准與Python應用進行通信。uWSGI把HTTP請求翻譯成Django程序能夠處理的格式。
安裝uSWGI:
pip install uwsgi==2.0.17
在pip安裝之后,會built uWSGI(編譯安裝),需要一個C編譯器,比如GCC或者clang,在linux環境下可以輸入命令:apt-get install build-essential
。
如果是MacOS X,可以通過Homebrew安裝,執行命令:brew install uwsgi
。如果在windows下安裝,需要Cygwin https://www.cygwin.com
。推薦在基於UNIX的操作系統上安裝uWSGI。
UNIX環境下如果看到Successfully built uwsgi就說明成功安裝了uWSGI。關於uWSGI的文檔可以在https://uwsgi-docs.readthedocs.io/en/latest/找到。
1.6配置uWSGI
可以通過命令行配置uWSGI,打開系統命令行模式,進入educa
項目的根目錄,然后輸入:
sudo uwsgi --module=educa.wsgi:application --env=DJANGO_SETTINGS_MODULE=educa.settings.pro --master --pidfile=/tmp/project-master.pid --http=127.0.0.1:8000 --uid=1000 --virtualenv=/home/env/educa/
必須需要su
權限才可以。通過這條命令,為本機上的uWSGI設置了如下的內容:
- 使用
educa.wsgi:application
作為調用接口 - 載入生產環境的設置文件
- 使用
virtualenv
設置的虛擬環境,注意將/home/env/educa/
替換為實際的虛擬環境所在路徑。如果未使用虛擬環境,該配置可以不填。
如果不是在項目目錄內執行的上述命令,需要額外加一個參數指定具體的項目目錄--chdir=/path/to/educa/
,將其中的/path/to/educa/
替換成educa的項目路徑。
通過瀏覽器訪問http://127.0.0.1:8000/(無需啟動django服務),可以看到站點內容顯示了出來,但沒有任何CSS樣式,也無法顯示圖片,這是因為還沒有配置uWSGI來提供靜態文件服務。
uWSGI允許使用一個.ini
配置文件進行自定義配置,比使用命令行要方便很多。在educa
項目根目錄下建立:
config/
uwsgi.ini
編輯uwsgi.ini
,添加如下代碼:
[uwsgi]
# variables
projectname = educa
base = /home/projects/educa
# configuration
master = true
virtualenv = /home/env/%(projectname)
pythonpath = %(base)
chdir = %(base)
env = DJANGO_SETTINGS_MODULE=%(projectname).settings.pro
module = educa.wsgi:application
socket = /tmp/%(projectname).sock
在這個.ini
文件里我們定義了兩個變量:
projectname
:Django項目的名稱,是educa
base
:educa
項目的絕對路徑,將其替換成實際項目路徑
上邊定義的這兩個變量是自定義變量,還可以定義任意其他變量,只要不和內置的名稱沖突。接下來是具體設置的解釋:
master
:表示啟用主進程virtualenv
:虛擬環境地址,將其替換成實際的路徑所在(不包含bin/activate
)pythonpath
:加入到Python PATH中的地址,一般就是項目的根目錄chdir
:項目的實際地址,uWSGI會在加載應用之前將工作目錄變更到這個路徑env
:環境變量,設置為DJANGO_SETTINGS_MODULE
,具體路徑指向生產環境的配置文件module
:要使用的WSGI模塊,指向項目中的wsgi.py
中的調用函數。application
是該函數在項目中默認的命名。socket
:綁定該服務的套接字。(是一個文件套接字,用於與NGINX通信)
其中的socket
套接字是用於和第三方路由軟件進行通信,比如NGINX。命令行模式中我們使用的--http 127.0.0.1:8000
指的是讓uWSGI自己接受HTTP請求並自己負責路由這些請求。我們需要把uWSGI作為socket啟動(在.ini
文件設置中並沒有設置--http
參數),因為我們要使用NGINX作為我們的web服務器,NGINX通過剛才設置的文件套接字與uWSGI進行通信。
關於uWSGI的詳細設置可以看 https://uwsgi-docs.readthedocs.io/en/latest/Options.html。
現在可以通過使用配置文件來啟動uWSGI(先關閉原來運行的uWSGI服務):
uwsgi --ini config/uwsgi.ini
這樣運行之后,可以發現暫時無法通過瀏覽器訪問http://127.0.0.1:8000/,因為此時uWSGI監聽文件套接字而不是HTTP端口,我們還需要繼續完善生產環境配置。
1.7配置uWSGI
當啟動一個Web服務的時候,很顯然必須提供動態的內容服務,但也需要靜態的文件服務,比如CSS,JavaScript文件,圖像等。如果用uWSGI來管理靜態文件,會為HTTP請求增加不必要的開銷,所以最好在uWSGI之前加一個Web服務,比如NGINX。
NGINX是一個高並發,低內存占用的Web服務端,也具有反向代理功能,即接受一個HTTP請求,然后把這個請求路由給不同的后端。通常來說,你需要一個web服務端如NGINX,用於快速高效的提供靜態文件,然后把動態的請求轉發給uWSGI。通過使用NGINX,還可以設置其反向代理功能從而更好的提供web服務。
安裝NGINX可以使用下列命令:
sudo apt-get install nginx
如果使用MacOS X,可以通過brew install nginx
來安裝。Windows下的NGINX可以通過https://nginx.org/en/download.html下載。
譯者注:安裝NGINX后不會立刻啟動,譯者使用的Centos 7.5 1804還需要啟動NGINX服務和開機啟動:
systemctl start nginx.service
systemctl enable nginx.service
正常情況下在啟動NGINX之后,直接訪問本機IP地址,可以看到NGINX歡迎頁面,表示基礎配置成功運行,之后可以先停用NGINX服務,以配置生產環境。
1.8生產環境
下面的圖表示了我們最終配置的生產環境的結構:
當一個瀏覽器發起一個HTTP請求的時候,發生如下事情:
- NGINX接收HTTP請求
- 如果請求靜態文件,NGINX直接提供服務。如果請求動態頁面,NGINX通過SOCKET與uWSGI通信,將請求轉交給uWSGI處理
- uWSGI將請求轉交給Django后端進行處理,返回的響應被傳遞給NGINX,NGINX再發回給瀏覽器。
1.9配置NGINX
在config/
目錄下創建nginx.conf
文件,在其中添加如下代碼:
# the upstream component nginx needs to connect to
upstream educa {
server unix:///tmp/educa.sock;
}
server {
listen 80;
server_name www.educaproject.com educaproject.com;
location / {
include /etc/nginx/uwsgi_params;
uwsgi_pass educa;
}
}
這是NGINX的基礎配置。我們建立了一個upstream名叫educa
,指定了uWSGI使用的socket名稱 ,然后使用server
指令,其中的設置有:
listen 80
表示讓NGINX監聽80
端口- 設置主機名為
www.educaproject.com
和educaproject.com
,NGINX會為這兩個主機地址提供服務 - 配置
location
參數,將所有在'/'
路徑下的URL轉發給上邊的upstreameduca
,也就是uWSGI的socket進行處理。還把NGINX自帶的關於和uwsgi協同工作的參數設置也包含進去。
NGINX還有很多復雜的設置,文檔可以參考https://nginx.org/en/docs/。
NGINX主要的設置文件位於/etc/nginx/nginx.conf
,該文件包含/etc/nginx/sites-enabled/
下的所有配置文件。為了讓NGINX使用我們剛才編寫的配置文件,打開系統命令行窗口建立一個軟連接:
sudo ln -s /home/projects/educa/config/nginx.conf /etc/nginx/sites-enabled/educa.conf
將其中的/home/projects/educa/
替換成實際的絕對路徑。注意,這里如果沒有/sites-enabled/
目錄,要先手工建立。
如果還沒有運行uWSGI,打開系統命令行窗口,在educa
項目根目錄先運行uWSGI:
uwsgi --ini config/uwsgi.ini
當前窗口會被uWSGI占用,再開一個命令行窗口,然后執行:
service nginx start
由於我們使用了自定義的域名,還必須修改/etc/hosts
,添加如下兩行:
127.0.0.1 educaproject.com
127.0.0.1 www.educaproject.com
這樣我們就把這兩個域名都路由到本地回環地址上,由於我們是從本機訪問本機,所以要更改HOSTS,實際生產環境不必做本步修改,因為生產環境會有固定的IP,域名和對應的DNS解析。
打開瀏覽器,輸入http://educaproject.com/,應該可以看到站點了,但是所有的靜態文件依然沒有被加載,沒關系,即將完成生產環境的配置。
如果系統是Centos 7,這里顯示502錯誤,查看/var/log/nginx/error.log
,如果其中的錯誤是[crit] 4036#4036: *1 connect() to unix:///tmp/educa.sock failed (13: Permission denied)
,就先執行/usr/sbin/sestatus
查看SELINUX的狀態,如果為開啟,就編輯SELINUX的設置,將其關閉,如下:
vi /etc/selinux/config
#SELINUX=enforcing
SELINUX=disabled
之后reboot
重啟系統才行。之后應該就可以正常顯示站點了。
之后為了安全起見,到settings/pro.py
中,修改ALLOWED_HOSTS
設置為NGINX配置文件中的兩個域名:
ALLOWED_HOSTS = ['educaproject.com', 'www.educaproject.com']
現在Django就只為這兩個主機名提供服務了。關於ALLOWED_HOSTS
的更多信息可以看https://docs.djangoproject.com/en/2.0/ref/settings/#allowed-hosts。
1.10讓NGINX提供靜態文件和媒體資源服務
NGINX提供靜態文件的速度很快。剛才我們把所有的地址轉發,都交給了uWSGI,現在要將所有的靜態文件通過NGINX提供服務,對於我們站點來說,就是把所有的CSS JS文件和用戶上傳的媒體文件都交給NGINX來代理。
編輯settings/base.py
,增加下邊一行:
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
這行表示存放站點靜態文件的地址,還記得之前學習過使用python manage.py collectstatic
嗎?現在就需要將所有的靜態文件收集過來放在此目錄中,在命令行中輸入:
python manage.py collectstatic --settings=educa.settings.pro
注意,原書的命令缺少了 --settings=educa.settings.pro
可以看到下列輸出:
160 static files copied to '/educa/static'.
靜態文件目錄設置好了,現在需要將這個目錄設置到NGINX中,編輯config/nginx.conf
,在server
指令后的大括號中增加下列內容:
location /static/ {
alias /home/projects/educa/static/;
}
location /media/ {
alias /home/projects/educa/media/;
}
將其中的/home/projects/educa/static/
和/home/projects/educa/media/
替換成你項目的實際static
和media
目錄的絕對路徑。這兩個參數解釋如下:
/static/
:這個路徑是Django中設置的STATIC_URL
,表示當NGINX看到/static/
的路徑請求的時候,就到這個設置對應的路徑中尋找所需文件。/media/
:這個路徑是Django中設置的MEDIA_URL
路徑,表示當NGINX看到/media/
的路徑請求的時候,就到這個設置對應的路徑中尋找所需文件。
重新啟動NGINX服務,以便讓配置文件生效:
service nginx reload
在瀏覽器中打開http://educaproject.com/,現在可以看到整個站點包含靜態資源都正確的顯示了。對於站點的靜態文件請求,NGINX將繞開uWSGI,把文件直接返回給瀏覽器。
現在生產環境就初步配置完畢。整個站點現在可以說運行在生產環境之下了。
1.11使用SSL安全連接
在配置完初步的生產環境之后,下一個話題是站點的安全性。Secure Sockets Layer現在逐漸成為提供Web安全連接服務的規范。強烈建議對於正式的網站使用HTTPS協議,現在就在NGINX中配置SSL認證來讓站點變得更加安全。
1.11.1創建一個SSL認證
在educa
項目根目錄下建立一個ssl
目錄,然后通過openssl
生成我們的SSL證書:
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ssl/educa.key -out ssl/educa.crt
用這條命令生成一個365天有效的2048位的SSL證書,然后系統會提示輸入一些信息:
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []: educaproject.com
Email Address []: email@domain.com
這其中最關鍵的是Common Name
,必須將主機域名名稱輸入:這里使用educaproject.com
之后會在ssl/
目錄下生成兩個文件,educa.key
是私鑰,educa.crt
是實際的SSL證書。
1.11.2配置NGINX使用SSL
編輯config/nginx.conf
,在server
設置中加入下列內容:
server {
listen 80;
listen 443 ssl;
ssl_certificate /home/projects/educa/ssl/educa.crt;
ssl_certificate_key /home/projects/educa/ssl/educa.key;
server_name www.educaproject.com educaproject.com;
# ...
}
將其中的路徑都修改為SSL證書所在的實際絕對路徑。
這么設置之后,NGINX將同時監聽80
端口(HTTP協議)和443
端口(HTTPS協議),然后指定了SSL的驗證信息ssl_certificate
與對應的密鑰ssl_certificate_key
。
現在重新啟動NGINX服務,訪問 https://educaproject.com/ ,會看到類似如下提示:
這個提示因瀏覽器而異。意思是警告當前站點並沒有使用一個值得信任的驗證方式,瀏覽器無法確定該站點安全與否。這是因為我們使用的SSL證書是由我們自行簽發的,而不是從一個受信任的機構(Certification Authority)獲得的證書。當我們有了實際的公開域名之后,就可以向一個受信任的證書頒發機構申請一個SSL證書,這樣瀏覽器就能識別該站點的HTTPS認證。
如果想為實際的站點申請證書,可以使用Linux基金會Linux Foundation的Let's Encrypt項目。這是一個致力於免費獲得和更新SSL證書的計划,該計划的站點在 https://letsencrypt.org/。
點擊 "Add Exception" 按鈕可以讓瀏覽器知道可以信任該站點,這時瀏覽器的顯示可能如下:
點擊小鎖按鈕,就可以看到SSL的詳細信息。
譯者注:這里也因瀏覽器而異,有的瀏覽器依舊會提示證書不可信或者存在問題,畢竟這個證書是我們自行簽發的。
1.11.3配置Django使用SSL
Django也有針對SSL的配置,編輯settings/pro.py
,增加下邊的代碼:
SECURE_SSL_REDIRECT = True
CSRF_COOKIE_SECURE = True
這兩個設置的含義如下:
SECURE_SSL_REDIRECT
:是否所有的HTTP請求都必須被重定向到HTTPSCSRF_COOKIE_SECURE
:是否建立加密cookie防止CSRF攻擊
現在我們就配置好了一個高效的提供Web服務的生產環境。
2自定義中間件
在之前我們已經了解了中間件MIDDLEWARE
的設置,該設置包含項目中所有使用到的中間件。關於中間件,可以認為其是一個底層的插件系統,為在請求/響應的過程中提供鈎子。每一個中間件都負責一個特定的行為,會在HTTP請求和響應的過程中得到執行。
注意不要添加開銷非常大的中間件,因為中間件會在項目的所有請求和響應的過程中被執行。
當一個HTTP請求進來的時候,中間件會按照其在MIDDLEWARE
設置中從上到下的順序執行,當HTTP響應被生成且發送的過程中,中間件會按照設置中從下到上的順序執行。
一個符合標准的函數可以作為一個中間件被注冊在settings.py
中。類似下邊的函數就可以作為一個中間件:
def my_middleware(get_response):
def middleware(request):
# 對於每個HTTP請求,在視圖和之后的中間件執行之前執行的代碼
response = get_response(request)
# 對於每個HTTP請求和響應,在視圖執行之后執行的代碼
return response
return middleware
一個中間件工廠函數接受一個get_response
可調用對象,然后返回一個中間件函數。一個中間件接受一個請求然后返回一個響應,類似於視圖。這里的get_response
可以是下一個中間件,如果自己就是中間件列表中的最后一個,也可以是一個視圖名稱。
如果任何一個中間件在尚未調用get_response
這個可調用對象之前就返回了一個響應,這個時候就會短路整個中間件鏈條的處理:其后的中間件不再被執行,這個響應開始從同級的中間件向上返回。
所以MIDDLEWARE
設置中的中間件順序非常重要,因為中間件依賴於上下中間件的數據進行工作。
在向MIDDLEWARE
中添加一個中間件時必須注意將其放置在正確的位置,反復強調,中間件在HTTP請求進來的時候從上到下執行,HTTP響應發出的時候從下到上執行。
原書在這里只是比較簡單的說了一下執行順序,詳細的中間件執行順序請參考Django進階-中間件以及https://docs.djangoproject.com/en/2.0/topics/http/middleware/。
2.1創建二級域名中間件
我們來創建一個自定義中間件,用於通過一個自定義的二級域名來訪問課程資源。例如:某個顯示課程的URL:https://educaproject.com/course/django/
,可以通過一個二級域名django.educaproject.com
來訪問。這樣用戶就可以使用二級域名作為快捷方式快速訪問課程,也比較容易記憶該路徑。所有發往這個二級域名的請求,都會被重定向到實際的educaproject.com/course/django/
這個URL。
與視圖,模型,表單等組件一樣,中間件也可以寫在項目的任何位置。推薦在應用目錄內建立middleware.py
文件來編寫中間件。
在courses
應用目錄內創建middleware.py
文件,並編寫如下代碼:
from django.urls import reverse
from django.shortcuts import get_object_or_404, redirect
from .models import Course
def subdomain_course_middleware(get_response):
"""
為課程提供二級域名
"""
def middleware(request):
host_parts = request.get_host().split('.')
if len(host_parts) > 2 and host_parts[0] != 'www':
# 通過指定的二級域名查詢課程對象
course = get_object_or_404(Course, slug=host_parts[0])
course_url = reverse('course_detail', args=[course.slug])
# 將二級域名請求重定向至實際的URL
url = '{}://{}{}'.format(request.scheme, '.'.join(host_parts[1:]), course_url)
return redirect(url)
response = get_response(request)
return response
return middleware
當一個HTTP請求進來的時候,這個中間件執行如下任務:
- 取得這個HTTP請求中的域名,然后將其分割成幾部分;例如
mycourse.educaproject.com
會被分割得到一個列表['mycourse', 'educaproject', 'com']
- 檢查這個域名是否包含二級域名,判斷分割后的域名是否包含多於2個元素。如果包含,就取出第一個元素也就是二級域名,如果這個域名不是www,那就通過根據
slug
查詢並取得該課程對象。 - 如果找不到對應的課程,就返回404錯誤;如果找到了,就重定向到課程對象對應的規范化URL。
編輯settings/base.py
,把自定義中間件添加到MIDDLEWARE
設置中:
MIDDLEWARE = [
# ......
'courses.middleware.subdomain_course_middleware',
]
還需要看一下ALLOWED_HOSTS
中的域名設置,這里我們將其設置為可以是任何eduproject.com
的二級域名:
ALLOWED_HOSTS = ['.educaproject.com']
ALLOWED_HOSTS
中以一個.
開始的域名,例如.educaproject.com
,會匹配educaproject.com
及所有的educaproject.com
的二級域名,比如course.educaproject.com
和django.educaproject.com
。
2.3配置NGINX的二級域名
編輯config/nginx.conf
,將以下這行:
server_name www.educaproject.com educaproject.com;
修改成:
server_name *.educaproject.com educaproject.com;
通過增加通配符設置,讓NGINX也可以代理所有的二級域名,為了測試中間件,還必須在etc/hosts
中配置相關內容,比如如果要測試二級域名django.educaproject.com
,需要增加一行:
127.0.0.1 django.educaproject.com
然后啟動站點到https://django.educaproject.com/,可以發現中間件現在將其重定向到 https://educaproject.com/course/django/
3實現自定義的管理命令
Django允許應用向manage.py
管理工具中注冊自定義的管理命令。所謂管理命令就是通過manage.py
使用的指令,例如,我們曾經使用在第9章使用過makemessages
和compilemessages
命令。
一個管理命令由一個Python模塊組成,這個模塊里包含一個Command
類,這個Command
類繼承django.core.management.base.BaseCommand
或者BaseCommand
的子類。我們可以創建一個簡單的包含參數和選項的自定義命令。
對於每個在INSTALLED_APPS
內注冊的應用,Django會在應用目錄下邊的management/commands/
目錄下搜索管理命令,搜索到的每個命令模塊,都會被注冊成為一個同名的命令。
更多自定義管理命令的信息可以查看https://docs.djangoproject.com/en/2.0/howto/custom-management-commands/。
我們准備來創建一個提醒學生至少選一個課程的命令。這個命令會向所有已經注冊超過一定時間,但還沒有選任何一門課程的學生發送一封郵件。
在student
應用下建立如下的目錄和文件結構:
management/
__init__.py
commands/
__init__.py
enroll_reminder.py
編輯enroll_reminder.py
,添加下列代碼:
import datetime
from django.conf import settings
from django.core.management.base import BaseCommand
from django.core.mail import send_mass_mail
from django.contrib.auth.models import User
from django.db.models import Count
class Command(BaseCommand):
help = 'Sends an e-mail reminder to users registered more than N days that are not enrolled into any courses yet'
def add_arguments(self, parser):
parser.add_argument('--days', dest='days', type=int)
def handle(self, *args, options):
emails = []
subject = 'Enroll in a course'
date_joined = datetime.date.today() - datetime.timedelta(days=options['days'])
users = User.objects.annotate(course_count=Count('courses_joined')).filter(course_count=0,
date_joined__lte=date_joined)
for user in users:
message = "Dear {},\n\n We noticed that you didn't enroll in any courses yet. What are you waiting for?".format(
user.first_name)
emails.append((subject, message, settings.DEFAULT_FROM_EMAIL, [user.email]))
send_mass_mail(emails)
self.stdout.write('Sent {} reminders'.format(len(emails)))
這是enroll_reminder
命令,解釋如下:
Command
類繼承BaseCommand
類Command
類包含一個help
屬性,為命令提供幫助信息,運行python manage.py help enroll_reminder
就可以看到這段信息。add_arguments()
用來設置可用的參數,這里設置了--days
參數,指定其類型為整型。運行命令時這個參數用於指定天數,方便篩選出要向其發送郵件的學生。handle()
方法定義命令的實際業務邏輯。這里從命令行中獲取解析后的days
屬性,然后查詢注冊時間超過該天數的用戶,再通過分組計算這些用戶的選課數量,從中選出未選課的用戶。然后使用一個emails
列表記錄所有需要發送的郵件,最后通過send_mass_mail()
方法發送郵件,這樣可以使用一個SMTP鏈接發送大量郵件,而不用每發一次郵件就新開一個SMTP鏈接。
編寫好上述代碼后,打開系統命令行來運行命令:
python manage.py enroll_reminder --days=20
如果還沒有配置SMTP服務器,可以參考第二章中的內容。如果確實沒有SMTP服務器,可以在settings.py
中加上:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
以讓Django將郵件內容顯示在控制台而不實際發送郵件。
還可以通過系統讓這個命令每天早上8點運行,如果使用了基於UNIX的操作系統,可以打開系統命令行模式,輸入crontab -e
來編輯crontab
,在其中增加下邊這行:
0 8 * * * python /path/to/educa/manage.py enroll_reminder --days=20 --settings=educa.settings.pro
將其中的/path/to/educa/manage.py
替換成實際的manage.py
所在的絕對路徑。如果不熟悉cron的使用,可以參考http://www.unixgeeks.org/security/newbie/unix/cron-1.html。
如果使用的是Windows,可以使用系統的計划任務功能,具體可以參考https://docs.microsoft.com/zh-cn/windows/desktop/TaskSchd/task-scheduler-start-page。
還有一個方法是使用Celery定期執行任務。我們在第7章使用過Celery,可以使用Celery beat scheduler來建立定期執行的異步任務,具體可以參考https://celery.readthedocs.io/en/latest/userguide/periodic-tasks.html。
對於想通過cron或者Windows的計划任務執行的單獨腳本,都可以通過自定義管理命令的方式來進行。
Django還提供了一個使用Python執行管理命令的方法,可以通過Python代碼來運行管理命令,例如:
from django.core import management
management.call_command('enroll_reminder', days=20)
程序在執行到這里的時候,就會去運行這個命令。現在我們就可以為自己的應用定制管理命令並且計划運行了。
總結
這一章里使用uWSGI和NGINX配置完成了生產環境,還實現了自定義中間件和管理命令。
到這里本書已經結束。祝賀你,本書通過創建實際的項目和將其他軟件與Django集成的方式,指引你學習使用Django建立Web應用所需的技能。無論一個簡單的項目原型還是大型的Web應用,你現在都具備使用Django創建它們的能力。
祝你未來的Django之旅愉快!
其他感興趣的書
如果你發現本書很有用,你可能還會對下列書籍感興趣。