第5章 Todo API
Django REST Framework¶
要添加 Django REST Framework,请键入 Control+c 停止本地服务器,然后使用 Pip 安装它。
Shell
(.venv) > python -m pip install djangorestframework~=3.13.0
然后将 rest_framework 添加到我们的 INSTALLED_APPS 设置中,就像任何其他第三方应用程序一样。我们还想开始配置 Django REST Framework 特定的设置,这些都存在于一个名为 REST_FRAMEWORK 的配置下,可以添加到文件的底部。
首先,让我们将权限明确设置为 AllowAny,它允许不受限制的访问,无论请求是否已认证。在生产环境中,API 权限是严格控制的,但出于学习目的,我们现在将使用 AllowAny。
Code
# django_project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# 3rd party
"rest_framework", # new
# Local
"todos.apps.TodosConfig",
]
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.AllowAny",
],
}
Django REST Framework 有一个很长的隐式设置的默认设置列表。您可以在这里看到完整的列表。AllowAny 是其中之一,这意味着当我们像上面那样显式设置它时,效果与我们没有设置 DEFAULT_PERMISSION_CLASSES 配置完全相同。
学习默认设置需要时间。在本书的过程中,我们将熟悉其中的一些。要记住的要点是,隐式默认设置的设计目的是让开发人员可以快速上手并在本地开发环境中工作。但就像传统的 Django 一样,默认的 Django REST Framework 设置不适合生产环境。在部署之前,我们通常会根据项目的过程中对它们进行许多更改。
好的,所以 Django REST Framework 已安装。接下来呢?与我们在上一章中构建了网页和 API 的图书馆项目不同,这里我们只是构建一个 API。因此,我们不需要创建任何模板文件或传统的 Django 视图。可以说,创建一个单独的 apis 应用程序也是不必要的,因为这个项目从设计上就是 API 优先的。虽然 Django 有很多关于项目结构的保护措施,但由开发人员决定如何组织他们的应用程序。这对于新手来说是一个常见的混淆点,但通过构建具有不同应用程序结构的多个项目,会更清楚地看到应用程序只是开发人员的一种组织工具。只要应用程序添加到 INSTALLED_APPS 并使用正确的导入结构,它们几乎可以用于任何配置。
要将我们现有的数据库模型转换为 Web API,我们需要更新 URL,添加 Django REST Framework 视图,并创建一个序列化器。让我们开始吧!
URLs 配置¶
我喜欢从 URL 开始,因为它们是我们的 API 端点的入口点。从位于 django_project/urls.py 的 Django 项目级文件开始。我们将在第二行导入 include,并在 api/ 路径处为我们的 todos 应用程序添加一个路由。将所有 API 端点放在一致的路径(如 api/)是一个好主意,以防您以后决定添加传统的 Django 网页。
Code
# django_project/urls.py
from django.contrib import admin
from django.urls import path, include # new
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("todos.urls")), # new
]
接下来,使用您的文本编辑器创建一个应用程序级的 todos/urls.py 文件,并添加以下代码:
Code
# todos/urls.py
from django.urls import path
from .views import ListTodo, DetailTodo
urlpatterns = [
path("<int:pk>/", DetailTodo.as_view(), name="todo_detail"),
path("", ListTodo.as_view(), name="todo_list"),
]
注意,我们在这里引用了两个视图:ListTodo 和 DetailTodo,我们还没有创建它们。但路由现在已经完成。在空字符串 "" 处将有一个所有 todos 的列表,换句话说在 api/,每个独立的 todo 将在其主键 pk 处可用,这是 Django 在每个数据库表中自动设置的值。第一个条目是 1,第二个是 2,依此类推。因此,我们的第一个 todo 最终将位于 API 端点 api/1/,第二个在 api/2/,依此类推。
序列化器¶
让我们回顾一下目前的位置。我们从传统的 Django 项目开始,添加了一个专用的应用程序,配置了我们的数据库模型,并添加了初始数据。然后我们安装了 Django REST Framework 并为它创建了一个 api 应用程序,我们刚刚配置了我们的 URL。还有两个步骤:序列化器和视图。让我们从序列化器开始,它将我们的模型数据转换为将在我们所需的 URL 输出的 JSON。创建一个名为 todos/serializers.py 的新文件,并使用以下代码更新它:
Code
# todos/serializers.py
from rest_framework import serializers
from .models import Todo
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = (
"id",
"title",
"body",
)
在顶部,我们导入了 Django REST Framework 的序列化器以及我们的 Todo 数据库模型。然后我们将 ModelSerializer 扩展为一个名为 TodoSerializer 的新类。这里的格式与我们如何在 Django 本身中创建模型类或表单非常相似。我们指定要使用的模型以及我们要暴露的模型上的特定字段。请记住,id(类似于 pk)是由 Django 自动创建的,所以我们不必在我们的 Todo 模型中定义它,但我们将在每个 todo 的单独详细视图中显示它。就是这样!Django REST Framework 将神奇地将我们的数据转换为 JSON,暴露我们的 Todo 模型中 id、title 和 body 的字段。
id 和 pk 有什么区别?它们都引用 Django ORM 自动添加到 Django 模型的字段。id 是 Python 标准库的内置函数,而 pk 来自 Django 本身。像 Django 中的 DetailView 这样的通用基于类的视图期望传递一个名为 pk 的参数,而在模型字段上,通常简单地引用 id。
我们需要做的最后一件事是配置一个 views.py 文件来伴随我们的序列化器和 URL。
视图¶
我们将在这里使用两个 DRF 通用视图:ListAPIView 用于显示所有 todos,以及 RetrieveAPIView 用于显示单个模型实例。
更新 todos/views.py 文件,使其如下所示:
Code
# todos/views.py
from rest_framework import generics
from .models import Todo
from .serializers import TodoSerializer
class ListTodo(generics.ListAPIView):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
class DetailTodo(generics.RetrieveAPIView):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
在顶部,我们导入了 Django REST Framework 的通用视图、我们的 Todo 模型和我们刚刚创建的 TodoSerializer。回想一下我们的 todos/urls.py 文件,我们有两个路由,因此有两个独立的视图。一个名为 ListTodo 的新视图子类化 ListAPIView,而 DetailTodo 子类化 RetrieveAPIView。
精明的读者会注意到这里的代码有一点冗余。我们基本上为每个视图重复了查询集和序列化器类,即使扩展的通用视图不同。在本书的后面,我们将学习视图集和路由器,它们解决了这个问题,并允许我们用更少的代码创建相同的 API 视图和 URL。
但现在我们已经完成了!我们的 API 已经可以使用了。
可浏览的 API¶
现在让我们使用 Django REST Framework 的可浏览 API 来与我们的数据交互。确保本地服务器正在运行,并导航到 [[http://127.0.0.1:8000/api/](http://127.0.0.1:8000/api/](http://127.0.0.1:8000/api/](http://127.0.0.1:8000/api/`)) 以查看我们的工作 API 列表视图端点。

此页面显示我们之前在数据库模型中创建的三个 todo。API 端点指的是用于发出请求的 URL。如果一个端点有多个项目,则称为集合,而单个项目称为资源。开发人员经常互换使用术语端点和资源,但它们意味着不同的事情。
我们还为每个独立的模型创建了一个 DetailTodo 视图,它应该可以在以下位置看到:
[[http://127.0.0.1:8000/api/1/](http://127.0.0.1:8000/api/1/](http://127.0.0.1:8000/api/1/](http://127.0.0.1:8000/api/1/`))

您还可以导航到以下端点:
- [[http://127.0.0.1:8000/api/2/](http://127.0.0.1:8000/api/2/](http://127.0.0.1:8000/api/2/](http://127.0.0.1:8000/api/2/))
-[http://127.0.0.1:8000/api/3/](http://127.0.0.1:8000/api/3/)
API 测试¶
正如我们在上一章中看到的,Django REST Framework 包含几个用于测试我们的 API 端点的辅助类。我们要检查是否使用了正确的 URL,返回 200 状态码,并包含正确的内容。这次有两个页面要测试:我们所有 todos 的列表页面和它们自己专用端点上的单个 todos。
使用您的文本编辑器打开 todos/tests.py 文件。要测试 API,我们需要在顶部导入三个新项目:来自 Django 的 reverse、来自 Django REST Framework 的 status 和来自 Django REST Framework 的 APITestCase。然后我们添加两个测试——test_api_listview 和 test_api_detailview——以检查列表和详细页面是否使用正确的命名 URL,返回 200 状态码,仅包含一个对象,并且响应具有所有预期的数据。这里唯一棘手的事情是,对于详细视图,我们必须传递对象的 pk。
Code
# todos/tests.py
from django.test import TestCase
from django.urls import reverse # new
from rest_framework import status # new
from rest_framework.test import APITestCase # new
from .models import Todo
class TodoModelTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.todo = Todo.objects.create(
title="First Todo",
body="A body of text here"
)
def test_model_content(self):
self.assertEqual(self.todo.title, "First Todo")
self.assertEqual(self.todo.body, "A body of text here")
self.assertEqual(str(self.todo), "First Todo")
def test_api_listview(self): # new
response = self.client.get(reverse("todo_list"))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(Todo.objects.count(), 1)
self.assertContains(response, self.todo)
def test_api_detailview(self): # new
response = self.client.get(
reverse("todo_detail", kwargs={"pk": self.todo.id}),
format="json"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(Todo.objects.count(), 1)
self.assertContains(response, "First Todo")
使用 python manage.py test 命令运行测试。
Shell
(.venv) > python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...
----------------------------------------------------------------------
Ran 3 tests in 0.007s
OK
Destroying test database for alias 'default'...
我们现在几乎完成了,但由于我们的后端将与使用不同端口的前端通信,还有两个额外的考虑因素。这引发了一系列安全问题,我们现在将解决。
CORS¶
跨源资源共享(CORS)指的是每当客户端与托管在不同域(mysite.com 与 yoursite.com)或端口(localhost:3000 与 localhost:8000)的 API 交互时,都存在潜在的安全问题。
具体来说,CORS 要求 Web 服务器包含特定的 HTTP 标头,以允许客户端确定是否以及何时允许跨域请求。因为我们使用的是 SPA 架构,所以前端在开发期间将位于不同的本地端口,一旦部署,将位于完全不同的域!
解决这个问题的最简单方法——也是 Django REST Framework 推荐的方法——是使用中间件,它将根据我们的设置自动包含适当的 HTTP 标头。第三方包 django-cors-headers 是 Django 社区中的默认选择,可以轻松添加到我们现有的项目中。
确保使用 Control+c 停止本地服务器,然后使用 Pip 安装 django-cors-headers。
Shell
(.venv) > python -m pip install django-cors-headers~=3.10.0
接下来,在三个位置更新我们的 django_project/settings.py 文件:
- 将 corsheaders 添加到 INSTALLED_APPS
- 在 MIDDLEWARE 中的 CommonMiddleware 上方添加 CorsMiddleware
- 在文件底部创建一个 CORS_ALLOWED_ORIGINS 配置
Code
# django_project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# 3rd party
"rest_framework",
"corsheaders", # new
# Local
"todos.apps.TodosConfig",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware", # new
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
CORS_ALLOWED_ORIGINS = (
"[http://localhost:3000](http://localhost:3000)",
"[http://localhost:8000](http://localhost:8000)",
)
非常重要的一点是,corsheaders.middleware.CorsMiddleware 出现在正确的位置,因为 Django 中间件是自上而下加载的。还要注意,我们已经白名单了两个域:localhost:3000 和 localhost:8000。前者是 React 的默认端口(如果使用的是前端),后者是默认的 Django 端口。
CSRF¶
就像 CORS 在处理 SPA 架构时是一个问题一样,表单也是。Django 带有强大的 CSRF 保护,应该添加到任何 Django 模板中的表单中,但对于专用的 React 前端设置,这种保护本身并不可用。幸运的是,我们可以通过设置 CSRF_TRUSTED_ORIGINS 来允许来自我们前端的特定跨域请求。
在 settings.py 文件的底部,在 CORS_ORIGIN_WHITELIST 旁边,为 React 的默认本地端口 3000 添加这一行:
Code
# django_project/settings.py
CSRF_TRUSTED_ORIGINS = ["localhost:3000"]
就是这样!我们的后端现在已经完成,并能够与任何使用端口 3000 的前端通信。如果我们选择的前端规定了不同的端口,可以很容易地在我们的代码中更新。
后端 API 部署¶
我们将再次使用 Heroku 部署 Django API 后端。如果您回忆起我们在第4章中的图书馆 API 的部署清单,包括以下内容:
- 配置静态文件并安装 WhiteNoise
- 安装 Gunicorn 作为生产 Web 服务器
- 创建 requirements.txt、runtime.txt 和 Procfile 文件
- 更新 ALLOWED_HOSTS 配置
我们现在可以更快地完成这些步骤。对于静态文件,从终端 Shell 创建一个新的静态目录。
Shell
(.venv) > mkdir static
使用您的文本编辑器在静态目录中创建一个 .keep 文件,以便它被 Git 拾取。然后安装 whitenoise 以在生产中处理静态文件。
Shell
(.venv) > python -m pip install whitenoise==5.3.0
WhiteNoise 必须添加到 django_project/settings.py 中的以下位置:
- 在 INSTALLED_APPS 中,django.contrib.staticfiles 上方的 whitenoise
- 在 CommonMiddleware 上方的 WhiteNoiseMiddleware
- 指向 WhiteNoise 的 STATICFILES_STORAGE 配置
Code
# django_project/settings.py
INSTALLED_APPS = [
...
"whitenoise.runserver_nostatic", # new
"django.contrib.staticfiles",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", # new
"corsheaders.middleware.CorsMiddleware",
...
]
STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR / "static"] # new
STATIC_ROOT = BASE_DIR / "staticfiles" # new
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" # new
最后运行 collectstatic 命令,以便所有静态目录和文件都编译到一个位置以用于部署目的。
Shell
(.venv) > python manage.py collectstatic
Gunicorn 将用作生产 Web 服务器,可以直接安装。
Shell
(.venv) > python -m pip install gunicorn~=20.1.0
使用您的文本编辑器在项目根目录中创建一个 runtime.txt 文件,旁边是 manage.py。它将有一行指定要在 Heroku 上运行的 Python 版本。
runtime.txt
python-3.10.2
现在在同一项目根目录位置创建一个空的 Procfile 文件。它应该包含以下单行命令:
Procfile
web: gunicorn django_project.wsgi --log-file -
我们可以在一个命令中自动生成包含我们虚拟环境内容的 requirements.txt 文件:
Shell
(.venv) > python -m pip freeze > requirements.txt
最后一步是更新 django_project/settings.py 中的 ALLOWED_HOSTS 配置。访问应该限制为 localhost、127.0.0.1 和 .herokuapp.com。
Code
# django_project/settings.py
ALLOWED_HOSTS = [".herokuapp.com", "localhost", "127.0.0.1"]
确保添加并提交新的更改到 Git。
Shell
(.venv) > git status
(.venv) > git add -A
(.venv) > git commit -m "New updates for Heroku deployment"
然后键入命令 heroku login 登录到 Heroku 的 CLI,这将需要您在 Heroku 网站上验证凭据。
Shell
(.venv) > heroku login
heroku: Press any key to open up the browser to login or q to exit:
Opening browser to ...
Logging in... done
Logged in as will@wsvincent.com
登录后,我们需要创建一个新的 Heroku 项目。由于 Heroku 名称是唯一的,您需要提出自己的变体。我称之为 wsvincent-todo。
Shell
(.venv) > heroku create wsvincent-todo
Creating wsvincent-todo... done
[https://wsvincent-todo.herokuapp.com/](https://wsvincent-todo.herokuapp.com/) | [https://git.heroku.com/wsvincent-todo.git](https://git.heroku.com/wsvincent-todo.git)
将代码推送到 Heroku 并添加一个 Web 进程,以便 dyno 正在运行。
Shell
(.venv) > git push heroku main
(.venv) > heroku ps:scale web=1
您的新应用程序的 URL 将在命令行输出中,或者您可以运行 heroku open 来找到它。确保导航到 /api/ 端点以查看所有 Todo 项目的列表。这是我的 Todo API 端点,列出了所有项目:

每个 Todo 项目的单独 API 端点也将在 /api/1/、/api/2/ 等处可用。部署的 Todo API 现在可以使用了。一旦了解了前端代码的部署 URL,就可以根据需要将它们添加到 CORS 和 CSRF 部分。
结论¶
Django REST Framework 用最少的代码允许我们从头开始创建一个 Django API。与我们上一章中的示例不同,我们没有为这个项目构建任何网页,因为我们的目标只是创建一个 API。但在未来的任何时候,我们都可以轻松地做到!这只需要添加一个新的视图、URL 和一个模板来暴露我们现有的数据库模型。
这个例子中的一个重要一点是,我们添加了 CORS 标头,并明确设置了只有域 localhost:3000 和 localhost:8000 才能访问我们的 API。正确设置 CORS 标头是您在第一次开始构建 API 时容易混淆的事情。
我们以后还可以做更多的配置,但归根结底,创建 Django API 是关于创建模型、编写一些 URL 路由,然后添加 Django REST Framework 的序列化器和视图提供的一点魔法。