1. 初识Django REST framework (DRF)

1.1 DRF开发预备

首先在命令行创建博客文章的App:

(venv) > python manage.py startapp article

创建一个简单的博客文章模型:

# article/models.py

from django.db import models
from django.utils import timezone


class Article(models.Model):
"""
博客文章
"""
title = models.CharField(max_length=100) # 标题
body = models.TextField() # 正文
created = models.DateTimeField(default=timezone.now) # 创建时间
updated = models.DateTimeField(auto_now=True) # 更新时间

def __str__(self):
return self.title

Django有一个非常优秀的库 Django REST framework (简称 DRF ),可以帮助我们封装好序列化的底层实现,让开发人员专注于业务本身。

安装 DRF 及其他依赖库:

(venv) > pip install djangorestframework=3.14.0
(venv) > pip install markdown==3.4.3
(venv) > pip install django-filter==23.1

然后将App注册到列表:

# backend/settings.py

INSTALLED_APPS = [
...

'rest_framework',
'article',
]

接着添加 DRF 的登录视图,以便 DRF 自动为你的可视化接口页面生成一个用户登录的入口:

# backend/urls.py

...
from django.urls import include

urlpatterns = [
...
path('api-auth/', include('rest_framework.urls')),
]

最后进行数据迁移:

(venv) > python manage.py makemigrations
(venv) > python manage.py migrate

准备工作做好了。

1.2 序列化与Django

前后端分离的核心思想之一是两端交互不通过模板语言,而只传输需要的数据。

在 Django 程序的运行过程中,变量都是存储在服务器的内存中,而且,后端 Django 程序存储的是 Python 变量,而前端浏览器中是 Javascript 变量,两者是无法直接通过网络进行传递和交流的,因此需要规定一个“标准格式”,前后端都根据这个标准格式,对资源进行保存、读取、传输等操作。

JSON就是这种标准格式之一,它很轻量,表示出来就是个字符串,可以直接被几乎所有的语言读取、转换,非常方便。

举个例子,把 Python 对象转化为 JSON 的过程叫做序列化(Serialization),把 JSON 对象转化为 Python 对象的过程叫做反序列化(Deserialization)。

>>> import json
>>> person = dict(name='Trump', age=82) # Python对象
>>> json.dumps(person) # 序列化
'{"name": "Trump", "age": 82}' # 字符串

>>> json_str = '{"name": "Trump", "age": 82}'
>>> json.loads(json_str) # 反序列化
{'name': 'Trump', 'age': 82} # Python对象

总之,把变量从内存中变为可存储或传输的过程称之为序列化,反过来把变量内容从序列化对象重新读到内存中称之为反序列化

回顾 Django 传统流程对一个网络请求的处理:

def a_list(request):
articles = Article.objects.all()
return render(..., context={'articles': articles})

视图函数将数据作为上下文返回,通过模板引擎将上下文渲染为页面中的数据。

Restful 的处理流程仅增加了一步,即对数据序列化处理:

def a_list(request):
articles = Article.objects.all()
# 序列化数据
serializer = Serializer(article, many=True)
return JsonResponse(serializer.data, safe=False)

数据被序列化为 JSON 字符串,直接交由前端处理。这就是前后端分离的雏形,后端提供数据,前端专注于操作数据、渲染页面。

前后端分离关联的新概念:Rest(表现层状态转化) 和 Restful。Restful 架构是指客户端和服务器之间的交互、操作符合 Rest 规范,即:每一个URI代表一种资源;客户端和服务器之间,传递资源的表现层;客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。

1.3 编写文章列表接口

按照该思路,写一个文章列表的接口:

# article/views.py

from django.http import JsonResponse
from article.models import Article
from article.serializers import ArticleListSerializer # ArticleListSerializer 后面会写


def article_list(request):
articles = Article.objects.all()
serializer = ArticleListSerializer(articles, many=True)
return JsonResponse(serializer.data, safe=False)

接口代码一共就三行:

  • 取出所有文章的QuerySet
  • 根据QuerySet数据,创建一个序列化器
  • 将序列化后的数据以 JSON 的形式返回

因此,返回的数据不再是传统的模板数据,而是 JSON 数据。

补充ArticleListSerializer的代码:

# article/serializers.py

from rest_framework import serializers


class ArticleListSerializer(serializers.Serializer):
"""
文章列表序列化类
"""
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(allow_blank=True, max_length=100)
body = serializers.CharField(allow_blank=True)
created = serializers.DateTimeField()
updated = serializers.DateTimeField()

