本文介绍REST API使用已有的HTTP功能(GET、POST、PUT、与DELETE)创建、更新、获取、以及删除WEB资源的设计,探讨如何在Django中利用RESTful架构的威力。
本文介绍REST API使用已有的HTTP功能(GET、POST、PUT、与DELETE)创建、更新、获取、以及删除WEB资源的设计,探讨如何在Django中利用RESTful架构的威力。
Roy Fielding
在其博士论文( http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm )中介绍了 Web 服务的 REST 架构方式,并列出了 6 个符合这一架构定义的特征。
客户端和服务器之间必须有明确的界线。
客户端发出的请求中必须包含所有必要的信息。服务器不能在两次请求之间保存客户端的任何状态。
服务器发出的响应可以标记为可缓存或不可缓存,这样出于优化目的,客户端(或客户端和服务器之间的中间服务)可以使用缓存。
客户端访问服务器资源时使用的协议必须一致,定义良好,且已经标准化。REST Web 服务最常使用的统一接口是 HTTP 协议。
在客户端和服务器之间可以按需插入代理服务器、缓存或网关,以提高性能、稳定性和伸缩性。
客户端可以选择从服务器上下载代码,在客户端的环境中执行。
资源是REST
架构方式的核心概念,在REST
架构中,每个资源都有一个地址,都可以使用唯一的 URL
表示。资源本身都是方法调用的目标,方法列表对所有资源都是一样的。这些方法都是标准方法,包括HTTP之GET
、POST
、PUT
、DELETE
,还可能包括HEADER
和OPTIONS
。
客户端程序在建立起的资源 URL
上发送请求,使用请求方法表示期望的操作。REST
的原则是使用HTTP
请求方法控制资源,这些方法(GET
,POST
,PUT
,DELETE
)具有特殊含义:
资源集合的 URL, 获取资源的集合(如果服务器实现了分页,就是一页中的资源),从而得到 citys
的列表。
单个资源的 URL,获取目标资源,得到一个单独的city。
资源集合的 URL,创建新资源,并将其加入目标集合。服务器为新资源指派 URL,创建一个新的city。
单个资源的 URL,修改一个现有资源。更新city。
单个资源的 URL,删除一个资源,删除一个city对象实例。
资源集合的 URL,删除目标集合中的所有资源。清除city的所有对象。
REST 架构不要求必须为一个资源实现所有的请求方法。如果资源不支持客户端使用的请求方法,响应的状态码为 405,返回“不允许使用的方法”。
REST
通过URL
提供对资源的增删改查
。因此在前端程序中通过调用REST
服务提供的 URL
就可以实现针对资源的增删改查
。
在请求和响应的主体中,资源在客户端和服务器之间来回传送,但 REST
没有指定编码资源的方式。请求和响应中的 Content-Type
首部用于指明主体中资源的编码方式。使用 HTTP
协议中的内容协商机制,可以找到一种客户端和服务器都支持的编码方式。
REST Web 服务常用的两种编码方式是 JavaScript 对象表示法(JavaScript Object Notation, JSON)和可扩展标记语言(Extensible Markup Language,XML)。对基于 Web 的 RIA
(Rich Internet Applications) 来说,JSON 更具吸引力,因为 JSON 和 JavaScript 联系紧密,而 JavaScript 是 Web 浏览器使用的客户端脚本语言。
在设计良好的 REST API
中,客户端只需知道几个顶级资源的 URL
,其他资源的 URL
则从响应中包含的链接上发掘。
在传统的以服务器为中心的 Web
程序中,服务器完全掌控程序。更新服务程序时,只需在服务器上部署新版本就可针对所有的用户实现更新,因为运行在用户 Web
浏览器中的那部分程序
也是从服务器上下载的。
但升级 RIA
和 Web
服务要复杂得多,因为客户端程序
和服务器上的程序
是独立开发的, 有时甚至由不同的人进行开发。Web
服务的容错能力要比一般的 Web
程序强,而且还要保证旧版客户端能继续使用。这一问题的常见解决办法是使用版本
区分 Web
服务所处理的 URL
。例如, 首次发布的Web 服务可以通过 /api/v1.0/citys/
提供城市的列表。
在 URL 中加入 Web 服务的版本有助于条理化管理新旧功能,让服务器能为新客户端提供 新功能,同时继续支持旧版客户端。提供多版本支持会增加服务器的维护负担,但在某些情况下,这是不破坏现有部署且能让 程序不断发展的唯一方式。
Django社区中有大量可复用的应用模块,本文选择django-rest-framework
应用模块进行说明,该应用支持从 ORM
或非ORM
数据创建资源,支持可插拔的认证和许可,还支持包括JSON
、XML
、YAML
和HTML
在内的多种序列化编码方法。
这里继续以城市景点
项目为例,在当前项目scenic_spot
路径下进行模块安装:
pip install djangorestframework
pip install django-filter
pip install markdown
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'widget_tweaks',
'rest_framework', # <--- 新添应用
'rest_framework.authtoken', # <--- 新添应用
'cityspot',
],
...
与 Django 同时提供 Form
类和 ModelForm
类的方式相同,REST 框架同时包含 Serializer
类和 ModelSerializer
类。使用 ModelSerializer 类重构我们的序列化器。在cityspot
目录中创建一个名为 serializers.py
的文件并添加以下内容。
from rest_framework import serializers
from .models import City, Spot
class CitySerializer(serializers.ModelSerializer):
class Meta:
model = City
fields = ['id', 'name', 'description', ]
class SpotSerializer(serializers.ModelSerializer):
class Meta:
model = Spot
fields = ('id', 'name', 'description', 'city', )
重要的是要记住 ModelSerializer
类没有做任何特别神奇的事情,它们只是创建序列化器类的快捷方式:
create()
和 update()
方法的简单默认实现。REST框架(REST framework
) 引入了一个 Request
对象,扩展了常规的 HttpRequest
,并提供了更灵活的请求解析。 Request
对象的核心功能是 request.data
属性,它类似于 request.POST
,但对于使用 Web API
更有用。
request.POST # 只处理表单数据。 仅适用于“POST”方法。
request.data # 处理任意数据。 适用于“POST”、“PUT”和“PATCH”方法。
REST框架(REST framework
)还引入了一个 Response
对象,它是一种 TemplateResponse
类型,它接受未渲染的内容并使用内容协商来确定正确的内容类型以返回给客户端。
return Response(data) # 呈现为客户端请求的内容类型。
在视图中使用数字 HTTP 状态代码并不总是能够一目了然,而且如果得到错误代码,很容易忽略。 REST框架(REST framework
) 为每个状态代码提供了更明确的标识符,例如状态模块中的 HTTP_400_BAD_REQUEST
。 最好始终使用这些标识符而不是使用数字码。
REST 框架提供了两个可用于编写 API 视图的包装器。
1. @api_view 装饰器 # 用于处理基于函数的视图的装饰器。
2. APIView 类 # 用于处理基于类的视图的基类。
这些包装器提供了一些功能,例如确保在视图中接收 Request
实例,并向 Response
对象添加上下文以便可以执行内容协商。
包装器还提供了一些行为,例如在适当的时候返回 405 Method Not Allowed
响应,并处理在使用格式错误的输入访问 request.data
时发生的任何 ParseError
异常。
为了区分常规视图,在cityspot
目录下,新增api_views.py
文件,并在其中添加以下内容。
from .models import City, Spot
from .serializers import CitySerializer, SpotSerializer
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(['GET', 'POST'])
def city_list(request):
"""
列出所有城市,或创建一个新城市。
"""
if request.method == 'GET':
citys = City.objects.all()
serializer = CitySerializer(citys, many=True)
return Response(serializer.data)
elif request.method == 'POST':
data=request.data
serializer = CitySerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['GET', 'PUT', 'DELETE'])
def city_detail(request, pk):
"""
检索、更新或删除城市。
"""
try:
city = City.objects.get(pk=pk)
except City.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
serializer = CitySerializer(city)
return Response(serializer.data)
elif request.method == 'PUT':
data = request.data
serializer = CitySerializer(city, data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
elif request.method == 'DELETE':
city.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@api_view(['GET', 'POST'])
def spot_list(request,cpk):
"""
列出某个城市的景点,或创建一个城市的新景点。
"""
try:
city = City.objects.get(pk=cpk)
except City.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
queryset = city.spots.order_by('pk')
serializer = SpotSerializer(queryset, many=True)
return Response(serializer.data)
elif request.method == 'POST':
data=request.data
serializer = SpotSerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['GET', 'PUT', 'DELETE'])
def spot_detail(request, cpk, pk):
"""
检索、更新或删除景点。
"""
try:
city = City.objects.get(pk=cpk)
spot = Spot.objects.get(pk=pk)
spot.city = city
except (City.DoesNotExist, Spot.DoesNotExist) as N:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
serializer = SpotSerializer(spot)
return Response(serializer.data)
elif request.method == 'PUT':
data = request.data
serializer = SpotSerializer(spot, data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
elif request.method == 'DELETE':
spot.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
我们的API 的根将是一个支持列出所有现有城市或创建新城市的视图,还需要一个对应于单个城市的视图,并且可以用于检索、更新或删除一个城市。 现在的代码感觉与我们使用 Forms API 非常相似。 我们还使用了命名状态码,这使得响应的含义更加明显。
请注意,我们不再明确地将我们的请求或响应绑定到给定的内容类型。 request.data
可以处理传入的 json
请求,但它也可以处理其他格式。 同样,我们正在返回带有数据的响应对象,但允许 REST 框架
将响应呈现为我们正确的内容类型。
也可以使用基于类的视图而不是基于函数的视图来编写我们的 API 视图。 正如即将看到的,这是一个强大的模式,它允许重用通用功能。
使用基于类的视图重写我们的 API,首先将根视图重写为基于类的视图。 所有这些都涉及到对api_views.py
的一点重构。
from .models import City, Spot
from .serializers import CitySerializer, SpotSerializer
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from django.http import Http404
from rest_framework.views import APIView
class CityList(APIView):
"""
列出所有城市,或创建一个新城市。
"""
def get(self, request):
citys = City.objects.all()
serializer = CitySerializer(citys, many=True)
return Response(serializer.data)
def post(self, request):
serializer = CitySerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class CityDetail(APIView):
"""
检索、更新或删除一个城市对象。
"""
def get_object(self, pk):
try:
return City.objects.get(pk=pk)
except City.DoesNotExist:
raise Http404
def get(self, request, pk):
city = self.get_object(pk)
serializer = CitySerializer(city)
return Response(serializer.data)
def put(self, request, pk):
city = self.get_object(pk)
serializer = CitySerializer(city, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk):
city = self.get_object(pk)
city.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class SpotList(APIView):
"""
列出某个城市的景点,或创建一个城市的新景点。
"""
def get_object(self, cpk):
try:
return City.objects.get(pk=cpk)
except City.DoesNotExist:
raise Http404
def get(self, request, cpk):
city = self.get_object(cpk)
queryset = city.spots.order_by('pk')
serializer = SpotSerializer(queryset, many=True)
return Response(serializer.data)
def post(self, request, cpk):
serializer = SpotSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class SpotDetail(APIView):
"""
检索、更新或删除特定城市的景点。
"""
def get_object(self, cpk, pk):
try:
city = City.objects.get(pk=cpk)
spot = Spot.objects.get(pk=pk)
spot.city = city
return spot
except (City.DoesNotExist,Spot.DoesNotExist):
raise Http404
def get(self, request, cpk, pk):
spot = self.get_object(cpk, pk)
serializer = SpotSerializer(spot)
return Response(serializer.data)
def put(self, request, cpk, pk):
spot = self.get_object(cpk, pk)
serializer = SpotSerializer(spot, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, cpk, pk):
spot = self.get_object(cpk, pk)
spot.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
这看起来不错。 同样,它与基于函数的视图非常相似。
我们需要将这些视图连接起来。 基于FBV
修订cityspot/urls.py
文件:
from django.urls import re_path, path
from . import views
from . import api_views
urlpatterns = [
re_path(r'^$', views.CitysView.as_view(),name='home'),
re_path(r'^newcity$', views.CityView.as_view(),name='newcity'),
re_path(r'^addcity$', views.CityView.as_view(),name='addcity'),
re_path(r'^citys/(?P<id>\d+)$', views.SpotsView.as_view(), name='city_spots'),
re_path(r'^citys/(?P<id>\d+)/newspot$', views.SpotView.as_view(), name='newspot'),
re_path(r'^addspot$', views.SpotView.as_view(),name='addspot'),
path('api/citys/', api_views.city_list),
path('api/citys/<int:pk>/', api_views.city_detail),
path('api/citys/<int:cpk>/spots/', api_views.spot_list),
path('api/citys/<int:cpk>/spots/<int:pk>/', api_views.spot_detail),
]
基于CBV
修订cityspot/urls.py
文件:
from django.urls import re_path, path
from . import views
from . import api_views
urlpatterns = [
...
path('api/citys/', api_views.CityList.as_view()),
path('api/citys/<int:pk>/', api_views.CityDetail.as_view()),
path('api/citys/<int:cpk>/spots/', api_views.SpotList.as_view()),
path('api/citys/<int:cpk>/spots/<int:pk>/', api_views.SpotDetail.as_view()),
]
启动 Django 的开发服务器:
python scenic_spot.py runserver
在另一个终端窗口中,我们可以测试服务器。可以使用 curl
或 httpie
测试当前的 API。 Httpie
是一个用 Python 编写的用户友好的 http
客户端。 可以使用 pip
安装 httpie
:
pip install httpie
最后,我们可以获得所有城市的列表:
http http://localhost:8000/cityspot/api/citys/
HTTP/1.1 200 OK
Content-Length: 72
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 19 Jun 2022 21:28:09 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.1
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
[
{
"description": "五羊城",
"id": 1,
"name": "广州"
}
]
或者我们可以通过引用它的 id
来获得一个特定的城市:
http http://localhost:8000/cityspot/api/citys/1/
HTTP/1.1 200 OK
...
{
"description": "五羊城",
"id": 1,
"name": "广州"
}
或者我们可以通过特定的城市的 id
来获得该城市的景点:
http http://localhost:8000/cityspot/api/citys/1/spots/
HTTP/1.1 200 OK
...
[
{
"city": 1,
"description": "广州白云山",
"id": 1,
"name": "广州白云山"
},
{
"city": 1,
"description": "广州中山纪念堂",
"id": 2,
"name": "中山纪念堂"
},
{
"city": 1,
"description": "位于广州市的广东科学中心",
"id": 3,
"name": "广东科学中心"
},
{
"city": 1,
"description": "位于广州番禺的长隆旅游度假区",
"id": 4,
"name": "长隆旅游度假区"
},
{
"city": 1,
"description": "广州陈家祠",
"id": 5,
"name": "陈家祠"
},
{
"city": 1,
"description": "位于广州越秀区的黄花岗七十二烈士陵园",
"id": 6,
"name": "黄花岗七十二烈士陵园"
},
{
"city": 1,
"description": "位于广州的华南植物园",
"id": 7,
"name": "华南植物园"
},
{
"city": 1,
"description": "位于广州越秀区的越秀公园",
"id": 8,
"name": "越秀公园"
},
{
"city": 1,
"description": "位于广州的碧水湾",
"id": 9,
"name": "碧水湾"
},
{
"city": 1,
"description": "位于广州番禺的香江野生动物世界",
"id": 10,
"name": "香江野生动物世界"
}
]
或者可以通过城市 id
与景点 id
来获得一个特定城市的特定景点:
http http://localhost:8000/cityspot/api/citys/1/spots/1/
HTTP/1.1 200 OK
...
{
"city": 1,
"description": "广州白云山",
"id": 1,
"name": "广州白云山"
}
同样,您可以通过在 Web 浏览器中访问这些 URL 来显示相同的城市与景点。
到目前为止,我们有一个感觉与 Django 的 Forms API
非常相似的序列化 API(Serializer API
),以及一些常规的 Django 视图。我们的 API 视图并没有做任何特别的事情,除了提供 json 响应之外,它是一个正常运行的 Web API
。
博文最后更新时间: