在这个比较 Python 框架的最后一篇中,让我们看看 Django。
在本系列(由四部分组成)的前三篇文章中,我们讨论了 Pyramid、Flask 和 Tornado 这 3 个 Web 框架。我们已经构建了三次相同的应用程序,最终我们遇到了 Django。总的来说,Django 是目前 Python 开发人员使用的主要 Web 框架,并且原因显而易见。它擅长隐藏大量的配置逻辑,让你专注于能够快速构建大型应用程序。
也就是说,当涉及到小型项目时,比如我们的待办事项列表应用程序,Django 可能有点像用消防水管来进行水枪大战。让我们来看看它们是如何结合在一起的。
关于 Django
Django 将自己定位为“一个鼓励快速开发和整洁、实用的设计的高级 Python Web 框架。它由经验丰富的开发人员构建,解决了 Web 开发的很多麻烦,因此你可以专注于编写应用程序而无需重新发明轮子”。而且它确实做到了!这个庞大的 Web 框架附带了非常多的工具,以至于在开发过程中,如何将所有内容组合在一起协同工作可能是个谜。
除了框架本身很大,Django 社区也是非常庞大的。事实上,它非常庞大和活跃,以至于有一个网站专门用于为人们收集第三方包,这些第三方包可集成进 Django 来做一大堆事情。包括从身份验证和授权到完全基于 Django 的内容管理系统,电子商务附加组件以及与 Stripe(LCTT 译注:美版“支付宝”)集成的所有内容。至于不要重新发明轮子:如果你想用 Django 完成一些事情,有人可能已经做过了,你只需将它集成进你的项目就行。
为此,我们希望使用 Django 构建 REST API,因此我们将使用流行的 Django REST 框架。它的工作是将 Django 框架(Django 使用自己的模板引擎构建 HTML 页面)转换为专门用于有效地处理 REST 交互的系统。让我们开始吧。
Django 启动和配置
$ mkdir django_todo
$ cd django_todo
$ pipenv install --python 3.6
$ pipenv shell
(django-someHash) $ pipenv install django djangorestframework
作为参考,我们使用的是 django-2.0.7
和 djangorestframework-3.8.2
。
与 Flask, Tornado 和 Pyramid 不同,我们不需要自己编写 setup.py
文件,我们并不是在做一个可安装的 Python 发布版。像很多事情一样,Django 以自己的方式处理这个问题。我们仍然需要一个 requirements.txt
文件来跟踪我们在其它地方部署的所有必要安装。但是,就 Django 项目中的目标模块而言,Django 会让我们列出我们想要访问的子目录,然后允许我们从这些目录中导入,就像它们是已安装的包一样。
首先,我们必须创建一个 Django 项目。
当我们安装了 Django 后,我们还安装了命令行脚本 django-admin
。它的工作是管理所有与 Django 相关的命令,这些命令有助于我们将项目整合在一起,并在我们继续开发的过程中对其进行维护。django-admin
并不是让我们从头开始构建整个 Django 生态系统,而是让我们从标准 Django 项目所需的所有必要文件(以及更多)的基础上开始。
调用 django-admin
的 start-project
命令的语法是 django-admin startproject
。我们希望文件存于当前的工作目录中,所以:
(django-someHash) $ django-admin startproject django_todo .
输入 ls
将显示一个新文件和一个新目录。
(django-someHash) $ ls
manage.py django_todo
manage.py
是一个可执行命令行 Python 文件,它最终成为 django-admin
的封装。因此,它的工作与 django-admin
是一样的:帮助我们管理项目。因此得名 manage.py
。
它在 django_todo
目录里创建了一个新目录 django_todo
,其代表了我们项目的配置根目录。现在让我们深入研究一下。
配置 Django
可以将 django_todo
目录称为“配置根目录”,我们的意思是这个目录包含了通常配置 Django 项目所需的文件。几乎所有这个目录之外的内容都只关注与项目模型、视图、路由等相关的“业务逻辑”。所有连接项目的点都将在这里出现。
在 django_todo
目录中调用 ls
会显示以下四个文件:
(django-someHash) $ cd django_todo
(django-someHash) $ ls
__init__.py settings.py urls.py wsgi.py
__init__.py
文件为空,之所以存在是为了将此目录转换为可导入的 Python 包。settings.py
是设置大多数配置项的地方。例如项目是否处于 DEBUG 模式,正在使用哪些数据库,Django 应该定位文件的位置等等。它是配置根目录的“主要配置”部分,我们将在一会深入研究。urls.py
顾名思义就是设置 URL 的地方。虽然我们不必在此文件中显式写入项目的每个 URL,但我们需要让此文件知道在其他任何地方已声明的 URL。如果此文件未指向其它 URL,则那些 URL 就不存在。wsgi.py
用于在生产环境中提供应用程序。就像 Pyramid、 Tornado 和 Flask 暴露了一些 “app” 对象一样,它们用来提供配置好的应用程序,Django 也必须暴露一个,就是在这里完成的。它可以和 Gunicorn、Waitress 或者 uWSGI 一起配合来提供服务。
设置 settings
看一看 settings.py
,它里面有大量的配置项,那些只是默认值!这甚至不包括数据库、静态文件、媒体文件、任何集成的钩子,或者可以配置 Django 项目的任何其它几种方式。让我们从上到下看看有什么:
BASE_DIR
设置目录的绝对路径,或者是manage.py
所在的目录。这对于定位文件非常有用。SECRET_KEY
是用于 Django 项目中加密签名的密钥。在实际中,它用于会话、cookie、CSRF 保护和身份验证令牌等。最好在第一次提交之前,尽快应该更改SECRET_KEY
的值并将其放置到环境变量中。DEBUG
告诉 Django 是以开发模式还是生产模式运行项目。这是一个非常关键的区别。- 在开发模式下,当弹出一个错误时,Django 将显示导致错误的完整堆栈跟踪,以及运行项目所涉及的所有设置和配置。如果在生产环境中将
DEBUG
设置为True
,这可能成为一个巨大的安全问题。 - 在生产模式下,当出现问题时,Django 会显示一个简单的错误页面,即除错误代码外不提供任何信息。
- 保护我们项目的一个简单方法是将
DEBUG
设置为环境变量,如bool(os.environ.get('DEBUG', ''))
。
- 在开发模式下,当弹出一个错误时,Django 将显示导致错误的完整堆栈跟踪,以及运行项目所涉及的所有设置和配置。如果在生产环境中将
ALLOWED_HOSTS
是应用程序提供服务的主机名的列表。在开发模式中,这可能是空的;但是在生产环境中,如果为项目提供服务的主机不在ALLOWED_HOSTS
列表中,Django 项目将无法运行。这是设置为环境变量的另一种情况。INSTALLED_APPS
是我们的 Django 项目可以访问的 Django “apps” 列表(将它们视为子目录,稍后会详细介绍)。默认情况下,它将提供:- 内置的 Django 管理网站
- Django 的内置认证系统
- Django 的数据模型通用管理器
- 会话管理
- Cookie 和基于会话的消息传递
- 站点固有的静态文件的用法,比如
css
文件、js
文件、任何属于我们网站设计的图片等。
MIDDLEWARE
顾名思义:帮助 Django 项目运行的中间件。其中很大一部分用于处理各种类型的安全,尽管我们可以根据需要添加其它中间件。ROOT_URLCONF
设置基本 URL 配置文件的导入路径。还记得我们之前见过的那个urls.py
吗?默认情况下,Django 指向该文件以此来收集所有的 URL。如果我们想让 Django 在其它地方寻找,我们将在这里设置 URL 位置的导入路径。TEMPLATES
是 Django 用于我们网站前端的模板引擎列表,假如我们依靠 Django 来构建我们的 HTML。我们在这里不需要,那就无关紧要了。WSGI_APPLICATION
设置我们的 WSGI 应用程序的导入路径 —— 在生产环境下使用的东西。默认情况下,它指向wsgi.py
中的application
对象。这很少(如果有的话)需要修改。DATABASES
设置 Django 项目将访问那些数据库。必须设置default
数据库。我们可以通过名称设置别的数据库,只要我们提供HOST
、USER
、PASSWORD
、PORT
、数据库名称NAME
和合适的ENGINE
。可以想象,这些都是敏感的信息,因此最好将它们隐藏在环境变量中。查看 Django 文档了解更多详情。- 注意:如果不是提供数据库的每个单个部分,而是提供完整的数据库 URL,请查看 djdatabaseurl。
AUTH_PASSWORD_VALIDATORS
实际上是运行以检查输入密码的函数列表。默认情况下我们有一些,但是如果我们有其它更复杂的验证需求:不仅仅是检查密码是否与用户的属性匹配,是否超过最小长度,是否是 1000 个最常用的密码之一,或者密码完全是数字,我们可以在这里列出它们。LANGUAGE_CODE
设置网站的语言。默认情况下它是美国英语,但我们可以将其切换为其它语言。TIME_ZONE
是我们 Django 项目后中自动生成的时间戳的时区。我强调坚持使用 UTC 并在其它地方执行任何特定于时区的处理,而不是尝试重新配置此设置。正如这篇文章 所述,UTC 是所有时区的共同点,因为不需要担心偏移。如果偏移很重要,我们可以根据需要使用与 UTC 的适当偏移来计算它们。USE_I18N
将让 Django 使用自己的翻译服务来为前端翻译字符串。I18N = 国际化(internationalization,“i” 和 “n” 之间共 18 个字符)。USE_L10N
L10N = 本地化(localization,在 l 和 n 之间共 10 个字符) 。如果设置为True
,那么将使用数据的公共本地格式。一个很好的例子是日期:在美国它是 MM-DD-YYYY。在欧洲,日期往往写成 DD-MM-YYYY。STATIC_URL
是用于提供静态文件的主体部分。我们将构建一个 REST API,因此我们不需要考虑静态文件。通常,这会为每个静态文件的域名设置根路径。所以,如果我们有一个 Logo 图像,那就是http:////logo.gif
。
默认情况下,这些设置已准备就绪。我们必须改变的一个选项是 DATABASES
设置。首先,我们创建将要使用的数据库:
(django-someHash) $ createdb django_todo
我们想要像使用 Flask、Pyramid 和 Tornado 一样使用 PostgreSQL 数据库,这意味着我们必须更改 DATABASES
设置以允许我们的服务器访问 PostgreSQL 数据库。首先是引擎。默认情况下,数据库引擎是 django.db.backends.sqlite3
,我们把它改成 django.db.backends.postgresql
。
有关 Django 可用引擎的更多信息,请查看文档。请注意,尽管技术上可以将 NoSQL 解决方案整合到 Django 项目中,但为了开箱即用,Django 强烈偏向于 SQL 解决方案。
接下来,我们必须为连接参数的不同部分指定键值对。
NAME
是我们刚刚创建的数据库的名称。USER
是 Postgres 数据库用户名。PASSWORD
是访问数据库所需的密码。HOST
是数据库的主机。当我们在本地开发时,localhost
或127.0.0.1
都将起作用。PORT
是我们为 Postgres 开放的端口,它通常是5432
。
settings.py
希望我们为每个键提供字符串值。但是,这是高度敏感的信息。任何负责任的开发人员都不应该这样做。有几种方法可以解决这个问题,一种是我们需要设置环境变量。
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME', ''),
'USER': os.environ.get('DB_USER', ''),
'PASSWORD': os.environ.get('DB_PASS', ''),
'HOST': os.environ.get('DB_HOST', ''),
'PORT': os.environ.get('DB_PORT', ''),
}
}
在继续之前,请确保设置环境变量,否则 Django 将无法工作。此外,我们需要在此环境中安装 psycopg2
,以便我们可以与数据库通信。
Django 路由和视图
让我们在这个项目中实现一些函数。我们将使用 Django REST 框架来构建 REST API,所以我们必须确保在 settings.py
中将 rest_framework
添加到 INSTALLED_APPS
的末尾。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework'
]
虽然 Django REST 框架并不专门需要基于类的视图(如 Tornado)来处理传入的请求,但类是编写视图的首选方法。让我们来定义一个类视图。
让我们在 django_todo
创建一个名为 views.py
的文件。在 views.py
中,我们将创建 “Hello, world!” 视图。
# in django_todo/views.py
from rest_framework.response import JsonResponse
from rest_framework.views import APIView
class HelloWorld(APIView):
def get(self, request, format=None):
"""Print 'Hello, world!' as the response body."""
return JsonResponse("Hello, world!")
每个 Django REST 框架基于类的视图都直接或间接地继承自 APIView
。APIView
处理大量的东西,但针对我们的用途,它做了以下特定的事情:
* 根据 HTTP 方法(例如 GET、POST、PUT、DELETE)来设置引导对应请求所需的方法
* 用我们需要的所有数据和属性来填充 `request` 对象,以便解析和处理传入的请求
* 采用 `Response` 或 `JsonResponse`,每个调度方法(即名为 `get`、`post`、`put`、`delete` 的方法)返回并构造格式正确的 HTTP 响应。
终于,我们有一个视图了!它本身没有任何作用,我们需要将它连接到路由。
如果我们跳转到 django_todo/urls.py
,我们会到达默认的 URL 配置文件。如前所述:如果 Django 项目中的路由不包含在此处,则它不存在。
我们在给定的 urlpatterns
列表中添加所需的 URL。默认情况下,我们有一整套 URL 用于 Django 的内置管理后端系统。我们会完全删除它。
我们还得到一些非常有用的文档字符串,它告诉我们如何向 Django 项目添加路由。我们需要调用 path()
,伴随三个参数:
- 所需的路由,作为字符串(没有前导斜线)
- 处理该路由的视图函数(只能有一个函数!)
- 在 Django 项目中路由的名称
让我们导入 HelloWorld
视图并将其附加到主路径 /
。我们可以从 urlpatterns
中删除 admin
的路径,因为我们不会使用它。
# django_todo/urls.py, after the big doc string
from django.urls import path
from django_todo.views import HelloWorld
urlpatterns = [
path('', HelloWorld.as_view(), name="hello"),
]
好吧,这里有一点不同。我们指定的路由只是一个空白字符串,为什么它会工作?Django 假设我们声明的每个路由都以一个前导斜杠开头,我们只是在初始域名后指定资源路由。如果一条路由没有去往一个特定的资源,而只是一个主页,那么该路由是 ''
,实际上是“没有资源”。
HelloWorld
视图是从我们刚刚创建的 views.py
文件导入的。为了执行此导入,我们需要更新 settings.py
中的 INSTALLED_APPS
列表使其包含 django_todo
。是的,这有点奇怪。以下是一种理解方式。
INSTALLED_APPS
指的是 Django 认为可导入的目录或包的列表。它是 Django 处理项目的各个组件的方式,比如安装了一个包,而不需要经过 setup.py
的方式。我们希望将 django_todo
目录视为可导入的包,因此我们将该目录包含在 INSTALLED_APPS
中。现在,在该目录中的任何模块也是可导入的。所以我们得到了我们的视图。
path
函数只将视图函数作为第二个参数,而不仅仅是基于类的视图。幸运的是,所有有效的基于 Django 类的视图都包含 .as_view()
方法。它的工作是将基于类的视图的所有优点汇总到一个视图函数中并返回该视图函数。所以,我们永远不必担心转换的工作。相反,我们只需要考虑业务逻辑,让 Django 和 Django REST 框架处理剩下的事情。
让我们在浏览器中打开它!
Django 提供了自己的本地开发服务器,可通过 manage.py
访问。让我们切换到包含 manage.py
的目录并输入:
(django-someHash) $ ./manage.py runserver
Performing system checks...
System check identified no issues (0 silenced).
August 01, 2018 - 16:47:24
Django version 2.0.7, using settings 'django_todo.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
当 runserver
执行时,Django 会检查以确保项目(或多或少)正确连接在一起。这不是万无一失的,但确实会发现一些明显的问题。如果我们的数据库与代码不同步,它会通知我们。毫无疑问,因为我们没有将任何应用程序的东西提交到我们的数据库,但现在这样做还是可以的。让我们访问 http://127.0.0.1:8000
来查看 HelloWorld
视图的输出。
咦?这不是我们在 Pyramid、Flask 和 Tornado 中看到的明文数据。当使用 Django REST 框架时,HTTP 响应(在浏览器中查看时)是这样呈现的 HTML,以红色显示我们的实际 JSON 响应。
但不要担心!如果我们在命令行中使用 curl
快速访问 http://127.0.0.1:8000
,我们就不会得到任何花哨的 HTML,只有内容。
# 注意:在不同的终端口窗口中执行此操作,在虚拟环境之外
$ curl http://127.0.0.1:8000
"Hello, world!"
棒极了!
Django REST 框架希望我们在使用浏览器浏览时拥有一个人性化的界面。这是有道理的,如果在浏览器中查看 JSON,通常是因为人们想要检查它是否正确,或者在设计一些消费者 API 时想要了解 JSON 响应。这很像你从 Postman 中获得的东西。
无论哪种方式,我们都知道我们的视图工作了!酷!让我们概括一下我们做过的事情:
django-admin startproject
开始一个项目django_todo/settings.py
中的 DEBUG
、SECRET_KEY
,还有 DATABASES
字典INSTALLED_APPS
django_todo/views.py
来包含我们的第一个类视图,它返回响应 “Hello, world!”django_todo/urls.py
,其中包含我们的根路由django_todo/settings.py
中更新 INSTALLED_APPS
以包含 django_todo
包创建模型
现在让我们来创建数据模型吧。
Django 项目的整个基础架构都是围绕数据模型构建的,它是这样编写的,每个数据模型够可以拥有自己的小天地,拥有自己的视图,自己与其资源相关的 URL 集合,甚至是自己的测试(如果我们想要的话)。
如果我们想构建一个简单的 Django 项目,我们可以通过在 django_todo
目录中编写我们自己的 models.py
文件并将其导入我们的视图来避免这种情况。但是,我们想以“正确”的方式编写 Django 项目,因此我们应该尽可能地将模型拆分成符合 Django Way™(Django 风格)的包。
Django Way 涉及创建所谓的 Django “应用程序”,它本身并不是单独的应用程序,它们没有自己的设置和诸如此类的东西(虽然它们也可以)。但是,它们可以拥有一个人们可能认为属于独立应用程序的东西:
- 一组自建的 URL
- 一组自建的 HTML 模板(如果我们想要提供 HTML)
- 一个或多个数据模型
- 一套自建的视图
- 一套自建的测试
它们是独立的,因此可以像独立应用程序一样轻松共享。实际上,Django REST 框架是 Django 应用程序的一个例子。它包含自己的视图和 HTML 模板,用于提供我们的 JSON。我们只是利用这个 Django 应用程序将我们的项目变成一个全面的 RESTful API 而不用那么麻烦。
要为我们的待办事项列表项创建 Django 应用程序,我们将要使用 manage.py
的 startapp
命令。
(django-someHash) $ ./manage.py startapp todo
startapp
命令成功执行后没有输出。我们可以通过使用 ls
来检查它是否完成它应该做的事情。
(django-someHash) $ ls
Pipfile Pipfile.lock django_todo manage.py todo
看看:我们有一个全新的 todo
目录。让我们看看里面!
(django-someHash) $ ls todo
__init__.py admin.py apps.py migrations models.py tests.py views.py
以下是 manage.py startapp
创建的文件:
__init__.py
是空文件。它之所以存在是因为此目录可看作是模型、视图等的有效导入路径。admin.py
不是空文件。它用于在 Django admin 中规范化这个应用程序的模型,我们在本文中没有涉及到它。apps.py
这里基本不起作用。它有助于规范化 Django admin 的模型。migrations
是一个包含我们数据模型快照的目录。它用于更新数据库。这是少数几个内置了数据库管理的框架之一,其中一部分允许我们更新数据库,而不必拆除它并重建它以更改 Schema。models.py
是数据模型所在。tests.py
是测试所在的地方,如果我们需要写测试。views.py
用于我们编写的与此应用程序中的模型相关的视图。它们不是一定得写在这里。例如,我们可以在django_todo/views.py
中写下我们所有的视图。但是,它在这个应用程序中更容易将我们的概念理清。在覆盖了许多概念的扩展应用程序的关系之间会变得更加密切。
它并没有为这个应用程序创建 urls.py
文件,但我们可以自己创建。
(django-someHash) $ touch todo/urls.py
在继续之前,我们应该帮自己一个忙,将这个新 Django 应用程序添加到 django_todo/settings.py
中的 INSTALLED_APPS
列表中。
# settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'django_todo',
'todo' #