序列化类看起来类似 Django 的 Form 表单类型,它指定了接口数据中各个字段的具体类型,自动对请求和响应中的数据进行序列化和反序列化转换。其底层实现逻辑已经由 DRF 框架封装好了。

接下来将各级 urls.py 配置好。

# backend/urls.py

...
urlpatterns = [
...
path('api/article/', include('article.urls', namespace='article')),
]

# article/urls.py

from django.urls import path
from article import views


app_name = 'article'

urlpatterns = [
path('', views.article_list, name='list'),
]

接下来创建一个管理员用户:

(env) > python manage.py createsuperuser
Username: admin
Email address: admin@example.com
Password:
Password (again):
Superuser created successfully.

Artile数据表注册到后台中

# article/admin.py

from django.contrib import admin
from article.models import Article


admin.site.register(Article)

命令行执行python manage.py runserver启动服务器,并在浏览器中访问 http://127.0.0.1:8000/admin/ ,登录后在后台中随意给 article 添加几个测试数据,并在浏览器中访问 http://127.0.0.1:8000/api/article/,可以看到页面中返回的 Json 字符串如下:

[{"id": 1, "title": "My first post", "body": "First post body ...", "created": "2023-03-27T00:07:00+08:00", "updated": "2023-03-27T00:09:26.036519+08:00"}, {"id": 2, "title": "Another post", "body": "Another post body ...", "created": "2023-03-27T00:09:00+08:00", "updated": "2023-03-27T00:09:52.545659+08:00"}, {"id": 3, "title": "3rd article", "body": "The 3rd article body ...", "created": "2023-03-27T00:09:00+08:00", "updated": "2023-03-27T00:10:14.199362+08:00"}]

到此,你已经完成了一个简单的接口。

2. 序列化器与视图

2.1 ModelSerializer

# article/serializers.py

from rest_framework import serializers


class ArticleListSerializer(serializers.Serializer):
"""
文章列表序列化类
"""
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(allow_blank=True, max_length=100)
body = serializers.CharField(allow_blank=True)
created = serializers.DateTimeField()
updated = serializers.DateTimeField()

# article/models.py

from django.db import models
from django.utils import timezone


class Article(models.Model):
"""
博客文章
"""
title = models.CharField(max_length=100) # 标题
body = models.TextField() # 正文
created = models.DateTimeField(default=timezone.now) # 创建时间
updated = models.DateTimeField(auto_now=True) # 更新时间

def __str__(self):
return self.title

上文中ArticleListSerializer序列化器类长得跟Artile类模型非常像,如果可以再简化下就好了,DRF提供了ModelSerializer用于简化序列化器。

# article/serializers.py

from rest_framework import serializers
from article.models import Article


class ArticleListSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ['id', 'title', 'created']

ModelSerializer的功能与Serializer基本一致,不同的是它还做了额外的工作:

  • 自动推断需要序列化的字段及类型
  • 提供对字段数据的验证器的默认实现
  • 提供了修改数据需要用到的.create().update()方法的默认实现
  • fields列表中挑选出需要的数据,可减少数据的体积

重新访问 http://127.0.0.1:8000/api/article/,页面呈现的数据如下:

[{"id": 1, "title": "My first post", "created": "2023-03-27T00:07:00+08:00"}, {"id": 2, "title": "Another post", "created": "2023-03-27T00:09:00+08:00"}, {"id": 3, "title": "3rd article", "created": "2023-03-27T00:09:00+08:00"}]

可以看到 JSON 数据仅包含fields规定的字段。

2.2 APIView

除了对序列化器的支持以外,DRF 还提供了对视图的扩展,以便视图更好的为接口服务,将文章的视图修改如下:

# article/views.py

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from article.models import Article
from article.serializers import ArticleListSerializer


@api_view(['GET', 'POST'])
def article_list(request):
if request.method == 'GET':
articles = Article.objects.all()
serializer = ArticleListSerializer(articles, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)

elif request.method == 'POST':
serializer = ArticleListSerializer(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)

主要的变化如下:

  • @api_view 装饰器允许视图接收 GETPOST 请求,以及提供如 405 Method Not Allowed 等默认实现,以便在不同的请求下进行正确的响应。
  • 返回了 Response ,该对象由 Django 原生响应体扩展而来,它可以根据内容协商来确定返回给客户端的正确内容类型。如果数据验证有误,还可以返回适当的状态码以表示当前的情况。

刷新文章列表接口,出现了可视化的接口界面:

可视化的接口界面

这是因为视图中 Response 提供的内容协商能力,Django 后端根据客户端请求响应的内容类型不同,自动选择适合的表现形式;浏览器请求资源时,就返回可视化的 HTML 资源表示,其他形式请求时,又可以返回 Json 纯数据的形式,给开发人员带来极大的方便。

2.3 测试接口

验证它是不是真的会返回 JSON 数据,可以从命令行使用诸如 curlhttpie访问,如:

(venv) > curl http://127.0.0.1:8000/api/article
[{"id":1,"title":"My first post","created":"2023-03-27T00:07:00+08:00"},{"id":2,"title":"Another post","created":"2023-03-27T00:09:00+08:00"},{"id":3,"title":"3rd article","created":"2023-03-27T00:09:00+08:00"}]
(venv) > pip install httpie
(venv) > http http://127.0.0.1:8000/api/article/
HTTP/1.1 200 OK
Allow: OPTIONS, GET, POST
Connection: close
Content-Length: 211
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 16:47:49 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

[
{
"created": "2023-03-27T00:07:00+08:00",
"id": 1,
"title": "My first post"
},
{
"created": "2023-03-27T00:09:00+08:00",
"id": 2,
"title": "Another post"
},
{
"created": "2023-03-27T00:09:00+08:00",
"id": 3,
"title": "3rd article"
}
]

再试试新建文章:

(venv) > http POST http://127.0.0.1:8000/api/article/ title=PostByJson body=HelloWorld!
HTTP/1.1 201 Created
Allow: OPTIONS, GET, POST
Connection: close
Content-Length: 74
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 16:53:33 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
"created": "2023-03-27T00:53:33.637185+08:00",
"id": 4,
"title": "PostByJson"
}

浏览文章列表、新建文章接口就完成了。

推荐使用Postman进行可视化接口测试和管理。

3. 基于类的视图

3.1 类视图

DRF 中也有基于类的视图的存在,可用于实现功能的模块化继承、封装,减少重复代码。

# article/views.py

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from rest_framework.views import APIView

from django.http import Http404

from article.models import Article
from article.serializers import ArticleListSerializer, ArticleDetailSerializer


class ArticleDetail(APIView):
"""
文章详情视图,处理文章的`get`, `put`, `delete`请求
"""

def get_object(self, pk):
try:
return Article.objects.get(pk=pk)
except:
raise Http404

def get(self, request, pk):
article = self.get_object(pk)
serializer = ArticleDetailSerializer(article)
return Response(serializer.data)

def put(self, request, pk):
article = self.get_object(pk)
serializer = ArticleDetailSerializer(article, 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):
article = self.get_object(pk)
article.delete()
return Response(status=status.HTTP_204_NO_CONTENT)


@api_view(['GET', 'POST'])
def article_list(request):
if request.method == 'GET':
articles = Article.objects.all()
serializer = ArticleListSerializer(articles, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)

elif request.method == 'POST':
serializer = ArticleListSerializer(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)


代码中提供了对文章详情的获取、修改和删除的3个方法,以及1个用户获取单个文章的辅助方法,DRF 类视图与传统的 Django 的区别是.get().put()多了一个将对象序列化(反序列化)的步骤,而.delete()方法因为不用返回实际数据,因此执行完删除动作就OK。

从这个地方就可以看出,序列化器 serializer 不仅可以将数据进行序列化、反序列化,还包含数据验证、错误处理、数据库操作等能力。

序列化这个概念与具体语言无关。Python 或 JavaScript 对象转换为 Json 都称为序列化,反之为反序列化。Json 是两种语言传输信息的桥梁,一但信息到达,对方都需要将其还原为自身的数据结构。

由于详情接口需要返回完整的数据,所以新增一个序列化器:

# article/serializers.py

from rest_framework import serializers
from article.models import Article


class ArticleListSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ['id', 'title', 'created']


class ArticleDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__'

配置urls.py:

# article/urls.py

from django.urls import path
from article import views

app_name = 'article'

urlpatterns = [
path('', views.article_list, name='list'),
path('<int:pk>/', views.ArticleDetail.as_view(), name='detail')
]

测试:

# 测试请求文章接口
(venv) > http http://127.0.0.1:8000/api/article/1/
HTTP/1.1 200 OK
Allow: GET, POST, DELETE, HEAD, OPTIONS
Connection: close
Content-Length: 70
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 17:14:50 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
"body": "First post body ...",
"created": "2023-03-27T00:07:00+08:00",
"id": 1,
"title": "My first post",
"updated": "2023-03-27T00:07:00+08:00",
}

