1. 初识Django REST framework (DRF) 1.1 DRF开发预备 首先在命令行创建博客文章的App:
(venv) > python manage.py startapp article
创建一个简单的博客文章模型:
from django.db import modelsfrom django.utils import timezoneclass 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注册到列表:
INSTALLED_APPS = [ ... 'rest_framework' , 'article' , ]
接着添加 DRF 的登录视图,以便 DRF 自动为你的可视化接口页面生成一个用户登录的入口:
... from django.urls import includeurlpatterns = [ ... 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 ) >>> json.dumps(person) '{"name": "Trump", "age": 82}' >>> json_str = '{"name": "Trump", "age": 82}' >>> json.loads(json_str) {'name' : 'Trump' , 'age' : 82 }
总之,把变量从内存中变为可存储或传输的过程称之为序列化 ,反过来把变量内容从序列化对象重新读到内存中称之为反序列化 。
回顾 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 编写文章列表接口 按照该思路,写一个文章列表的接口:
from django.http import JsonResponsefrom article.models import Articlefrom article.serializers import 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
的代码:
from rest_framework import serializersclass 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
配置好。
... urlpatterns = [ ... path('api/article/' , include('article.urls' , namespace='article' )), ]
from django.urls import pathfrom article import viewsapp_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
数据表注册到后台中
from django.contrib import adminfrom article.models import Articleadmin.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 from rest_framework import serializersclass 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()
from django.db import modelsfrom django.utils import timezoneclass 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
用于简化序列化器。
from rest_framework import serializersfrom article.models import Articleclass 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 还提供了对视图的扩展,以便视图更好的为接口服务,将文章的视图修改如下:
from rest_framework.decorators import api_viewfrom rest_framework.response import Responsefrom rest_framework import statusfrom article.models import Articlefrom 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
装饰器允许视图接收 GET
、POST
请求,以及提供如 405 Method Not Allowed
等默认实现,以便在不同的请求下进行正确的响应。返回了 Response
,该对象由 Django 原生响应体扩展而来,它可以根据内容协商 来确定返回给客户端的正确内容类型。如果数据验证有误,还可以返回适当的状态码以表示当前的情况。 刷新文章列表接口,出现了可视化的接口界面:
这是因为视图中 Response
提供的内容协商 能力,Django 后端根据客户端请求响应的内容类型不同,自动选择适合的表现形式;浏览器请求资源时,就返回可视化的 HTML 资源表示,其他形式请求时,又可以返回 Json 纯数据的形式,给开发人员带来极大的方便。
2.3 测试接口 验证它是不是真的会返回 JSON 数据,可以从命令行使用诸如 curl
或httpie
访问,如:
(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 中也有基于类的视图 的存在,可用于实现功能的模块化继承、封装,减少重复代码。
from rest_framework.decorators import api_viewfrom rest_framework.response import Responsefrom rest_framework import statusfrom rest_framework.views import APIViewfrom django.http import Http404from article.models import Articlefrom article.serializers import ArticleListSerializer, ArticleDetailSerializerclass 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 是两种语言传输信息的桥梁,一但信息到达,对方都需要将其还原为自身的数据结构。
由于详情接口需要返回完整的数据,所以新增一个序列化器:
from rest_framework import serializersfrom article.models import Articleclass ArticleListSerializer (serializers.ModelSerializer ): class Meta : model = Article fields = ['id' , 'title' , 'created' ] class ArticleDetailSerializer (serializers.ModelSerializer ): class Meta : model = Article fields = '__all__'
配置urls.py
:
from django.urls import pathfrom article import viewsapp_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 类直接集成对应的功能。
from rest_framework.decorators import api_viewfrom rest_framework.response import Responsefrom rest_framework import status, mixins, genericsfrom article.models import Articlefrom article.serializers import ArticleListSerializer, ArticleDetailSerializerclass 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)
还可以进一步简化
from rest_framework import genericsfrom article.models import Articlefrom article.serializers import ArticleListSerializer, ArticleDetailSerializerclass 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
的视图是完全相同的。
除了上述介绍的以外,框架还提供 ListModelMixin
、 CreateModelMixin
等混入类或通用视图,覆盖了基础的增删改查需求。
4. 用户权限 权限 是Web应用的重要组成部分,在 DRF 中可以进行权限管理。
4.1 文章与用户 依靠用户身份来限制权限,作者与文章是一对多的关系,需要给文章模型添加用户外键,确定每篇文章的作者。保险起见,首先删除现有的所有文章数据。
修改文章的 model ,让每篇文章都对应一个作者:
from django.db import modelsfrom django.utils import timezonefrom django.contrib.auth.models import Userclass 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 内置了IsAuthenticated
、IsAdminUser
、AllowAny
等权限控制类,个人博客只允许管理员发布文章。修改文章列表视图如下:
from rest_framework import genericsfrom rest_framework.permissions import IsAdminUserfrom article.models import Articlefrom article.serializers import ArticleListSerializer, ArticleDetailSerializerclass 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
文件,写入:
from rest_framework import permissionsclass 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
。此方法在每次请求到来时被唤醒执行,里面简单判断了请求的种类是否安全(即不更改数据的请求),如果安全则直接通过,不安全则只允许管理员用户通过。
再次修改视图:
from rest_framework import genericsfrom article.permissions import IsAdminUserOrReadOnlyfrom article.models import Articlefrom article.serializers import ArticleListSerializer, ArticleDetailSerializerclass 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 ,用普通用户身份进行请求:
(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 测试:
(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
提取用户信息,把额外的用户信息注入到已有的数据中。
修改视图:
from rest_framework import genericsfrom article.permissions import IsAdminUserOrReadOnlyfrom article.models import Articlefrom article.serializers import ArticleListSerializer, ArticleDetailSerializerclass 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
,序列化器允许你指定只读字段。
from rest_framework import serializersfrom article.models import Articleclass 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
文件,写入:
from django.contrib.auth.models import Userfrom rest_framework import serializersclass UserDescSerializer (serializers.ModelSerializer ): """ 文章列表中引用的嵌套用户信息 """ class Meta : model = User fields = [ 'id' , 'username' , 'last_login' , 'date_joined' ]
这个序列化器专门用在文章列表中,展示用户的基本信息,最后修改文章列表的序列化器,把它们嵌套到一起:
from rest_framework import serializersfrom article.models import Articlefrom user_info.serializers import UserDescSerializerclass ArticleListSerializer (serializers.ModelSerializer ): author = UserDescSerializer(read_only=True ) class Meta : model = Article fields = ['id' , 'title' , 'created' ] 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
:
from rest_framework import serializersfrom article.models import Articlefrom user_info.serializers import UserDescSerializerclass 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
里配置一下就行了:
... 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 框架提供了视图集 作为更高层的抽象,可以让代码量进一步的减少。
因为大部分对接口的操作,都是在增删改查的基础上衍生出来的。既然这样,视图集 就将这些通用操作集成在一起了。
将之前写的与文章有关的序列化器 都注释掉,新增一个提供给视图集的新序列化器:
from rest_framework import serializersfrom article.models import Articlefrom user_info.serializers import UserDescSerializerclass ArticleSerializer (serializers.HyperlinkedModelSerializer ): author = UserDescSerializer(read_only=True ) class Meta : model = Article fields = '__all__'
序列化器继承的 HyperlinkedModelSerializer
基本上与之前用的 ModelSerializer
差不多,区别是它自动提供了外键字段的超链接,并且默认不包含模型对象的 id 字段。
把之前写的文章视图 也全注释掉,并新增代码:
from article.permissions import IsAdminUserOrReadOnlyfrom article.models import Article from rest_framework import viewsetsfrom article.serializers import ArticleSerializerclass 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 的连接。
修改项目根路由 :
from django.contrib import adminfrom django.urls import path, includefrom rest_framework.routers import DefaultRouterfrom article import viewsrouter = DefaultRouter() router.register(r'article' , views.ArticleViewSet) urlpatterns = [ path('api/' , include(router.urls)), ]
最后为了让分页更准确,给模型类规定好查询排序:
from django.db import modelsfrom django.utils import timezonefrom django.contrib.auth.models import Userclass 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 国际许可协议 本文对原始作品作了修改,转载请保留原文链接及作者