Django 3网页开发指南第4版 第1章 Django 3.0入门

本文完整目录请见 Django 3网页开发指南 - 第4版

本章包含如下主题:

  • 使用虚拟环境
  • 创建项目文件结构
  • 通过pip处理项目依赖
  • 为开发、测试、预发布和生产环境配置设置
  • 在设置中定义相对路径
  • 处理敏感信息设置
  • 在项目中包含外部依赖
  • 动态设置STATIC_URL
  • 为MySQL配置设置UTF-8为默认编码
  • 创建 Git ignore文件
  • 删除Python编译文件
  • 在Python文件中重视导入顺序
  • 创建应用配置
  • 定义可重写的app设置
  • 使用Docker容器处理Django, Gunicorn, Nginx和PostgreSQL

引言

本章中我们将通过几个实例来使用Python 3通过Django 3.0开启新的项目。我们选取了最有效的方法来处理可扩展项目布局、配置,使用virtualenv或Docker来管理项目皆可。

读者应当已熟悉Django基础知识、Git版本控制、MySQL和PostgreSQL数据以及命令行的使用。应当使用基于Unix的操作系统,如macOS或Linux。使用基于Unix的系统平台来开发Django会更有意义,因为大多情况下项目会在Linux服务器上进行发布,通过这种方式不论对开发还是部署都可以建立日常的工作习惯。如果读者使用Windows来在本地开发,很多工作方式相同,但同时会存在一些差别。

不论在什么本地平台中的开发环境中使用Docker,都可以通过部署提升应用的可移植性,因为Docker容器的环境可以与部署服务器保持精准一致。还应说明不论是否使用 Docker 来进行开发,本章中的各小节都要求读者在本地机器上安装了相应的版本控制系统和数据库服务。

技术要求

使用本书中的代码,读者需要有最新稳定版本的Python,可通过https://www.python.org/downloads/进行下载。在编写本书时,最新版本为3.8.X.。还需要有MySQL或PostgreSQL数据库。可以通过https://dev.mysql.com/downloads/下载MySQL数据库服务,通过https://www.postgresql.org/download/下载PostgreSQL数据库服务。其它要求会在具体小节中说明。

第一章的所有代码可在 GitHub仓库的Chapter01目录中进行查看。

使用虚拟环境

你很有可能会在自己的电脑上开发多个Django项目。有些模块如 virtualenv、setuptools、wheel或Ansible可以只安装一次并在所有项目中共享。另外一些模块,如Django、第三方Python库和Django应用则需要相互分离。virtualenv工具是一个可以将不同Python项目进行分离成相互独立环境的工具。本小节中我们就来学习如何使用它。

准备工作

管理Python包需要使用pip。如果读者使用的是Python 3.4以上版本,在安装Python时就已经自带了。如果使用的是其它版本的Python,需要根据https://pip.readthedocs.io/en/stable/installing/中的安装指南执行pip的安装。下面我们来升级共享Python模块、pip、setuptools和wheel:

$ sudo pip3 install --upgrade pip setuptools wheel

虚拟环境从Python 3.3开始就已进行内置了。

如何实现...

一旦安装了所要求的软件,选择一个位置(如家目录)保存所有的Django项目。在创建好目录后执行如下步骤:

  1. 进入新建的目录、创建使用共享系统级包的虚拟环境:
$ cd ~/projects
    $ mkdir myproject_website 、
    $ cd myproject_website
    $ python3 -m venv env
  1. 要使用新建的虚拟环境,需要在当前shell中启动激活脚本。可通过如下命令来执行:
$ source env/bin/activate
    
  1. 根据所使用的shell的不同,可能会无法使用 source命令。另一种方式是通过如下命令,可以实现同样的效果(注意在点号和env中间有一个空格) :
$ . env/bin/activate
  1. 这时可以看到命令行提示符中有一个项目名称的前缀,如下所示:
(env)$
  1. 要退出虚拟环境,使用如下命令:
(env)$ deactivate
    

实现原理...

在创建虚拟环境时,会创建一些特定的目录(bin, include及lib) 来存储Python安装版本的拷贝,还会定义一些共享Python路径。在启用虚拟环境后,通过pip或easy_install安装的包都会放到虚拟环境的站点包中以供使用,而不会放到原来安装的Python的全局站点包中。

要在虚拟环境中安装Django 3.0.x,输入如下命令:

(env)$ pip install "Django~=3.0.0"

其它内容

  • 创建项目文件结构一节
  • 使用Docker容器处理Django, Gunicorn, Nginx和PostgreSQL一节
  • 第12章 部署中的在预发布环境通过mod_wsgi部署Apache一节
  • 第12章 部署中的在生产环境通过mod_wsgi部署Apache一节
  • 第12章 部署中的在生产环境中基于Nginx和Gunicorn部署一节

创建项目文件结构

项目中连贯性的文件结构可以让组织上更清晰、也更具生产力。在定义了基础工作流时,可以更快速地将精力放在业务逻辑上、创建优秀的项目。

准备工作

如果还没照着前面操作,请创建一个~/projects目录,在其中存放所有的Django项目(在使用虚拟环境一节中有相应的说明)。

然后为具体的项目创建一个目录,例如myproject_website。启动env目录中的虚拟环境。像前一小节中那样激活环境并安装Django。我们推荐为与项目相关的shell脚本添加一个commands目录,为数据库导出创建一个db_backups目录,为网站设计文件添加一个mockups目录,最重要的是为Django项目添加一个src目录。

如何实现...

按照如下步骤来为项目创建文件结构:

  1. 先启动虚拟环境,进入src目录启动一个新的Django项目,如下:
(env)$ django-admin.py startproject myproject
所执行命令会创建一个名为myproject的目录,其中存放项目文件。目录中包含一个名称同样为myproject的Python模块。为避免混淆、保持方便,我们将顶级目录重命名为django-myproject。这个目录会进行版本控制,其中应包含.git或相应的子目录。
  1. 在django-myproject目录中,创建一个README.md文件来描述通过django-admin.py startproject myproject所创建的项目。
  2. django-myproject目录中还将包含如下内容:
    • 项目的Python包,名为myproject
    • Django框架的项目pip需求文件以及其它外部依赖(阅读通过pip处理项目依赖一节)
    • LICENSE文件中的项目证书。如果项目开源,可以通过https://choosealicense.com/选择一种通用证书。
  3. 在项目的根目录django-myproject,创建如下目录
    • 用于上传的media目录
    • 用于存放静态文件的static目录
    • 用于项目翻译的locale目录
    • 用于在使用pip安装项目所包含外部依赖的externals目录
  4. myproject目录中包含如下目录和文件:
    • apps目录中存放项目内建的Django应用。推荐为项目共享功能创建名为core或utils的应用。
    • 用于项目设置的settings目录(参见为开发、测试、预发布和生产环境配置设置一节)
    • 用于项目特有静态文件的site_static目录。
    • 用于项目HTML模板的templates目录。
    • 用于项目URL配置的 urls.py 文件。
    • 用于项目web服务配置的wsgi.py文件。
  5. 在site_static目录中,为具体站点静态文件创建site目录来作为命名空间。然后,我们在其中分类的子目录之前分割静态文件。例如,像下面这样:
    • 用于Sass文件的scss(可选)
    • 用于所生成最小化层叠样式表(CSS)的css
    • 用于样式图片、favicon和logo的img
    • 用于项目JavaScript的js
    • 用于组合各种类型文件的第三方模块的vendor,如TinyMCE富文本编辑器
  6. 除site目录外,site_static中也可能包含重写的第三方应用的静态文件目录,例如,可能会包含重写Django CMS中静态文件的cms。要从Sass生成CSS文件、最小化JavaScript文件,可以使用带有图形化界面的CodeKitPrepros 应用。
  7. 将由应用分隔的模板文件放到templates目录中。如果模板文件代表一个页面(如change_item.html 或 item_list.html),那么将其放到应用的模板目录中。如果模板中包含了另一个模板(如similar_items.html),将其放到includes子目录中。同时模板目录可对全局可复用脚本包含一个名为utils的目录,如翻页和语言选择。

