上线

在上一章,建立了RESTful API。作为本书的最后一章,将学习如何建立生产环境让我们的网站正式上线,主要内容有:

  • 配置生产环境
  • 创建自定义中间件
  • 实现自定义管理命令

建立生产环境

建立生产环境是本章的第一个主要课题。之前我们编写的四个项目都是在测试服务器上,现在该将Django站点正式部署到生产环境中了。

我们将遵循下列步骤将站点部署到生产环境中:

  1. 为生产环境修改项目设置
  2. 使用PostgreSQL数据库
  3. 使用uWSGI和NGINX建立web服务器
  4. 管理静态资源
  5. 使用SSL加强站点安全管理

为不同环境管理不同配置

在实际的项目中,很可能要面对不同的环境。一般至少有一个本地开发环境和一个生产环境,也可能有其他环境比如测试环境,预上线环境等。对于不同的环境,有些设置是通用的,有些则因环境而异。

让我们将项目设置为可以适合不同环境,又可以保证项目结构不会被改变。

在educa/educa/目录下建立settings目录(包),与settings.py同级,将settings.py文件重命名为base.py然后移动到settings目录中来,再建立其他文件,该目录和文件如下所示:

settings/
    __init__.py
    base.py
    local.py
    pro.py
    These

这些文件用途如下:

  • 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中的设置内容,然后写了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。

打开系统shell输入:

export DJANGO_SETTINGS_MODULE=educa.settings.pro

这条命令会为当前的会话窗口设置DJANGO_SETTINGS_MODULE环境变量。如果不想每次运行shell都执行一遍,可以把这条命令加入到shell配置文件如 .bashrc 或者 .bash_profile中。

如果不想对shell进行任何设置,那么在启动站点的时候必须加上–settings参数,如下:

python manage.py migrate --settings=educa.settings.pro

现在我们就为多环境配置做好了基础设置。

使用PostgreSQL数据库

在整本书中,我们大部分都使用了Python自带的SQLite数据库,只要在博客全文检索的时候推荐使用了PostgreSQL数据库。SQLite轻量而且易于使用,但对于生产环境而言太过简陋,必须需要一个更强力的数据库比如PostgreSQL和MySQL或者Oracle。

PostgreSQL的安装在第三章中已经介绍过,不再赘述。

让我们为我们的应用建立一个PostgreSQL用户,打开系统shell输入如下命令:

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,需要修改配置文件和修改认证方式为md5或者trust,建议网上搜寻官方文档和各种安装教程进行配置。

部署前检查项目

Django提供了一个check命令,可以在任何时候检查项目。通常检查过程包括检查所有注册的应用,输出所有错误和警告信息。如果包含 –deploy参数,还会额外执行针对生产环境的检查。

打开shell然后输入如下命令进行检查:

python manage.py check --deploy

作者这里遗漏了配置文件的路径,应该写成 python manage.py check --deploy --settings=educa.settings.***,其中***为base,local或pro

如果站点编写正确的话,会看到没有错误输出,但是会有一些警告信息。这说明站点通过了检查,但这些警告信息应该得到处理,以让站点更加安全。本书不会深入这里的内容,但是要记得在正式部署之前一定要进行部署前检查。

通过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

安装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/找到。

配置uWSGI

可以通过命令行配置uWSGI,打开shell,进入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. 使用virtual env设置的虚拟环境,注意将 /home/env/educa/ 替换为实际的虚拟环境所在路径。如果未使用虚拟环境,该配置可以不填。

如果不是在项目目录内执行的上述命令,需要额外加一个参数 --chdir=/path/to/educa/,其中的 /path/to/educa/替换成educa的项目路径。

执行完上述的uwsgi加后边一长串命令之后,就可以通过浏览器访问 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。命令行模式中我们使用的–http 127.0.0.1:8000指的是让uWSGI自己接受HTTP请求并自己负责路由这些请求。

我们需要把uWSGI作为socket启动,因为后边我们要使用NGINX作为我们的web服务器,NGINX通过socket与uWSGI进行通信。

关于uWSGI的详细设置可以看 https://uwsgi-docs.readthedocs.io/en/latest/Options.html

先通过ctrl-C停止现在运行的uWSGI,然后在项目的根目录下,通过简单很多的命令来重新运行uWSGI:

uwsgi --ini config/uwsgi.ini

这样运行之后,无法通过浏览器访问 http://127.0.0.1:8000/ 因为此时uWSGI以socket方式运行,我们还需要继续完善生产环境配置。

安装NGINX

当启动一个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

正常情况下在启动页面之后,直接访问本机IP地址,可以看到NGINX欢迎页面。看到配置成功后可以先停用NGINX服务,会更改其设置。

生产环境

下面的图表示了我们最终配置的生产环境的结构:

当一个浏览器发起一个HTTP请求的时候,发生如下事情:

  1. NGINX接收HTTP请求
  2. 如果请求静态文件,NGINX直接提供服务。如果请求动态页面,NGINX通过SOCKET与uWSGI通信,将请求转交给uWSGI处理
  3. uWSGI将请求转交给Django后端进行处理,返回的响应被传递给NGINX,NGINX再发回给浏览器。

配置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转发给educa socket,也就是uWSGI处理。还把NGINX自带的关于和uwsgi协同工作的参数设置也包含进去。

NGINX还有很多复杂的设置,文档可以参考 https://nginx.org/en/docs/

NGINX主要的设置文件位于 /etc/nginx/nginx.conf。还可以使用在/etc/nginx/sites-enabled/下找到的配置文件。为了让NGINX使用我们刚才编写的配置文件,打开shell建立一个软连接:

sudo ln -s /home/projects/educa/config/nginx.conf /etc/nginx/sites-enabled/educa.conf