# 测试修改文章接口
(venv) > http PUT http://127.0.0.1:8000/api/article/1/ title=somthing... body=changed...
HTTP/1.1 200 OK
Allow: GET, PUT, DELETE, HEAD, OPTIONS
Connection: close
Content-Length: 133
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 17:20:06 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
"body": "changed...",
"created": "2023-03-27T00:07:00+08:00",
"id": 1,
"title": "somthing...",
"updated": "2023-03-27T01:20:06.079919+08:00"
}

# 测试删除文章接口
(venv) > http DELETE http://127.0.0.1:8000/api/article/1/
HTTP/1.1 204 No Content
Allow: GET, PUT, DELETE, HEAD, OPTIONS
Connection: close
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 17:21:13 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

3.2 通用视图

对数据的增删改查是几乎每个项目的通用操作,因此可以通过 DRF 提供的 Mixin 类直接集成对应的功能。

# article/views.py

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status, mixins, generics

from article.models import Article
from article.serializers import ArticleListSerializer, ArticleDetailSerializer


class ArticleDetail(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, generics.GenericAPIView):
"""
文章详情视图,处理文章的`get`, `put`, `delete`请求
"""

queryset = Article.objects.all()
serializer_class = ArticleDetailSerializer

def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)

def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)

def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)


@api_view(['GET', 'POST'])
def article_list(request):
"""
文章列表视图,处理文章的`post`和文章列表的`get`请求
"""
if request.method == 'GET':
articles = Article.objects.all()
serializer = ArticleListSerializer(articles, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)

elif request.method == 'POST':
serializer = ArticleListSerializer(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)

还可以进一步简化

# article/views.py

from rest_framework import generics

from article.models import Article
from article.serializers import ArticleListSerializer, ArticleDetailSerializer


class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
"""
文章详情视图,处理文章的`get`, `put`, `delete`请求
"""

queryset = Article.objects.all()
serializer_class = ArticleDetailSerializer


class ArticleList(generics.ListCreateAPIView):
"""
文章列表视图,处理文章的`post`和文章列表的展示请求
"""
queryset = Article.objects.all()
serializer_class = ArticleListSerializer


功能和最开头那个继承 APIView 的视图是完全相同的。

除了上述介绍的以外,框架还提供 ListModelMixinCreateModelMixin 等混入类或通用视图,覆盖了基础的增删改查需求。

4. 用户权限

权限是Web应用的重要组成部分,在 DRF 中可以进行权限管理。

4.1 文章与用户

依靠用户身份来限制权限,作者与文章是一对多的关系,需要给文章模型添加用户外键,确定每篇文章的作者。保险起见,首先删除现有的所有文章数据。

修改文章的 model,让每篇文章都对应一个作者:

# article/models.py


from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User


class Article(models.Model):
"""
博客文章
"""
author = models.ForeignKey(
User,
null=True,
on_delete=models.CASCADE,
related_name="articles"
) # 作者
title = models.CharField(max_length=100) # 标题
body = models.TextField() # 正文
created = models.DateTimeField(default=timezone.now) # 创建时间
updated = models.DateTimeField(auto_now=True) # 更新时间

def __str__(self):
return self.title

执行迁移

(venv) > python manage.py makemigrations
(venv) > python manage.py migrate

启动服务后查看当前文章列表:

(venv) > http http://127.0.0.1:8000/api/article/                                
HTTP/1.1 200 OK
Allow: GET, POST, HEAD, OPTIONS
Connection: close
Content-Length: 2
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 17:50:07 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

[]

4.2 权限控制

DRF 内置了IsAuthenticatedIsAdminUserAllowAny等权限控制类,个人博客只允许管理员发布文章。修改文章列表视图如下:

# article/views.py

from rest_framework import generics
from rest_framework.permissions import IsAdminUser

from article.models import Article
from article.serializers import ArticleListSerializer, ArticleDetailSerializer


class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
"""
文章详情视图,处理文章的`get`, `put`, `delete`请求
"""

queryset = Article.objects.all()
serializer_class = ArticleDetailSerializer


class ArticleList(generics.ListCreateAPIView):
"""
文章列表视图,处理文章的`post`和文章列表的`get`请求
"""
queryset = Article.objects.all()
serializer_class = ArticleListSerializer
permission_classes = [IsAdminUser]

测试一下

(venv) > http http://127.0.0.1:8000/api/article/   
HTTP/1.1 403 Forbidden
Allow: GET, POST, HEAD, OPTIONS
Connection: close
Content-Length: 43
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 18:04:30 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
"detail": "Authentication credentials were not provided."
}