实现原理...

项目的整体文件结构类似下面这样:

myproject_website
├── commands/
├── db_backups/
├── mockups/
├── src/
│   └── django-myproject/
│       ├── externals/
│       │   ├── apps/
│       │   │   └── README.md
│       │   └── libs/
│       │       └── README.md
│       ├── locale/
│       ├── media/
│       ├── myproject/
│       │   ├── apps/
│       │   │   ├── core/
│       │   │   │   ├── __init__.py
│       │   │   │   └── versioning.py
│       │   │   └── __init__.py
│       │   ├── settings/
│       │   │   ├── __init__.py
│       │   │   ├── _base.py
│       │   │   ├── dev.py
│       │   │   ├── production.py
│       │   │   ├── sample_secrets.json
│       │   │   ├── secrets.json
│       │   │   ├── staging.py
│       │   │   └── test.py
│       │   ├── site_static/
│       │   │   └── site/
│       │   │       ├── css/
│       │   │       │   └── style.css
│       │   │       ├── img/
│       │   │       │   ├── favicon-16x16.png
│       │   │       │   ├── favicon-32x32.png
│       │   │       │   └── favicon.ico
│       │   │       ├── js/
│       │   │       │   └── main.js
│       │   │       └── scss/
│       │   │           └── style.scss
│       │   ├── templates/
│       │   │   ├── base.html
│       │   │   └── index.html
│       │   ├── __init__.py
│       │   ├── urls.py
│       │   └── wsgi.py
│       ├── requirements/
│       │   ├── _base.txt
│       │   ├── dev.txt
│       │   ├── production.txt
│       │   ├── staging.txt
│       │   └── test.txt
│       ├── static/
│       ├── LICENSE
│       └── manage.py*
└── venv/

扩展知识...

要加速按刚刚所描述的方式创建项目,可以使用https://github.com/archatas/django-myproject框架模板。下载代码后,执行全局搜索并将myproject替换为对你有意义的名称,这时就可以开始使用了。

其它内容

  • 通过pip处理项目依赖一节
  • 在项目中包含外部依赖一节
  • 为开发、测试、预发布和生产环境配置设置一节
  • 第12章 部署中的在预发布环境通过mod_wsgi部署Apache一节
  • 第12章 部署中的在生产环境通过mod_wsgi部署Apache一节
  • 第12章 部署中的在预发布环境中基于Nginx和Gunicorn部署一节
  • 第12章 部署中的在生产环境中基于Nginx和Gunicorn部署一节

通过pip处理项目依赖

安装和管理Python包最便捷的工具是pip。不需要一个个地安装每个包,可以将所要安装的包列表定义到文本文件中。将文件传递给pip工具,然后它就会自动处理列表中所有包的安装。这种方法的另一个好处是可以对包列表进行版本控制。

通常来说,有一个匹配生产环境的依赖文件就足够了。可以在开发机器上修改版本或添加、删除依赖,然后通过版本控制来进行管理。这种方式通过一组依赖(及其关联的修改)迁移到另一组依赖就和切换分支一样简单。

在某些情况下,环境差异太大,项目会需要有至少两个实例。

  • 开发环境,用于创建新功能
  • 在托管主机上的对外网站环境,通常称为生产环境

可能会有针对其它开发者的开发环境,或是在生产环境中使用但生产环境用不到的特殊工具。也可能会有测试及预发布环境,用于在本地和对外网站设置环境中进行测试。

为保持良好的可维护性,应当能够在开发、测试、预发布和生产环境中安装所需要的Python模块。有些模块是共享的,另一些模块仅在某些环境中使用。本节中,读者将学习到如何对多环境组织项目依赖并通过pip进行管理。

准备工作

在继续本小节之前,需要准备好Django项目、安装了pip并启动虚拟环境。更多操作细节,参见使用虚拟环境一节。

如何实现...

执行如下步骤来为虚拟环境中的Django项目准备好pip依赖:

  1. 我们进入处于版本控制下的Django项目,创建requirements目录并包含如下文本文件:
    • 用于共享模块的_base.txt
    • 用于开发环境的dev.txt
    • 用于测试环境的 test.txt
    • 用于预发布环境的staging.txt
    • 用于生产环境的production.txt
  2. 编辑_base.txt并逐行添加在各个环境中所共享的Python模块:
# requirements/_base.txt
    Django~=3.0.4
    djangorestframework
    -e git://github.com/omab/python-social-auth.git@6b1e301c79#egg=python-social-auth
    
  1. 如果特定环境的依赖和_base.txt中的相同,在该环境的依赖文件中添加一行包含_base.txt,如下例所示:
# requirements/production.txt
    -r _base.txt
    
  1. 如果环境中有特殊的依赖,在包含_base.txt之后添加这些依赖,如以下代码所示:
# requirements/dev.txt
    -r _base.txt 
    coverage 
    django-debug-toolbar 
    selenium
    
  1. 可以在虚拟环境中运行如下命令来安装开发环境(或对其它环境使用相就的命令)所需的依赖,如下:
(env)$ pip install -r requirements/dev.txt
    

实现原理...

前面的pip install命令,不论指定虚拟环境还是在全局执行,会通过requirements/_base.txt 和 requirements/dev.txt安装所有的项目依赖。可以看到,我们可以指定Django框架所需模块的版本,甚至是Git仓库中指定的提交(commit)来进行安装,示例中对python-social-auth就是这么做的。

在项目中有多条依赖时,好的做法是控制使用固定的一些Python模板发行版本。这样就不用担心在依赖升级时影响到项目的完整度,因为升级可能会导致冲突或向后兼容性的问题。在部署项目或移交项目给新开发者时这尤为重要。

如果已经通过pip逐一手动进行了项目依赖的安装,可以在虚拟环境中通过如下命令生成requirements/_base.txt文件:

(env)$ pip freeze > requirements/_base.txt

扩展知识...

要想保持简化,可以对所有环境使用相同的依赖,可以通过定义为所有依赖只生成一个文件requirements.txt,如下所示:

(env)$ pip freeze > requirements.txt

要在新的虚拟环境中安装这些模块,仅需使用如下命令:

(env)$ pip install -r requirements.txt

如果需要通过其它版本控制系统或本地路径安装Python库,可以通过官方文档学习pip的更多用法。

另一种管理Python依赖的方法是越来越流行的Pipenv。可以通过https://github.com/pypa/pipenv进行了解。

其它内容

  • 使用虚拟环境一节
  • 使用Docker容器处理Django, Gunicorn, Nginx和PostgreSQL一节
  • 在项目中包含外部依赖一节
  • 为开发、测试、预发布和生产环境配置设置一节