将其中的 /home/projects/educa/替换成实际的绝对路径。注意,这里如果没有/sites-enabled/目录,要先手工建立。

如果还没有运行uWSGI,打开shell窗口,在educa项目根目录先运行uWSGI:

uwsgi --ini config/uwsgi.ini

当前shell会被uWSGI占用,再开一个shell,然后执行:

service nginx start

由于我们使用了自定义的域名,还必须修改/etc/hosts,添加如下两行:

127.0.0.1 educaproject.com
127.0.0.1 www.educaproject.com

这样我们就把这两个域名都路由到127.0.0.1上,由于我们是从本机访问本机,所以要更改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的更多信息可以看官方文档

让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以便让NGINX重新载入配置文件:

service nginx reload

Centos 7 采用命令systemctl来停止和重启服务。

在浏览器中打开 http://educaproject.com/ ,现在可以看到整个站点包含静态资源都正确的显示了。对于站点的静态文件请求,NGINX将绕开uWSGI,把文件直接返回给浏览器。

现在生产环境就初步配置完毕。整个站点现在可以说运行在生产环境之下了。

这里做一个简单的回顾,要开机以后让我们的站点运行起来,需要如下步骤:

  1. 将站点所需的Memcached和NGINX服务都设置为开机启动
  2. 启用站点对应的virtual env环境
  3. 到educa项目根目录下执行 uwsgi --ini config/uwsgi.ini

然后就可以通过设置的域名 或者 IP地址来访问站点了。再也不需要讨厌的8000端口啦。

需要修改站点时先停止uwsgi服务,修改完成之后再启动uwsgi。

使用SSL安全连接

在配置完初步的生产环境之后,下一个话题是站点的安全性。Secure Sockets Layer现在逐渐成为提供Web安全连接服务的规范。

使用较新版Chrome浏览器浏览我们的开发环境网站时,会发现在地址栏开头提示该网站不安全。这是因为我们的网站没有使用HTTPS协议。强烈建议对于正式的网站使用HTTPS协议,现在就在NGINX中配置SSL认证来让站点变得更加安全。

建立一个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证书。

配置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的验证信息。

现在重新启动NGINX服务,访问 https://educaproject.com/ ,会看到类似如下提示:

这个提示因浏览器而异。意思是警告当前站点并没有使用一个值得信任的验证方式,浏览器无法确定该站点安全与否。这是因为我们使用的SSL证书是由我们自行签发的,而不是从一个受信任的机构(Certification Authority)获得的证书。

当我们有了实际的公开域名之后,就可以向一个受信任的证书颁发机构申请一个SSL证书,这样浏览器就能识别该站点的HTTPS认证。

如果想为实际的站点申请证书,可以使用Linux基金会 Linux Foundation的Let’s Encrypt项目。这是一个致力于免费获得和更新SSL证书的计划,该计划的站点在 https://letsencrypt.org/

点击 “Add Exception” 按钮可以让浏览器知道可以信任该站点,这时浏览器的显示可能如下:

点击小锁按钮,就可以看到SSL的详细信息。这里也因浏览器而异,有的浏览器依旧会提示证书不可信或者存在问题,这是正常的。

配置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服务的生产环境。

自定义中间件

在之前我们已经了解了中间件的设置 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进阶-中间件以及官方文档

建立自定义中间件

我们来建立一个自定义中间件,用于通过一个自定义的二级域名来访问课程资源。例如:某个显示课程的URL:https://educaproject.com/course/django/,可以通过django.educaproject.com来访问。

这样用户就可以使用二级域名快速访问课程,也比较容易记忆该路径。所有发往这个二级域名的请求,都会被重定向到实际的educaproject.com/course/django/这个URL。

与视图,模型,表单,templatetag等组件一样,中间件也可以写在项目的任何位置。推荐在应用目录内建立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):
    """
    Provides subdomains for courses
    """

    def middleware(request):
        host_parts = request.get_host().split('.')
        if len(host_parts) > 2 and host_parts[0] != 'www':
            # get course for the given subdomain
            course = get_object_or_404(Course, slug=host_parts[0])
            course_url = reverse('course_detail', args=[course.slug])
            # redirect current request to the course_detail view
            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,那就通过反向代理取得对应course的URL。
  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.com 和

django.educaproject.com。

配置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://slug.educaproject.com/ ,将slug替换成实际某个课程的slug,就可以发现中间件现在将其重定向到 https://educaproject.com/course/slug/

实现自定义的管理命令

Django允许应用向manage.py管理工具中注册自定义的管理命令。例如,我们曾经使用在第9章使用过makemessages 和 compilemessages 命令。

一个管理命令包含由一个Python模块组成,这个模块里包含一个Command类,这个Command类继承 django.core.management.base.BaseCommand 或者BaseCommand的子类。可以建立一个简单的包含参数和选项的自定义命令。

对于每个注册的应用,Django会在应用目录下边的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链接。

编写好上述代码后,打开shell来运行命令:

python manage.py enroll_reminder --days=20

如果还没有配置SMTP服务器,可以参考第二章中的内容。如果确实没有SMTP服务器,可以在settings.py中加上:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

以让Django将邮件内容显示在控制台而不实际发送邮件。

还可以通过系统让这个命令每天早上8点运行,如果使用了基于 UNIX 的操作系统,可以打开shell,输入 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的使用,可以参考其文档

如果使用的是Windows,可以使用系统的计划任务功能,具体可以参考这里

还有一个方法是使用Celery定期执行任务。我们在第7章使用过Celery,可以使用Celery beat scheduler来建立定期执行的异步任务,具体可以参考这里

对于想通过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

Django Design Patterns and Best Practices – Second Edition

Building Django 2.0 Web Applications