(venv) > http POST http://127.0.0.1:8000/api/article/ title=may body=notSuccess
HTTP/1.1 403 Forbidden
Allow: GET, POST, HEAD, OPTIONS
Connection: close
Content-Length: 43
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 18:05:02 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
"detail": "Authentication credentials were not provided."
}

可以看到,权限控制确实起作用了,但是目前只有管理员才能查看文章,因此可以自定义一个权限类,新建article/permissions.py文件,写入:

# article/permissions.py

from rest_framework import permissions


class IsAdminUserOrReadOnly(permissions.BasePermission):
"""
允许管理员进行修改,其他用户仅可查看
"""
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True

return request.user.is_superuser

定义的权限类继承了 BasePermission 类,并实现了父类中的钩子方法 def has_permission。此方法在每次请求到来时被唤醒执行,里面简单判断了请求的种类是否安全(即不更改数据的请求),如果安全则直接通过,不安全则只允许管理员用户通过。

再次修改视图:

# article/views.py

from rest_framework import generics
from article.permissions import IsAdminUserOrReadOnly

from article.models import Article
from article.serializers import ArticleListSerializer, ArticleDetailSerializer


class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
"""
文章详情视图,处理文章的`get`, `put`, `delete`请求
"""

queryset = Article.objects.all()
serializer_class = ArticleDetailSerializer
permission_classes = [IsAdminUserOrReadOnly]


class ArticleList(generics.ListCreateAPIView):
"""
文章列表视图,处理文章的`post`和文章列表的`get`请求
"""
queryset = Article.objects.all()
serializer_class = ArticleListSerializer
permission_classes = [IsAdminUserOrReadOnly]

测试用户未登录时:

(venv) D:\WebProject\my_blog\backend>http http://127.0.0.1:8000/api/article/
HTTP/1.1 200 OK
Allow: GET, POST, HEAD, OPTIONS
Connection: close
Content-Length: 2
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 18:17:14 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

[]



(venv) D:\WebProject\my_blog\backend>http POST http://127.0.0.1:8000/api/article/ title="post with permission" body="new test"
HTTP/1.1 403 Forbidden
Allow: GET, POST, HEAD, OPTIONS
Connection: close
Content-Length: 43
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 18:17:23 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
"detail": "Authentication credentials were not provided."
}

在后台中创建一个普通用户 test,用普通用户身份进行请求:

# 普通用户 test,密码 test123321

(venv) > http -a test:test123321 http://127.0.0.1:8000/api/article/
HTTP/1.1 200 OK
Allow: GET, POST, HEAD, OPTIONS
Connection: close
Content-Length: 2
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 18:22:09 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

[]



(venv) >http -a test:test123321 POST http://127.0.0.1:8000/api/article/ title="post with permission" body="new test"
HTTP/1.1 403 Forbidden
Allow: GET, POST, HEAD, OPTIONS
Connection: close
Content-Length: 49
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 18:22:26 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
“detail": "Authentication credentials were not provided."
}

最后,再用管理员用户 admin 测试:

# 普通用户 admin,密码 admin

(venv) > http -a admin:admin http://127.0.0.1:8000/api/article/
HTTP/1.1 200 OK
Allow: GET, POST, HEAD, OPTIONS
Connection: close
Content-Length: 2
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 18:24:09 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

[]



(venv) > http -a admin:admin POST http://127.0.0.1:8000/api/article/ title="post with permission" body="new test"
HTTP/1.1 201 Created
Allow: GET, POST, HEAD, OPTIONS
Connection: close
Content-Length: 84
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 18:24:27 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
"created": "2023-03-27T02:24:27.017416+08:00",
"id": 1,
"title": "post with permission"
}

最终,任何人都可以查看资源;但是新增(CREATE)、更新(PUT)、删除(DELETE)等修改操作就只允许管理员执行。

5. 文章关联用户

5.1 提取用户信息

上文用户以外键关联到文章中,由于author字段允许为空,因此理论上可以发布没有作者的文章。但是,我们可以从Request提取用户信息,把额外的用户信息注入到已有的数据中。

修改视图:

# author/views.py

from rest_framework import generics
from article.permissions import IsAdminUserOrReadOnly

from article.models import Article
from article.serializers import ArticleListSerializer, ArticleDetailSerializer


class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
"""
文章详情视图,处理文章的`get`, `put`, `delete`请求
"""

queryset = Article.objects.all()
serializer_class = ArticleDetailSerializer
permission_classes = [IsAdminUserOrReadOnly]