为开发、测试、预发布和生产环境配置设置

前面也提到了,在开发环境中创建新功能、在测试环境中进行测试,然后将网站放到预发布服务器上让人们试用新功能。随后将网站部署到生产服务器上对公众开放。每个环境都可能会有特有的设置,本小节我们将学习如何组织这些设置。

准备工作

在Django项目中,我们将为每个环境创建设置文件:包括开发、测试、预发布还有生产。

如何实现...

按照如下步骤来配置项目设置:

  1. 在myproject目录中,创建settings Python模块,包含如下文件:
    • init.py 让settings目录成为一个Python模块
    • _base.py用作共享设置
    • dev.py用于开发设置
    • test.py用于测试设置
    • production.py用于生产设置
  2. 将在启动新Django项目时自动创建的settings.py中的内容拷贝到settings/_base.py中。然后删除settings.py
  3. 修改settings/_base.py中的BASE_DIR来指向上一级。一开始像下面这样:
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
修改后长下面这样:
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  1. 如果环境的设置与共享设置一致,那么只需要通过_base.py导入即可,如下所示:
# myproject/settings/production.py
    from ._base import *
  1. 在其它文件中对特定的环境应用想要添加或重写的设置,例如,开发环境中的设置应放在dev.py中,如下面的代码片断所示:
# myproject/settings/dev.py
    from ._base import *
    EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
  1. 修改manage.py 和myproject/wsgi.py文件来默认使用其中一种环境的设置,所修改的代码行如下:
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
  1. 应当将该行内容修改如下:
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings.production')

实现原理...

默认Django管理命令使用myproject/settings.py中的设置。通过本小节中所定义的方法,可以将所有环境的非敏感设置放到config目录中进行版本控制。另一方面,settings.py文件本身会被版本控制所忽略,并且仅包含当前开发、测试、预发布或生产环境所必需的设置。

ℹ️对于每种环境,推荐在PyCharm的设置、env/bin/activate脚本或.bash_profile中单独设置DJANGO_SETTINGS_MODULE环境变量

其它内容

  • 使用Docker容器处理Django, Gunicorn, Nginx和PostgreSQL一节
  • 处理敏感信息设置一节
  • 在设置中定义相对路径一节
  • 创建 Git ignore文件一节

在设置中定义相对路径

Django要求你在设置中定义各种文件路径,如媒体文件的根路径、静态文件的根路径、模板路径以及翻译文件路径。对项目的每个开发者,路径可以不同,因为可在任何地方设置虚拟环境并且用户可能会使用macOS、Linux或Windows。即使使用Docker容器来封装项目,定义绝对路径也会降低可维护性和可移植性。在各种用例中,都有动态定义这些路径的方式,让它们成为Django项目目录的相对位置。

准备工作

启动Django项目并打开settings/_base.py。

如何实现...

相应地修改路径相关的设置,而不是硬编码本地目录,如下:

# settings/_base.py
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# ... 
TEMPLATES = [{
# ... 
    DIRS: [
        os.path.join(BASE_DIR, 'myproject', 'templates'), 
    ],
# ... 
}]
# ... 
LOCALE_PATHS = [
    os.path.join(BASE_DIR, 'locale'), 
]
# ... 
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'myproject', 'site_static'), 
]
STATIC_ROOT = os.path.join(BASE_DIR, 'static') 
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

实现原理...

默认Django设置包含一个BASE_DIR值,它是包含manage.py目录的绝对路径(通常比settings.py高一级或比settings/_base.py高两级)。然后我们使用os.path.join()函数来设置所有路径相对BASE_DIR的位置。

根据我们在创建项目文件结构一节中所确定的目录布局,我们将为前面的一些示例插入myproject来作为中间路径块,因为相关的文件夹在其中进行的创建。

其它内容

  • 创建项目文件结构一节
  • 使用Docker容器处理Django, Gunicorn, Nginx和PostgreSQL一节
  • 在项目中包含外部依赖一节

处理敏感信息设置

在配置Django项目时,肯定会处理一些敏感信息,比如密码和API密钥。不推荐将这些信息放在版本控制中。有两种存储这类信息的方式:放在环境变量中或单独不进行追踪的文件中。本小节中我们就来探讨这两种方式。

准备工作

项目的大部分设置都在所有环境中共享并存储在版本控制中。这些可以直接放在设置文件中进行定义;但有一些针对项目实例特定环境或是需要安全强化的敏感信息,如数据库或email。下面我们来探讨使用环境变量来暴露这些信息。

如何实现...

要从环境变量中读取敏感信息,可以执行如下步骤:

  1. 在settings/_base.py的开头,定义如下的get_secret() 函数:
# settings/_base.py
    import os
    from django.core.exceptions import ImproperlyConfigured

    def get_secret(setting):
      """Get the secret variable or return explicit exception.""" 
      try:
        return os.environ[setting] 
      except KeyError:
          error_msg = f'Set the {setting} environment variable' # 译者注:f-string 用法在3.6版本开始引入 
          raise ImproperlyConfigured(error_msg)
  1. 然后,在需要定义敏感值时,使用 get_secret() 函数,如下例所示:
SECRET_KEY = get_secret('DJANGO_SECRET_KEY')

    DATABASES = { 
      'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2', 
        'NAME': get_secret('DATABASE_NAME'),
        'USER': get_secret('DATABASE_USER'),
        'PASSWORD': get_secret('DATABASE_PASSWORD'), 
        'HOST': 'db',
        'PORT': '5432',
      }
    }

实现原理...

如果在运行Django管理命令时使用了未定义的环境变量,会看到抛出的报错信息,如 Set the DJANGO_SECRET_KEY environment variable(请设置DJANGO_SECRET_KEY环境变量)。

可以在PyCharm的配置、远程服务器配置控制台、env/bin/activate脚本、.bash_profile或直接像下面这样在Terminal中设置环境变量:

$ export DJANGO_SECRET_KEY="此处修改为50个字符的随机长字符串"
$ export DATABASE_NAME="myproject"
$ export DATABASE_USER="myproject"
$ export DATABASE_PASSWORD="修改为数据库密码"

注意对于所有密码、API密钥及其它需要在Django配置中使用的敏感信息都应使用get_secret() 函数。

扩展知识...

除环境变量外,我们还可以使用版本控制以外的文本文件来存储敏感信息。文件格式可以是YAML、INI、CSV或JSON,存放在硬盘的其它位置。例如,对于JSON文件,应当有一个像下面这样的 get_secret()函数:

# settings/_base.py
import os
import json

with open(os.path.join(os.path.dirname(__file__), 'secrets.json'), 'r') as f:
  secrets = json.loads(f.read())

def get_secret(setting):
  """Get the secret variable or return explicit exception.""" 
  try:
    return secrets[setting]
  except KeyError:
    error_msg = f'Set the {setting} secret variable' 
    raise ImproperlyConfigured(error_msg)

它会读取配置目录中的secrets.json 文件,该文件至少应当有如下结构:

{
  "DATABASE_NAME": "myproject",
  "DATABASE_USER": "myproject",
  "DATABASE_PASSWORD": "修改为数据库密码",
  "DJANGO_SECRET_KEY": "此处修改为50个字符的随机长字符串" 
}

确保在版本控制中忽略secrets.json文件,但为方便起见,可以创建一个带有空值的sample_secrets.json 文件并对其进行版本控制:

