1. 程式人生 > 其它 >Django2實戰示例 第十三章 上線

Django2實戰示例 第十三章 上線

第十三章 上線

在上一章,為其他程式與我們的Web應用互動建立了RESTful API。本章將學習如何建立生產環境讓我們的網站正式上線,主要內容有:

  • 配置生產環境
  • 建立自定義中介軟體
  • 實現自定義管理命令

1建立生產環境

現在該將Django專案正式部署到生產環境中了。我們將按照下列步驟將站點部署到生產環境中:

  1. 為生產環境配置專案設定
  2. 使用PostgreSQL資料庫
  3. 使用uWSGI和NGINX建立web伺服器
  4. 管理靜態資源
  5. 使用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.py
  • local.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中的設定內容,然後寫了DEBUGDATABASES兩個設定,這兩個設定會覆蓋原來base.py中的設定,成為本檔案中的設定。由於DEBUG設定和DATABASES設定在每個配置檔案中都會修改,也可以將這兩個設定從base.py中刪除。

再來編輯settings/pro.py,如下所示:

from .base import *

DEBUG = False
ADMINS = (
    ('Antonio M', '[email protected]'),
)
ALLOWED_HOSTS = ['*']
DATABASES = {
    'default': {
    }
}

這是生產環境的配置檔案,來詳細看一下其中的內容:

  • DEBUG:設定DEBUGFalse是生產環境的強制要求。如果不關閉,會將錯誤跟蹤和敏感配置資訊洩露給所有人。
  • 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下安裝,需要Cygwinhttps://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設定瞭如下的內容:

  1. 使用educa.wsgi:application作為呼叫介面
  2. 載入生產環境的設定檔案
  3. 使用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
  • baseeduca專案的絕對路徑,將其替換成實際專案路徑

上邊定義的這兩個變數是自定義變數,還可以定義任意其他變數,只要不和內建的名稱衝突。接下來是具體設定的解釋:

  • 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請求的時候,發生如下事情:

  1. NGINX接收HTTP請求
  2. 如果請求靜態檔案,NGINX直接提供服務。如果請求動態頁面,NGINX通過SOCKET與uWSGI通訊,將請求轉交給uWSGI處理
  3. 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.comeducaproject.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/替換成你專案的實際staticmedia目錄的絕對路徑。這兩個引數解釋如下:

  • /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 protected]

這其中最關鍵的是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請求都必須被重定向到HTTPS
  • CSRF_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請求進來的時候,這個中介軟體執行如下任務:

  1. 取得這個HTTP請求中的域名,然後將其分割成幾部分;例如mycourse.educaproject.com會被分割得到一個列表['mycourse', 'educaproject', 'com']
  2. 檢查這個域名是否包含二級域名,判斷分割後的域名是否包含多於2個元素。如果包含,就取出第一個元素也就是二級域名,如果這個域名不是www,那就通過根據slug查詢並取得該課程物件。
  3. 如果找不到對應的課程,就返回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.comdjango.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章使用過makemessagescompilemessages命令。

一個管理命令由一個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之旅愉快!

其他感興趣的書

如果你發現本書很有用,你可能還會對下列書籍感興趣。

Python Programming Blueprints

Django RESTful Web Services