class ArticleList(generics.ListCreateAPIView):
"""
文章列表视图,处理文章的`post`和文章列表的`get`请求
"""

def perform_create(self, serializer):
serializer.save(author=self.request.user)

queryset = Article.objects.all()
serializer_class = ArticleListSerializer
permission_classes = [IsAdminUserOrReadOnly]

  • 新增的这个 perform_create() 从父类 ListCreateAPIView 继承而来,它在序列化数据真正保存之前调用,因此可以在这里添加额外的数据(即用户对象)。
  • serializer 参数是 ArticleListSerializer 序列化器实例,并且已经携带着验证后的数据。它的 save() 方法可以接收关键字参数作为额外的需要保存的数据。

在命令行测试:

(venv) > http -a admin:admin POST http://127.0.0.1:8000/api/article/ title="post with user" body="new test again" 
HTTP/1.1 201 Created
Allow: GET, POST, HEAD, OPTIONS
Connection: close
Content-Length: 78
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 18:33:54 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
"created": "2023-03-27T02:33:54.420714+08:00",
"id": 2,
"title": "post with user"
}

(venv) > http http://127.0.0.1:8000/api/article/2/
HTTP/1.1 200 OK
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Connection: close
Content-Length: 144
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 18:43:32 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
"author": 1,
"body": "",
"created": "2023-03-27T02:33:54.420714+08:00",
"id": 2,
"title": "post with user",
"updated": "2023-03-27T02:33:54.420714+08:00"
}

但是用户依然可以手动传入一个错误的 author,修改 ArticleListSerializer,序列化器允许你指定只读字段。

# article/serializers.py

from rest_framework import serializers
from article.models import Article


class ArticleListSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ['id', 'title', 'created']
read_only_fields = ['author']


class ArticleDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__'

5.2 显示用户信息

虽然作者外键已经出现在序列化数据中了,但是仅仅显示作者的 id 不太有用,我们更想要的是比如名字、性别等更具体的结构化信息。所以就需要将序列化数据嵌套起来。

新创建一个用户 app:

(venv) > python manage.py startapp user_info

并将新app添加到注册列表:

# backend/settings.py

INSTALLED_APPS = [
...
'user_info',
]

新建user_info/serializers.py文件,写入:

# user_info/serializers.py

from django.contrib.auth.models import User
from rest_framework import serializers


class UserDescSerializer(serializers.ModelSerializer):
"""
文章列表中引用的嵌套用户信息
"""

class Meta:
model = User
fields = [
'id',
'username',
'last_login',
'date_joined'
]

这个序列化器专门用在文章列表中,展示用户的基本信息,最后修改文章列表的序列化器,把它们嵌套到一起:

# article/serializers.py

from rest_framework import serializers
from article.models import Article
from user_info.serializers import UserDescSerializer


class ArticleListSerializer(serializers.ModelSerializer):
author = UserDescSerializer(read_only=True)
class Meta:
model = Article
fields = ['id', 'title', 'created']
# 嵌套序列化器已经设置了只读
# read_only_fields = ['author']


class ArticleDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__'

在命令行测试一下:

(venv) > http http://127.0.0.1:8000/api/article/   
HTTP/1.1 200 OK
Allow: GET, POST, HEAD, OPTIONS
Connection: close
Content-Length: 1355
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 19:03:18 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

[
{
"author": null,
"created": "2023-03-27T02:24:27.017416+08:00",
"id": 1,
"title": "post with permission"
},
{
"author": {
"date_joined": "2023-03-27T02:08:41.097951+08:00",
"id": 1,
"last_login": "2023-03-27T02:08:50.601981+08:00",
"username": "admin"
},
"created": "2023-03-27T02:33:54.420714+08:00",
"id": 2,
"title": "post with user"
},
{
"author": {
"date_joined": "2023-03-27T02:08:41.097951+08:00",
"id": 1,
"last_login": "2023-03-27T02:08:50.601981+08:00",
"username": "admin"
},
"created": "2023-03-27T02:36:12.859805+08:00",
"id": 3,
"title": "post with user"
},
{
"author": {
"date_joined": "2023-03-27T02:08:41.097951+08:00",
"id": 1,
"last_login": "2023-03-27T02:08:50.601981+08:00",
"username": "admin"
},
"created": "2023-03-27T02:37:48.969211+08:00",
"id": 4,
"title": "test body"
}
]

6. 超链接与分页

6.1 超链接