{
  "DATABASE_NAME": "",
  "DATABASE_USER": "",
  "DATABASE_PASSWORD": "",
  "DJANGO_SECRET_KEY": "此处修改为50个字符的随机长字符串" 
}

其它内容

  • 创建项目文件结构一节
  • 使用Docker容器处理Django, Gunicorn, Nginx和PostgreSQL一节

在项目中包含外部依赖

有时,我们无法使用pip来安装外部依赖,就需要将其直接放到项目中,如下面的用例:

  • 在你自己修复了第三方应用的bug或是添加了新功能但未被项目所有者所采纳时
  • 你需要使用通过Python包索引(PyPI)或公共版本控制库中无法访问到的私有应用时
  • 在你需要使用老版本的依赖而PyPI中不再包含时

在项目中包含外部依赖可以确保不论何时开发者对依赖进行升级,所有其他开发者都会在版本控制系统的下一次更新中收到升级后的版本。

准备工作

应在虚拟环境下启动Django项目。

如何实现...

对一个虚拟环境项目逐一执行如下步骤:

  1. 如尚未创建,在Django项目目录django-myproject下创建externals目录
  2. 然后在该目录下创建libs和apps这两个目录。libs目录用于项目所需用到的Python模块, 例如Boto、Requests、Twython和Whoosh。apps用于第三方Django应用,如Django CMS、Django Haystack和django-storages。
    我们强烈推荐在libs和apps目录中创建README.md文件,在其中描述各个模块的用途、版本或修订版,以及其来源。
  3. 目录结构类似下面这样:
externals/
    ├── apps
    │   ├── README.md
    │   ├── cms
    │   ├── haystack
    │   └── storages
    └── libs
        ├── README.md
        ├── boto
        ├── requests
        └── twython
  1. 下一步是将外部库和应用放到Python路径中,这样就可以像已安装过那样进行识别了。可通过在配置中添加如下代码来实现:
# settings/_base.py
    import os
    import sys
    BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    EXTERNAL_BASE = os.path.join(BASE_DIR, "externals") 
    EXTERNAL_LIBS_PATH = os.path.join(EXTERNAL_BASE, "libs")
    EXTERNAL_APPS_PATH = os.path.join(EXTERNAL_BASE, "apps")
    sys.path = ["", EXTERNAL_LIBS_PATH, EXTERNAL_APPS_PATH] + sys.path

实现原理...

模块应当放到Python路径下来让Python可以运行并导入该模块。将模块放到Python路径中的一种方式是在导入非常规位置中的模块前修改sys.path变量。像设置中指定的sys.path的值,是一个目录列表,其中的空字符串表示当前目录,随后是项目中的目录,最后是Python软件的全局共享目录。可以在Python shell中查看sys.path的值,如下:

(env)$ python manage.py shell 
>>> import sys
>>> sys.path

在尝试导入模块时,Python在列表中搜索模块并在查找到时返回每一个结果。

因此,我们首先定义了BASE_DIR变量,它是django-myproject的绝对路径或是比myproject/settings/_base.py高3级。然后,我们定义了变量EXTERNAL_LIBS_PATH和EXTERNAL_APPS_PATH,它们是BASE_DIR的相对路径。最后,我们修改了sys.path属性,在列表的开头添加新路径。注意我们还将空字符串添加为第一个路径用于搜索,表示在查找其它Python路径之前应总是先检查当前目录中的模块。

这种包含外部库的方式对于有C语言约束的Python包不具备跨平台性,比如lxml。对于这种依赖,我们推荐按照通过pip处理项目依赖一节中所介绍的方式来使用pip安装依赖。

其它内容

  • 创建项目文件结构一节
  • 使用Docker容器处理Django, Gunicorn, Nginx和PostgreSQL一节
  • 通过pip处理项目依赖一节
  • 在设置中定义相对路径一节
  • 第10章 锦上添花中的使用Django shell 一节

动态设置STATIC_URL

如果将STATIC_URL设置为静态值,那么在每次更新CSS文件、JavaScript文件或图片时,你和网站用户会需要清除浏览器缓存来让修改生效。有一个技巧可以避免清除浏览器缓存 ,那就是对STATIC_UR中L所显示的最新修改添加时间戳。不论何时修改代码,访客的浏览器会强制加载所有新的静态文件。

本小节中,我们将学习如何为Git用户在STATIC_URL中添加时间戳。

准备工作

确保项目处于Git的版本控制中并且已经在设置中定义了BASE_DIR,在设置中定义相对路径一节中进行过介绍。

如何实现...

将Git时间戳放到STATIC_URL设置中包含下面两个步骤:

  1. 如尚未创建,请在Django项目中创建myproject.apps.core应用。应在里面创建一个versioning.py文件:
# versioning.py
    import subprocess
    from datetime import datetime

    def get_git_changeset_timestamp(absolute_path): 
      repo_dir = absolute_path
      git_log = subprocess.Popen(
        "git log --pretty=format:%ct --quiet -1 HEAD", 
        stdout=subprocess.PIPE, 
        stderr=subprocess.PIPE,
        shell=True,
        cwd=repo_dir,
        universal_newlines=True, 
      )
      timestamp = git_log.communicate()[0] 
      try:
        timestamp = datetime.utcfromtimestamp(int(timestamp)) 
      except ValueError:
        # Fallback to current timestamp
        return datetime.now().strftime('%Y%m%d%H%M%S') 
      changeset_timestamp = timestamp.strftime('%Y%m%d%H%M%S') 
      return changeset_timestamp
  1. 在设置中导入新创建的get_git_changeset_timestamp()函数并对STATIC_URL进行使用,如下:
# settings/_base.py
    from myproject.apps.core.versioning import get_git_changeset_timestamp
    # ...
    timestamp = get_git_changeset_timestamp(BASE_DIR) 
    STATIC_URL = f'/static/{timestamp}/'
    

实现原理...

get_git_changeset_timestamp()接收absolute_path目录作为参数并通过该参数调用git log shell命令来显示目录中的HEAD修订版的Unix时间戳。我们将BASE_DIR传递给该函数,因为要确保它处于版本控制中。在解析时间戳时,转化为一个由所返回年、月、日、小时、分钟和秒组成的字符串,然后在STATIC_URL的定义中包含它。

扩展知识...

这种方法仅在每种环境包含项目的完整Git仓库,在某些情况下,比如使用Heroku或Docker来进行部署时,无法访问Git仓库和远程服务器中的git log 命令。这时要为STATIC_URL添加动态区块,则需要从文件文件中读取时间戳,例如每次提交时都会更新的myproject/settings/last-modified.txt。

这时,设置将包含如何内容:

# settings/_base.py
with open(os.path.join(BASE_DIR, 'myproject', 'settings', 'last-modified.txt'), 'r') as f:
  timestamp = f.readline().strip() 

STATIC_URL = f'/static/{timestamp}/'

可以通过预提交钩子(hook)来让Git仓库更新last-modified.txt。这一可执行bash脚本名称就为pre-commit并放在django-myproject/.git/hooks/目录下:

# django-myproject/.git/hooks/pre-commit
#!/usr/bin/env python
from subprocess import check_output, CalledProcessError
import os
from datetime import datetime

def root():
  ''' returns the absolute path of the repository root '''
  try:
    base = check_output(['git', 'rev-parse', '--show-toplevel']) 
  except CalledProcessError:
    raise IOError('Current working directory is not a git repository') 
  return base.decode('utf-8').strip()

def abspath(relpath):
  '''returns the absolute path for a path given relative to the root of the git repository'''
  return os.path.join(root(), relpath)

def add_to_git(file_path):
  ''' adds a file to git '''
  try:
    base = check_output(['git', 'add', file_path]) 
  except CalledProcessError:
    raise IOError('Current working directory is not a git repository') 
  return base.decode('utf-8').strip()

def main():
  file_path = abspath("myproject/settings/last-modified.txt")

  with open(file_path, 'w') as f: 
    f.write(datetime.now().strftime("%Y%m%d%H%M%S"))
  add_to_git(file_path)

if __name__ == '__main__': 
  main()

这个脚本会提交到Git仓库时更新last-modified.txt并将该文件添加到Git索引中。

其它内容

  • 创建 Git ignore文件一节

为MySQL配置设置UTF-8为默认编码

MySQL自称是最为流行的开源数据库。在本小节中,我们将讲解如何将其默认编码设置为UTF-8。注意如果你没有在数据库配置中设置编码,很可能在应用UTF-8编辑的数据使用的是LATIN1。这会在使用€这样的符号是出现数据库错误。本小节还省却你将LATIN1转化为UTF-8麻烦,尤其是在对一些表使用LATIN1而另一些表使用UTF-8编码时。

准备工作

确保安装了MySQL数据库管理系统及mysqlclient Python模块,并且在项目的设置中使用MySQL引擎。

如何实现...

使用你常用的编辑器打开MySQL配置文件/etc/mysql/my.cnf,确保下面的设置分别在[client]、[mysql]和[mysqld]版块中 ,如下:

# /etc/mysql/my.cnf
[client] 
default-character-set = utf8

[mysql] 
default-character-set = utf8

[mysqld]
collation-server = utf8_unicode_ci 
init-connect = 'SET NAMES utf8' 
character-set-server = utf8

如果以上有任何版块不存在,请自行在文件中添加。如果各版块已经存在,将设置添加到已有的配置中,并使用命令行工具重启MySQL,如下:

$ /etc/init.d/mysql restart

实现原理...

此时再新建MySQL数据时,所有的数据库和数据表默认都设置为UTF-8编码。别忘记在开发或发布项目的所有电脑上进行这一设置。

扩展知识...

在PostgreSQL中,默认服务器编码已经是UTF-8是,但如果你想要显式地使用UTF-8编码创建PostgreSQL数据库,那么可以使用如下命令来实现:

$ createdb --encoding=UTF8 --locale=en_US.UTF-8 --template=template0 myproject

其它内容

  • 创建项目文件结构一节
  • 使用Docker容器处理Django, Gunicorn, Nginx和PostgreSQL一节

创建 Git ignore文件

Git是最流行的分布式版本控制系统,你可能已经在Django项目中使用它了。虽然是对绝大多数文件追踪修改,但推荐将一些特定的文件及文件夹放在版本控制以外。通常缓存、已编译代码、日志文件及隐藏的系统文件都不应在Git仓库中跟踪记录。

准备工作

确保你的Django项目处于Git版本控制之中。

如何实现...

使用你喜欢的编辑器在Django项目的根目录下创建一个.gitignore文件,并在其中加入如下的文件和目录:

# .gitignore
### Python template
# Byte-compiled / optimized / DLL files 
__pycache__/
*.py[cod]
*$py.class

# Installer logs
pip-log.txt 
pip-delete-this-directory.txt

# Unit test / coverage reports 
htmlcov/
.tox/
.nox/
.coverage 
.coverage.* 
.cache 
nosetests.xml 
coverage.xml 
*.cover 
.hypothesis/ 
.pytest_cache/

# Translations 
*.mo
*.pot

# Django stuff: 
*.log 
db.sqlite3

# Sphinx documentation
docs/_build/

# IPython 
profile_default/ 
ipython_config.py

# Environments
env/

# Media and Static directories 
/media/
!/media/.gitkeep

/static/ 
!/static/.gitkeep

# Secrets 
secrets.json

实现原理...

.gitignore指定不由Git版本控制系统所追踪的模式。本小节中所创建的.gitignore文件会忽略掉Python编译文件、本地设置、采集的静态文件以及带有上传文件的媒体目录。

注意对媒体和静态文件有一些感叹号标记的例外语法:

/media/ 
!/media/.gitkeep

这告诉Git要忽略 /media/目录但保持使用版本控制追踪 /media/.gitkeep文件。因为Git版本控制仅追踪文件而不是目录,所以我们使用.gitkeep 来确保在每个环境中都会创建media目录,但不进行追踪。

其它内容

  • 创建项目文件结构一节
  • 使用Docker容器处理Django, Gunicorn, Nginx和PostgreSQL一节

删除Python编译文件

首次运行项目时,Python使用字节码编译文件.pyc来对所有的.py 代码进行编译,并在稍后执行时使用。通常在修改.py文件时,.pyc会重新进行编译;但有时在切换分支或移动目录时,需要手动清理掉这些编译文件。

准备工作

使用你喜欢的编辑器在家目录中编辑或创建一个.bash_profile文件。

如何实现...

  1. 在.bash_profile文件的最后添加这一别名,如下:
# ~/.bash_profile
   alias delpyc='
   find . -name "*.py[co]" -delete
   find . -type d -name "__pycache__" -delete'
  1. 此时要清理Python编译后文件,进入项目目录并在命令行中输入如下命令:
(env)$ delpyc

实现原理...

首先我们创建了一个Unix别名来搜索*.pyc 和 *.pyo文件以及pycache目录并在当前目录及其子目录中删除它们。在命令行工具中启动新会话时会执行.bash_profile文件。

扩展知识...

如果想要避免一起创建Python编译后文件,可以在.bash_profile、env/bin/activate 脚本或 PyCharm配置中设置一个环境变量PYTHONDONTWRITEBYTECODE=1。

其它内容

  • 创建 Git ignore文件

在Python文件中重视导入顺序

在创建Python模块时,一个好的实践是对文件结构保持连贯性。这会让你自己和其他开发人员在阅读代码时更为容易。本节展示如何架构导入文件。

准备工作

创建一个虚拟环境并在其中创建一个Django项目。

如何实现...

为你所创建的Python文件应用如下的结构。将导入分类成不同的版块,如下:

 # 系统库
 import os
 import re
 from datetime import datetime

# 第三方库
import boto
from PIL import Image

# Django模块
from django.db import models 
from django.conf import settings
from cms.models import Page

# 当前应用模块
from .models import NewsArticle 
from . import app_settings

实现原理...

对于导入我们有5个主要分类,如下:

  • 系统库:主要为Python默认安装的包
  • 第三方库:额外安装的Python包
  • Django模块:Django框架中的不同模块
  • Django应用:第三方和本地应用
  • 当前应用模块:当前应用的相对路径导入

扩展知识...

在Python和Django中进行编码时,使用Python代码的官方样式指南PEP 8。可以在https://www.python.org/dev/peps/pep-0008/中进行查看。

其它内容

  • 通过pip处理项目依赖
  • 在项目中包含外部依赖

创建应用配置