目前文章数据看不出每篇文章的实际url地址,最好 JSON 数据直接提供每篇文件的 url,以后前端用起来就更方便了。实现超链接可以用DRF框架提供的HyperlinkedIdentityField:

# article/serializers.py

from rest_framework import serializers
from article.models import Article
from user_info.serializers import UserDescSerializer


class ArticleListSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name="article:detail")
author = UserDescSerializer(read_only=True)
class Meta:
model = Article
fields = ['url', 'title', 'created', 'author']


class ArticleDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__'

  • HyperlinkedIdentityField 是 DRF 框架提供的超链接字段,只需要你在参数里提供路由的名称,它就自动帮你完成动态地址的映射。
  • view_name 是路由的名称,也就是我们在 path(... name='xxx') 里的那个 name
  • 别忘了在序列化器的 fields 列表里加上 url

在命令行测试:

(venv) > http http://127.0.0.1:8000/api/article/   
HTTP/1.1 200 OK
Allow: GET, POST, HEAD, OPTIONS
Connection: close
Content-Length: 1355
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 19:03:18 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

[
{
"url": "http://127.0.0.1:8000/api/article/1/",
"title": "post with permission",
"created": "2023-03-27T02:24:27.017416+08:00",
"author": null
},
{
"url": "http://127.0.0.1:8000/api/article/2/",
"title": "post with user",
"created": "2023-03-27T02:33:54.420714+08:00",
"author": {
"id": 1,
"username": "admin",
"last_login": "2023-03-27T02:08:50.601981+08:00",
"date_joined": "2023-03-27T02:08:41.097951+08:00"
}
},
{
"url": "http://127.0.0.1:8000/api/article/3/",
"title": "post with user",
"created": "2023-03-27T02:36:12.859805+08:00",
"author": {
"id": 1,
"username": "admin",
"last_login": "2023-03-27T02:08:50.601981+08:00",
"date_joined": "2023-03-27T02:08:41.097951+08:00"
}
},
{
"url": "http://127.0.0.1:8000/api/article/4/",
"title": "test body",
"created": "2023-03-27T02:37:48.969211+08:00",
"author": {
"id": 1,
"username": "admin",
"last_login": "2023-03-27T02:08:50.601981+08:00",
"date_joined": "2023-03-27T02:08:41.097951+08:00"
}
}
]

DRF 框架还提供了一个专门的超链接序列化器 HyperlinkedModelSerializer,大体上跟普通序列化器差不多,不同的是默认以超链接来表示关系字段。详情见官方文档

6.2 分页

DRF 框架继承了 Django 方便易用的传统,分页这种常见功能提供了默认实现。

你只需要在 settings.py 里配置一下就行了:

# backend/settings.py
...

REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 2
}

在命令行测试:

(venv) > http http://127.0.0.1:8000/api/article/
HTTP/1.1 200 OK
Allow: GET, POST, HEAD, OPTIONS
Connection: close
Content-Length: 478
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 19:44:49 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
"count": 4,
"next": "http://127.0.0.1:8000/api/article/?page=2",
"previous": null,
"results": [
{
"author": null,
"created": "2023-03-27T02:24:27.017416+08:00",
"title": "post with permission",
"url": "http://127.0.0.1:8000/api/article/1/"
},
{
"author": {
"date_joined": "2023-03-27T02:08:41.097951+08:00",
"id": 1,
"last_login": "2023-03-27T02:08:50.601981+08:00",
"username": "admin"
},
"created": "2023-03-27T02:33:54.420714+08:00",
"title": "post with user",
"url": "http://127.0.0.1:8000/api/article/2/"
}
]
}

DRF 封装了分页相关的元信息:

  • count:文章总数
  • next:下一页的 url
  • previous:上一页的 url
  • results:实际的数据

试着获取第二页的数据:

(venv) D:\WebProject\my_blog\backend>http http://127.0.0.1:8000/api/article/?page=2
HTTP/1.1 200 OK
Allow: GET, POST, HEAD, OPTIONS
Connection: close
Content-Length: 619
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sun, 26 Mar 2023 19:46:41 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.6
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
"count": 4,
"next": null,
"previous": "http://127.0.0.1:8000/api/article/",
"results": [
{
"author": {
"date_joined": "2023-03-27T02:08:41.097951+08:00",
"id": 1,
"last_login": "2023-03-27T02:08:50.601981+08:00",
"username": "admin"
},
"created": "2023-03-27T02:36:12.859805+08:00",
"title": "post with user",
"url": "http://127.0.0.1:8000/api/article/3/"
},
{
"author": {
"date_joined": "2023-03-27T02:08:41.097951+08:00",
"id": 1,
"last_login": "2023-03-27T02:08:50.601981+08:00",
"username": "admin"
},
"created": "2023-03-27T02:37:48.969211+08:00",
"title": "test body",
"url": "http://127.0.0.1:8000/api/article/4/"
}
]
}