Django项目由多个称为应用(或更普遍地称为app)的Python模块所组成,它将不同的模块化功能进行拼接。每个应用可包含模型、视图、表单、URL配置、管理命令、数据库迁移、信号、测试、上下文处理器和middleware等等。Django框架有一个应用仓库,所有的应用和模型在这里汇集用于稍后的配置和审查。从Django 1.7开始在每个应用的AppConfig实例中保存有应用的元信息。我们来创建一个magazine示例应用,学习如何使用应用配置。

准备工作

可以通过调用startapp管理命令来创建Django应用或手动创建应用模块:

(env)$ cd myproject/apps/
(env)$ django-admin.py startapp magazine

创建好magazine应用之后,在models.py中添加一个NewsArticle模型、在admin.py中为该模型配置管理界面并在设置的INSTALLED_APPS中添加myproject.apps.magazine。如何对于这些操作你还不熟悉,请参照Django官方课程进行学习。

如何实现...

按照如下步骤来创建和使用应用配置:

  1. 修改apps.py文件并在其中插入如下内容:
# myproject/apps/magazine/apps.py
    from django.apps import AppConfig
    from django.utils.translation import gettext_lazy as _

    class MagazineAppConfig(AppConfig): 
      name = "myproject.apps.magazine" 
      verbose_name = _("Magazine")

      def ready(self):
        from . import signals
  1. 在magazine模块中编辑init.py来包含如下内容
# myproject/apps/magazine/__init__.py
    default_app_config = "myproject.apps.magazine.apps.MagazineAppConfig"
  1. 我们来创建一个signals.py文件并在其中添加一些信号处理器:
# myproject/apps/magazine/signals.py
    from django.db.models.signals import post_save, post_delete 
    from django.dispatch import receiver
    from django.conf import settings

    from .models import NewsArticle

    @receiver(post_save, sender=NewsArticle) 
    def news_save_handler(sender, **kwargs):
      if settings.DEBUG: 
        print(f"{kwargs['instance']} saved.")

    @receiver(post_delete, sender=NewsArticle) 
    def news_delete_handler(sender, **kwargs):
      if settings.DEBUG: 
        print(f"{kwargs['instance']} deleted.")

实现原理...

在运行HTTP服务或调用管理命令时,会调用django.setup()。它会加载设置、配置日志并准备应用仓库。这一仓库通过3个步骤进行初始化。首先从设置的INSTALLED_APPS中导入每项应用的配置。这些项可指向应用名或直接指向配置,如"myproject.apps.magazine" 或 "myproject.apps.magazine.apps.MagazineAppConfig"。

然后Django尝试通过INSTALLED_APPS中的每个应用导入models.py并汇集所有模型。

最后,Django对每个应用配置运行ready()方法。该方法是开发过程中注册信号处理器(若有)一个很好的点。ready() 方法是可选的。

在我们的示例中,MagazineAppConfig类设置magazine应用的配置。name参数定义了当前应用的模块。verbose_name参数定义了Django模型管理后台中供人阅读的名称,在这一后台中模型按照应用进行分组。ready()方法导入并激活信号处理器,在DEBUG模型下,向终端打印NewsArticle被保存或删除。

扩展知识...

在调用django.setup()之后,可以像下面这样通过仓库加载应用配置及模型:

>>> from django.apps import apps as django_apps
>>> magazine_app_config = django_apps.get_app_config("magazine") 
>>> magazine_app_config
<MagazineAppConfig: magazine>
>>> magazine_app_config.models_module
<module 'magazine.models' from '/path/to/myproject/apps/magazine/models.py'>
>>> NewsArticle = django_apps.get_model("magazine", "NewsArticle") 
>>> NewsArticle
<class 'magazine.models.NewsArticle'>

译者注:以上请在django-myproject目录下操作并设置环境变量export DJANGO_SETTINGS_MODULE='myproject.settings.xxx'

可以在Django官方文档中阅读更多有关应用配置的信息。

其它内容

  • 使用虚拟环境一节
  • 使用Docker容器处理Django, Gunicorn, Nginx和PostgreSQL一节
  • 定义可重写的app设置一节
  • 第6章 模型管理

定义可重写的app设置

本小节将展示如何定义应用的设置并可在项目设置文件中进行重写。这对于可复用应用犹为有用,可以通过添加配置来实现自定义。

准备工作

按照创建应用配置一节中准备工作的步骤创建Django应用。

如何实现...

  1. 如果只有一两个设置在models.py中使用getattr()模式来定义应用设置,或者设置很多并希望更好地进行组织的话在app_settings.py文件中进行定义 :
# myproject/apps/magazine/app_settings.py
    from django.conf import settings
    from django.utils.translation import gettext_lazy as _

    # Example:
    SETTING_1 = getattr(settings, "MAGAZINE_SETTING_1", "default value")

    MEANING_OF_LIFE = getattr(settings, "MAGAZINE_MEANING_OF_LIFE", 42)

    ARTICLE_THEME_CHOICES = getattr( 
      settings,
      "MAGAZINE_ARTICLE_THEME_CHOICES",
      [
          ('futurism', _("Futurism")),
          ('nostalgia', _("Nostalgia")),
          ('sustainability', _("Sustainability")),
          ('wonder', _("Wonder")),
      ] 
    )
  1. models.py中包含NewsArticle模型,如下:
# myproject/apps/magazine/models.py
   from django.db import models
   from django.utils.translation import gettext_lazy as _

   class NewsArticle(models.Model):
     created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
     title = models.CharField(_("Title"), max_length=255) 
     body = models.TextField(_("Body"))
     theme = models.CharField(_("Theme"), max_length=20)

     class Meta:
       verbose_name = _("News Article") 
       verbose_name_plural = _("News Articles")

     def __str__(self): 
       return self.title
  1. 然后在admin.py中我们将导入并使用app_settings.py中的设置,如下:
# myproject/apps/magazine/admin.py
    from django import forms
    from django.contrib import admin

    from .models import NewsArticle

    from .app_settings import ARTICLE_THEME_CHOICES

    class NewsArticleModelForm(forms.ModelForm): 
      theme = forms.ChoiceField(
        label=NewsArticle._meta.get_field("theme").verbose_name, 
        choices=ARTICLE_THEME_CHOICES,
        required=not NewsArticle._meta.get_field("theme").blank,
      )
      class Meta:
        fields = "__all__"

    @admin.register(NewsArticle)
    class NewsArticleAdmin(admin.ModelAdmin):
      form = NewsArticleModelForm
  1. 如果希望重写给定项目中的ARTICLE_THEME_CHOICES设置,应在项目设置中添加MAGAZINE_ARTICLE_THEME_CHOICES:
# myproject/settings/_base.py
    from django.utils.translation import gettext_lazy as _ 
    # ...
    MAGAZINE_ARTICLE_THEME_CHOICES = [
      ('futurism', _("Futurism")),
      ('nostalgia', _("Nostalgia")),
      ('sustainability', _("Sustainability")),
      ('wonder', _("Wonder")),
      ('positivity', _("Positivity")),
      ('solutions', _("Solutions")),
      ('science', _("Science")),
    ]
    

实现原理...

getattr(object, attribute_name[, default_value]) Python函数尝试通过object获取attribute_name属性并在未查找到时返回default_value。我们会从Django设置块中读取不同的设置,在不存在时使用默认值。

注意我们也可以在models.py中定义theme字段的选项,但这里在后台管理代码中创建了一个自定义的ModelForm并其中设置了选项。这样做是避免在每次修改ARTICLE_THEME_CHOICES的时候产生一次新的数据库迁移。

其它内容

  • 创建应用配置一节
  • 第6章 模型管理

使用Docker容器处理Django, Gunicorn, Nginx和PostgreSQL

Django项目不仅有Python包的依赖,还对系统有很多要求,如web服务器、数据库、服务端缓存和邮件服务。在开发Django项目时,需要确保所有环境和所有开发者安装了相同的依赖。一种保持这些依赖同步的方式是使用Docker。通过Docker,可以对每个项目单独拥有不同版本的数据库、web 或其它服务。

Docker是一种用于创建称之为容器的带配置、自定义虚拟机的系统。它允许我们精确地复制生产环境的设置。Docker通过称为Docker镜像的东西进行创建。镜像由如何构建容器的层(或指令)组成。可以有PostgreSQL镜像、Redis镜像、Memcached镜像,以及针对Django项目的自定义镜像,所有这些镜像可通过Docker Compose 整合成关联的容器。

在本小节中,我们将使用项目样板来设置带有数据库的Django项目,由Nginx和Gunicorn提供服务、并通过Docker Compose对它们进行管理。

准备工作

首先,我们需要安装Docker引擎,可按照https://www.docker.com/get-started上的教程进行安装。这通常包含Compose工具,让我们可以管理需要多个容器的系统,对于隔离环境的Django也非常理想。如果要单独安装,可参见https://docs.docker.com/compose/install/中有关Compose的内容。

译者注: 考虑到国内的网络环境可配置 Docker 仓库的加速:Preferences>Docker Engine,如添加

  "registry-mirrors": [
    "http://hub-mirror.c.163.com",
  ]

如何实现...

我们来共同探讨下Django和Docker样板:

  1. https://github.com/archatas/django_docker中的代码下载到本地,如~/projects/django_docker 目录中。

    ℹ️如果你选择的是其它目录,如myproject_docker,那么需要进行全局搜索并将django_docker替换为myproject_docker。

  2. 打开docker-compose.yml文件。需要创建3个容器:nginx, gunicorn和db。不必担心它看上去很复杂,我们将在后面详细讲解:

# docker-compose.yml
    version: "3.7"
    services: 
      nginx:
        image: nginx:latest
        ports:
          - "80:80" 
        volumes:
          - ./config/nginx/conf.d:/etc/nginx/conf.d 
          - static_volume:/home/myproject/static
          - media_volume:/home/myproject/media
        depends_on: 
          - gunicorn

      gunicorn: 
        build:
          context: . 
          args:
            PIP_REQUIREMENTS: "${PIP_REQUIREMENTS}"
            command: bash -c "/home/myproject/env/bin/gunicorn --workers 3 --bind 0.0.0.0:8000 myproject.wsgi:application"
        depends_on:
          - db 
        volumes:
          - static_volume:/home/myproject/static
          - media_volume:/home/myproject/media 
        expose:
          - "8000" 
        environment:
          DJANGO_SETTINGS_MODULE: "${DJANGO_SETTINGS_MODULE}"
          DJANGO_SECRET_KEY: "${DJANGO_SECRET_KEY}"
          DATABASE_NAME: "${DATABASE_NAME}"
          DATABASE_USER: "${DATABASE_USER}"
          DATABASE_PASSWORD: "${DATABASE_PASSWORD}"
          EMAIL_HOST: "${EMAIL_HOST}"
          EMAIL_PORT: "${EMAIL_PORT}"
          EMAIL_HOST_USER: "${EMAIL_HOST_USER}"
          EMAIL_HOST_PASSWORD: "${EMAIL_HOST_PASSWORD}"

      db:
        image: postgres:latest 
        restart: always 
        environment:
          POSTGRES_DB: "${DATABASE_NAME}"
          POSTGRES_USER: "${DATABASE_USER}"
          POSTGRES_PASSWORD: "${DATABASE_PASSWORD}"
        ports:
          - 5432
        volumes:
          - postgres_data:/var/lib/postgresql/data/

    volumes:
      postgres_data:
      static_volume:
      media_volume:
  1. 打开并通读Dockerfile文件。有创建gunicorn容器所需要的一些层(或指令):
# Dockerfile
    # pull official base image 
    FROM python:3.8

    # accept arguments
    ARG PIP_REQUIREMENTS=production.txt

    # set environment variables
    ENV PYTHONDONTWRITEBYTECODE 1
    ENV PYTHONUNBUFFERED 1

    # install dependencies
    RUN pip install --upgrade pip setuptools

    # create user for the Django project 
    RUN useradd -ms /bin/bash myproject

    # set current user
    USER myproject

    # set work directory
    WORKDIR /home/myproject

    # 添加以下内容避免因 Volume 所带来的 root 权限问题
    RUN mkdir static && mkdir media

    # create and activate virtual environment 
    RUN python3 -m venv env

    # copy and install pip requirements
    COPY --chown=myproject ./src/myproject/requirements /home/myproject/requirements/
    # 添加豆瓣源在国内加速
    RUN ./env/bin/pip3 install -i https://pypi.doubanio.com/simple/ -r /home/myproject/requirements/${PIP_REQUIREMENTS}

    # copy Django project files
    COPY --chown=myproject ./src/myproject /home/myproject/
  1. 复制build_dev_example.sh 脚本到build_dev.sh中并编辑其内容。有一些要传递给docker-compose脚本的环境变量:
# build_dev.sh
    #!/usr/bin/env bash 
    DJANGO_SETTINGS_MODULE=myproject.settings.dev \
    DJANGO_SECRET_KEY="change-this-to-50-characters-long-random-string" \
    DATABASE_NAME=myproject \
    DATABASE_USER=myproject \
    DATABASE_PASSWORD="change-this-too" \
    PIP_REQUIREMENTS=dev.txt \
    docker-compose up --detach --build
  1. 在命令行工具中,为build_dev.sh添加执行权限并运行它来构建容器:
$ chmod +x build_dev.sh 
    $ ./build_dev.sh
  1. 如果此时访问http://0.0.0.0/en/,应当会看到Hello, World!页面。
    在访问http://0.0.0.0/en/admin/时,会看到如下内容:
OperationalError at /en/admin/
    FATAL: role "myproject" does not exist
这表示你需要在Docker容器中创建数据库用户及数据库。
  1. 我们通过SSH连接db容器并在Docker容器中创建数据库用户、密码及数据库本身:
$ docker exec -it django_docker_db_1 bash
    /# su - postgres
    /$ createuser --createdb --password myproject 
    /$ createdb --username myproject myproject
在被询问时,输入与build_dev.sh脚本中相同的数据库密码。(如未出现6中的报错则无需手动执行以上用户创建)
连续按两次Ctrl + D来登出PostgreSQL用户和Docker容器。现在访问http://0.0.0.0/en/admin/的话,会看到如下内容:
ProgrammingError at /en/admin/ relation "django_session" does not exist LINE 1: ...ession_data", "django_session"."expire_date" FROM "django_se...
这表示我们需要运行迁移来创建数据库模式。
  1. 通过SSH进入gunicorn容器并运行必要的Django管理命令:
$ docker exec -it django_docker_gunicorn_1 bash 
    $ source env/bin/activate
    (env)$ python manage.py migrate
    (env)$ python manage.py collectstatic
    (env)$ python manage.py createsuperuser