7. 视图集

DRF 框架提供了视图集作为更高层的抽象,可以让代码量进一步的减少。

因为大部分对接口的操作,都是在增删改查的基础上衍生出来的。既然这样,视图集就将这些通用操作集成在一起了。

将之前写的与文章有关的序列化器都注释掉,新增一个提供给视图集的新序列化器:

# article/serializers.py

from rest_framework import serializers
from article.models import Article
from user_info.serializers import UserDescSerializer


# class ArticleListSerializer(serializers.ModelSerializer):
# url = serializers.HyperlinkedIdentityField(view_name="article:detail")
# author = UserDescSerializer(read_only=True)
# class Meta:
# model = Article
# fields = ['url', 'title', 'created', 'author']


# class ArticleDetailSerializer(serializers.ModelSerializer):
# class Meta:
# model = Article
# fields = '__all__'

class ArticleSerializer(serializers.HyperlinkedModelSerializer):
author = UserDescSerializer(read_only=True)

class Meta:
model = Article
fields = '__all__'

序列化器继承的 HyperlinkedModelSerializer 基本上与之前用的 ModelSerializer 差不多,区别是它自动提供了外键字段的超链接,并且默认不包含模型对象的 id 字段。

把之前写的文章视图也全注释掉,并新增代码:

# article/view.py

# from rest_framework import generics
from article.permissions import IsAdminUserOrReadOnly

from article.models import Article
# from article.serializers import ArticleListSerializer, ArticleDetailSerializer


# class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
# """
# 文章详情视图,处理文章的`get`, `put`, `delete`请求
# """

# queryset = Article.objects.all()
# serializer_class = ArticleDetailSerializer
# permission_classes = [IsAdminUserOrReadOnly]


# class ArticleList(generics.ListCreateAPIView):
# """
# 文章列表视图,处理文章的`post`和文章列表的`get`请求
# """

# def perform_create(self, serializer):
# serializer.save(author=self.request.user)

# queryset = Article.objects.all()
# serializer_class = ArticleListSerializer
# permission_classes = [IsAdminUserOrReadOnly]

from rest_framework import viewsets
from article.serializers import ArticleSerializer


class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = [IsAdminUserOrReadOnly]

def perform_create(self, serializer):
serializer.save(author=self.request.user)

视图集类把前面章节写的列表、详情等逻辑都集成到一起,并且提供了默认的增删改查的实现。perform_create() 跟之前一样,在创建文章前,提供了视图集无法自行推断的用户外键字段。

由于使用了视图集,使用框架提供的 Router 类自动处理视图和 url 的连接。

修改项目根路由

# backend/urls.py

from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from article import views


router = DefaultRouter()
router.register(r'article', views.ArticleViewSet)

urlpatterns = [
path('api/', include(router.urls)),

# article/urls.py 可以全注释掉,不需要了
# path('api/article/', include('article.urls', namespace='article')),
]

最后为了让分页更准确,给模型类规定好查询排序:

# article/models.py

from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User


class Article(models.Model):
"""
博客文章
"""
author = models.ForeignKey(
User,
null=True,
on_delete=models.CASCADE,
related_name="articles"
) # 作者
title = models.CharField(max_length=100) # 标题
body = models.TextField() # 正文
created = models.DateTimeField(default=timezone.now) # 创建时间
updated = models.DateTimeField(auto_now=True) # 更新时间

def __str__(self):
return self.title

class Meta:
ordering = ['-created']

浏览器访问http://127.0.0.1:8000/api/,访问到Router 类送给我们的接口导航!

接口导航

顺着导航里给的链接:

文章列表

视图集最大程度地减少需要编写的代码量,并允许你专注于 API 提供的交互和表示形式,而不是 URL 的细节。但并不意味着用它总是比构建单独的视图更好。原因是它的抽象程度太高了。如果你对 DRF 框架的理解不深并且需要做某种定制化业务,可能让你一时间无从下手。

本文标题: Django-Vue搭建个人博客(2):DRF的使用
原文链接: https://www.dusaiphoto.com/article/105/
原文作者: 杜赛
许可协议: 署名-非商业性使用 4.0 国际许可协议
本文对原始作品作了修改,转载请保留原文链接及作者