回复管理命令所询问的所有问题。
按两次Ctrl + D登出Docker容器。
如果现在导航至http:/​/​0.​0.​0.​0/​en/​admin/​,应当会看到Django后台管理页面,在这里可以通过所创建的超级用户来进行登录。
  1. 创建相似的脚本 build_test.sh、build_staging.sh和build_production.sh,其中仅环境变量存在差别。

实现原理...

样板中的代码结构类似于虚拟环境中的那个。项目源文件放在src目录中。这里有作为预提交钩子的git-hooks目录用于追踪最后一次变更日期及容器中使用的服务配置目录:

django_docker/
├── Dockerfile
├── LICENSE
├── README.md
├── build_dev_example.sh
├── config
│   └── nginx
│       └── conf.d
│           └── myproject.conf
├── docker-compose.yml
├── git-hooks
│   ├── install_hooks.sh
│   └── pre-commit
└── src
    └── myproject
        ├── locale
        │   └── de
        │       └── LC_MESSAGES
        │           └── django.po
        ├── manage.py
        ├── myproject
        │   ├── __init__.py
        │   ├── apps
        │   │   └── __init__.py
        │   ├── settings
        │   │   ├── __init__.py
        │   │   ├── _base.py
        │   │   ├── dev.py
        │   │   ├── last-update.txt
        │   │   ├── production.py
        │   │   ├── staging.py
        │   │   └── test.py
        │   ├── site_static
        │   │   └── site
        │   │       ├── css
        │   │       │   └── style.css
        │   │       ├── img
        │   │       │   ├── favicon-16x16.png
        │   │       │   ├── favicon-32x32.png
        │   │       │   └── favicon.ico
        │   │       ├── js
        │   │       │   └── main.js
        │   │       └── scss
        │   │           └── style.scss
        │   ├── templates
        │   │   ├── base.html
        │   │   └── index.html
        │   ├── urls.py
        │   └── wsgi.py
        └── requirements
            ├── _base.txt
            ├── dev.txt
            ├── production.txt
            ├── staging.txt
            └── test.txt

Docker相关的主要配置在docker-compose.yml和Dockerfile中。Docker Compose是对Docker的命令行API进行的封装。build_dev.sh脚本运行Django项目,Gunicorn WSGI HTTP服务器的端口号为8000,Nginx的端口号为80(对静态和媒体文件提供服务并将请求代理到Gunicorn),PostgreSQL数据库的端口为5432。

在docker-compose.yml文件中,要求创建三个Docker容器:

  • nginx:用于Nginx网页服务器
  • gunicorn:用于带有Gunicorn网页服务器的Django项目
  • db:用于PostgreSQL数据库

nginx和db通过位于https://hub.docker.com/的官方镜像进行创建。它们有具体的配置参数,如所运行的端口、环境变量、对其它容器的依赖和磁盘卷(Volume)。

Docker磁盘卷是在重建Docker 容器不会被改变的特殊目录。需要定义磁盘卷来供数据库数据文件、媒体、静态文件等使用。

gunicorn容器通过Dockerfile中的指令进行构建,由docker-compose.yml文件中的构建上下文进行定义。我们来详解里面的每一层(或命令):

  • gunicorn容器基于python:3.7 镜像
  • 会接收docker-compose.yml文件中的PIP_REQUIREMENTS作为参数
  • 会为容器设置环境变量
  • 会安装并升级pip, setuptools和virtualenv
  • 会为Django项目创建名为myproject的系统用户
  • 会设置myproject为当前用户
  • 会为Django项目创建名为myproject的系统用户
  • 会设置myproject为当前用户
  • 会设置myproject用户的家目录作为当前工作目录
  • 会在该处创建虚拟环境
  • 会从本地电脑将pip依赖拷贝到Docker容器
  • 会为当前环境安装由PIP_REQUIREMENTS变量定义的pip依赖
  • 会拷贝整个Django项目的源代码

config/nginx/conf.d/myproject.conf中的内容会保存到nginx容器的/etc/nginx/conf.d/中。Nginx web服务器的这一配置是说要监听80端口(默认HTTP端口)并转发端口为8000的Gunicorn服务的请求,要求静态或媒体内容的请求除外:

#/etc/nginx/conf.d/myproject.conf
upstream myproject {
  server django_docker_gunicorn_1:8000;
}

server {
  listen 80;

  location / {
    proxy_pass http://myproject;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
    proxy_set_header Host $host;
    proxy_redirect off;
  }

  rewrite "/static/\d+/(.*)" /static/$1 last; 

  location /static/ {
    alias /home/myproject/static/;
  }

  location /media/ {
    alias /home/myproject/media/;
  } 
}

第12章 部署中的在预发布环境中基于Nginx和Gunicorn部署在生产环境中基于Nginx和Gunicorn部署小节中会学习更多有关Nginx和Gunicorn配置的知识。

扩展知识...

可以通过docker-compose down命令销毁Docker容器及使用构建脚本重建这些容器:

$ docker-compose down 
$ ./build_dev.sh

如果和所预期的效果不一致,可以通过docker-compose logs命令来检查日志:

$ docker-compose logs nginx
$ docker-compose logs gunicorn 
$ docker-compose logs db

通过SSH来连接其中的任一容器,可以使用下面的命令:

$ docker exec -it django_docker_gunicorn_1 bash 
$ docker exec -it django_docker_nginx_1 bash
$ docker exec -it django_docker_db_1 bash

可以使用docker cp命令将Docker容器的数据卷进行拷入和拷出文件、目录的处理:

$ docker cp ~/avatar.png django_docker_gunicorn_1:/home/myproject/media/ 
$ docker cp django_docker_gunicorn_1:/home/myproject/media ~/Desktop/

如果希望更好地掌握Docker和Docker Compose,可以学习官方文档的内容,尤其是https://docs.docker.com/compose/部分。

其它内容

  • 创建项目文件结构一节
  • 第12章 部署中的在预发布环境通过mod_wsgi部署Apache一节
  • 第12章 部署中的在生产环境通过mod_wsgi部署Apache一节
  • 第12章 部署中的在预发布环境中基于Nginx和Gunicorn部署一节
  • 第12章 部署中的在生产环境中基于Nginx和Gunicorn部署一节

本文首发于Alan Hou的个人博客

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 启动一个新项目 项目创建 执行下面的命令来创建一个新的 Django 项目: django-admin start...
    冰321阅读 308评论 0 4
  • 模板标签除了几个常用的,还真心没有仔细了解一下,看到2.0发布后,翻译学习一下。 本文尽量忠实原著,毕竟大神的东西...
    海明_fd17阅读 1,993评论 0 5
  • 从前工作单位买来四五斤酱牛肉 一只扒皮鸭 一兜鸡翅 两兜肉丸子 还有腊肠和扣肉若干 全给我一个人吃完了 加热扒皮鸭...
    颜阳天阅读 279评论 0 0
  • 一:下载小小输入法以及QT5插件 小小输入法官方网盘 or 打包好的98五笔版小小输入法 QT插件对于『小小输入法...
    Ubuntu_2017阅读 1,447评论 2 2
  • 你给简书找bug,简书给你送好礼。即日起,参与简书公测就有机会获得简书提供的精美周边! 本期公测版本-简书iOS ...
    简书阅读 11,389评论 136 56