概述

本教程将介绍如何创建一个简单的对代码片段进行高亮展示的Web API。这个过程中, 将会介绍组成DRF框架的各个组件,并让你大概了解各个组件是如何一起工作的。

这个教程是相当深入的,可能需要结合后面的API,反复揣摩。

创建虚拟环境

我们先用virtualenv创建一个新的虚拟环境。这样就能确保与我们正在开展的任何其他项目保持 良好的隔离。

以下为linux环境。MacOS或Pycharm环境

virtualenv env
source env/bin/activate

进入virtualenv环境后,安装我们需要的包。

pip install django #当前为3.1.5版本
pip install djangorestframework #当然为3.9版本 
pip install pygments # 代码高亮插件

DRF在导入时的名字为 rest_framework ,不要搞错了。

注意: 要随时退出virtualenv环境,只需输入 deactivate 。

创建项目

好了,我们现在要开始写代码了。 首先,创建一个新的项目。

cd ~
django-admin.py startproject tutorial
cd drftest

再创建一个app,名字叫做snippets,这个单词的意思是‘片段’,理解为代码片段。

python manage.py startapp snippets

然后将我们新建的app注册到INSTALLED_APPS

INSTALLED_APPS = (
    ...
    'rest_framework',
    'snippets.apps.SnippetsConfig',
)

好了,我们的准备工作做完了。

编写model模型

为了实现本教程的目的,我们将开始创建一个用于存储代码片段的简单的 Snippet model模 型。打开 snippets/models.py 文件,并写入下面的代码:

from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles

# Create your models here.


# 下面的几行代码是处理代码高亮的,不好理解,但没关系,它不重要。
LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted((item, item) for item in get_all_styles())


class Snippet(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True, default='')
    code = models.TextField()
    linenos = models.BooleanField(default=False)
    language = models.CharField(choices=LANGUAGE_CHOICES, default='python',
                                max_length=100)
    style = models.CharField(choices=STYLE_CHOICES, default='friendly',
                             max_length=100)

    class Meta:
        ordering = ('created',)

然后,使用下面的命令创建数据表:

python manage.py makemigrations snippets
python manage.py migrate

创建序列化类

开发Web API的第一件事是为我们的代码片段对象创建一种序列化和反序列方法,将其与诸如 json 格式进行互相转换。具体方法是声明与Django forms非常相似序列化器(serializers)来实现。 在 snippets 的目录下创建一个名为 serializers.py 文件,并添加以下内容。

为每一个你需要序列化的model创建一个对应的序列化类。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@author: 多点笔记
@contact: wouldmissyou@163.com
@time: 2021/1/19 下午3:06
@file: serializers.py
@desc: 
"""
from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES


class SnippetSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)  # 序列化时使用,反序列化时不用
    title = serializers.CharField(required=False, allow_blank=True, max_length=100)
    code = serializers.CharField(style={'base_template': 'textarea.html'})
    linenos = serializers.BooleanField(required=False)
    language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python')
    style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly')

    # 注意,没有为model的created创建对应的序列化字段
    def create(self, validated_data):
        """
        使用验证后的数据,创建一个代码片段对象。使用的是Django的ORM的语法。
        """
        return Snippet.objects.create(**validated_data)

    def update(self, instance, validated_data):
        """
        使用验证过的数据,更新并返回一个已经存在的‘代码片段’对象。依然使用的是Django的 ORM的语法
        """
        instance.title = validated_data.get('title', instance.title)
        instance.code = validated_data.get('code', instance.code)
        instance.linenos = validated_data.get('linenos', instance.linenos)
        instance.language = validated_data.get('language', instance.language)
        instance.style = validated_data.get('style', instance.style)
        instance.save()
        return instance

Serializer是DRF提供的序列化基本类,供我们继承使用,它位于DRF的serializers包中。使用这个 类,你需要自己编写所有的字段以及createupdate方法,比较底层,抽象度较低,接近Django 的form表单类的层次。

让我们看一下上面的代码。SnippetSerializer类的第一部分定义了序列化/反序列化过程中需要的 字段。 create()update() 方法定义了在调用 serializer.save() 时如何创建和修改实例。

序列化类与Django 的 类非常相似,并在各种字段中包含类似的验证标志,例如 requiredmax_lengthdefault 。这里暂时不讲解各种字段的含义,以及它们包含的参数的用法,详见API。

字段参数可以控制serializer在某些情况下如何显示,比如渲染HTML的时候。上面的style={'base_template':'textarea.html'}等同于在Django 的 Form 类中使用widget=widgets.Textarea,也就是使用文本输入框标签。这对于控制如何显示可浏览的API特别有用。

实际上也可以通过使用ModelSerializer类来节省一些编写代码的时间,就像教程后面会用到的那样,但是现在还是继续使用我们刚才定义的serializer。

序列化器的基本使用

我们先来熟悉一下Serializer类的基本使用方法。输入下面的命令进入Django shell。

python manage.py shell

在命令行中,像下面一样导入几个模块,然后创建一些代码片段:

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
snippet = Snippet(code='foo = "bar"\n')
snippet.save()
snippet = Snippet(code='print("hello, world")\n')
snippet.save()

我们现在已经有2个代码片段实例了,让我们将第二个实例序列化:

serializer = SnippetSerializer(snippet)
serializer.data
{'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}

此时,我们将模型实例对象转换为了Python的原生数据类型。

type(serializer)
<class 'snippets.serializers.SnippetSerializer'>
type(serializer.data)
<class 'rest_framework.utils.serializer_helpers.ReturnDict'>
issubclass(type(serializer.data), dict)
True

但是,要完成最终的序列化过程,我们还需要将数据转换成 json 格式,这样的话,客户端才 可以理解。

content = JSONRenderer().render(serializer.data)
content
b'{"id":2,"title":"","code":"print(\\"hello, world\\")\\n","linenos":false,"language":"python","style":"friendly"}'
type(content)
<class 'bytes'>

以上是序列化过程,也就是使用ORM从数据库中读取对象,然后序列化为DRF的某种格式,再 转换为json格式(为什么上面是bytes类型,这是和Python语言相关的),最后将json数据通过 HTTP发送给客户端。

反序列化则是上面过程的逆向。首先我们使用Python内置的io模块将我们前面生成的content转 换为一个流(stream)对象,模拟从前端发送过来的json格式的请求数据,然后将数据解析为 Python原生数据类型:

import io
stream = io.BytesIO(content)
data = JSONParser().parse(stream)
type(data)
<class 'dict'>

然后我们要将数据类型转换成模型对象实例并保存。

serializer = SnippetSerializer(data=data)
serializer.is_valid()
True
serializer.validated_data
OrderedDict([('title', ''), ('code', 'print("hello, world")'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
serializer.save()
<Snippet: Snippet object (3)>

上面的操作都是在shell中进行的,实际中我们不会这么麻烦

可以看到序列化器的API和Django的表单(forms)是多么相似。

也可以序列化查询结果集(querysets)而不是单个模型实例,也就是同时序列化多个对象。只 需要为serializer添加一个 many=True 标志。(这个功能是比较重要的)

serializer = SnippetSerializer(Snippet.objects.all(), many=True)
serializer.data
[OrderedDict([('id', 1), ('title', ''), ('code', 'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', ''), ('code', 'print("hello, world")'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])]

使用ModelSerializers类

除了前面的Serializer类,DRF还给我们提供了几种别的可以继承的序列化类,ModelSerializers就 是常用的一个。

前面我们写的的ModelSerializers类中重复了很多包含在Snippet模型类(model)中的 信息。如果能自动生成这些内容,像Django的 ModelForm 那样就更好了。事实上REST framework的 ModelSerializer 类就是这么一个类,它会根据指向的model,自动生成默认的 字段和简单的create及update方法。

下面让我们来实践一下,再次打开sinppets/serializers.py,并将SinppetSerializer类替换为一下内容:

class SnippetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Snippet
        fields = ('id', 'title', 'code', 'linenos', 'language', 'style')

额外的提示一下,DRF的序列化类有一个repr属性可以通过打印序列化器类实例的结构 (representation)查看它的所有字段。以下操作在命令行中进行:

from snippets.serializers import SnippetSerializer
serializer = SnippetSerializer()
print(repr(serializer))
SnippetSerializer():
    id = IntegerField(read_only=True)
    title = CharField(allow_blank=True, max_length=100, required=False)
    code = CharField(style={'base_template': 'textarea.html'})
    linenos = BooleanField(required=False)
    language = ChoiceField(choices=[('abap', 'ABAP'), ('abnf', 'ABNF'), ('ada', 'Ada'), ('adl', 'ADL'), ('agda', 'Agda'),
                                    ...
    style = ChoiceField(choices=[('abap', 'abap'), ('algol', 'algol'), ...], default='friendly')

注意: ModelSerializer 类并不会做任何特别神奇的事情,它们只是创建序列化器类的快捷 方式:

  • 一组自动确定的字段。
  • 默认简单实现的 create() update() 方法。

这个 ModelSerializer 类帮我们节省了很多代码,但同时,又降低了可定制性,如何取舍, 取决于你的业务逻辑。

没有谁规定必须用 ModelSerializer 类,不能用前面的更基础的Serializer类,实际上在复杂 的业务逻辑中,定制性更高的Serializer类,反而是更实用的。 ModelSerializer 类感觉比较 鸡肋。

编写常规的Django视图

让我们看看如何使用我们新的Serializer类编写一些API视图。目前我们不会使用任何REST框架的其他功能,我们只需将视图作为常规Django视图编写。

编辑 snippets/views.py 文件,并且添加以下内容:

from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer

我们API的根视图的功能是列出所有的snippets或创建一个新的snippet。

@csrf_exempt  # 防止403 
def snippet_list(request):
    """
    列出所有的代码片段或者创建新的。
    """
    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)  # 注意many参数 
        # 实用Django自带方法,响应json格式的数据
        return JsonResponse(serializer.data, safe=False)
    
    elif request.method == 'POST':
        data = JSONParser().parse(request)
        serializer = SnippetSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=201)
        return JsonResponse(serializer.errors, status=400)

请注意,因为我们后面会使用没有CSRF令牌的客户端对此视图进行POST测试,因此我们需要为 视图增加 csrf_exempt 装饰器,跳过csrf的检测,避免403。事实上DRF有专门应对的策略。

另外,我们还需要写一个与单个snippet对象相应的detail视图,用于获取,更新和删除这个 snippet。

@csrf_exempt
def snippet_detail(request, pk):
    """
    获取、更新和删除指定的某个代码片段。
    :param request:
    :param pk:
    :return:
    """
    try:
        snippet = Snippet.objects.get(pk=pk)
    except Snippet.DoesNotExist:
        return HttpResponse(status=404)
    if request.method == 'GET':
        serializer = SnippetSerializer(snippet)
        return JsonResponse(serializer.data)
    elif request.method == 'PUT':
        data = JSONParser().parse(request)
        serializer = SnippetSerializer(snippet, data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data)
        return JsonResponse(serializer.errors, status=400)
    elif request.method == 'DELETE':
        snippet.delete()
        return HttpResponse(status=204)

注意视图函数的名称!注意两个视图函数各自支持的HTTP操作!注意区分POST和PUT方法!注 意,同时只能创建或更新一个对象,暂时不支持批量更新或创建!但是读取可以批量!

视图有了,序列化器有了,模型有了,我们还差编写路由把请求和视图链接起来。创建一个 snippets/urls.py 文件:

from django.urls import path
from snippets import views
urlpatterns = [
    path('snippets/', views.snippet_list),
    path('snippets/<int:pk>/', views.snippet_detail),
]

上面是snippets这个app自己的二级路由文件,我们还需要在项目根URL配置,添加我们的snippet应用的include语句。

tutorial/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('snippets.urls')),
]

注:原来的admin路由,保留与否,随意。

这样, 127.0.0.1:8000/snippets/ 将访问 snippet_list 视图。

值得注意的是,目前我们还没有正确处理好几种特殊情况。比如假设我们发送格式错误的 json 数据,或者使用视图不处理的HTTP方法发出请求,那么我们最终会出现一个500“服务器错误”响应。不过,暂时没有关系。

测试前面的工作

现在退出所有的shell...,启动Django开发服务器:

Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
January 19, 2021 - 08:02:34
Django version 3.1.5, using settings 'tutorial.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

打开一个终端窗口,我们在命令行下测试服务器。

我们可以使用curl或httpie测试我们的服务器。Httpie是用Python编写的用户友好的http客户端, 我们安装它。

访问下面的url可以得到所有snippet的列表:

输入命令:http http://127.0.0.1:8000/snippets/
      
结果如下
HTTP/1.1 200 OK
Content-Length: 354
Content-Type: application/json
Date: Tue, 19 Jan 2021 08:04:17 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.6.7
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

[
    {
        "code": "foo = \"bar\"\n",
        "id": 1,
        "language": "python",
        "linenos": false,
        "style": "friendly",
        "title": ""
    },
    {
        "code": "print(\"hello, world\")\n",
        "id": 2,
        "language": "python",
        "linenos": false,
        "style": "friendly",
        "title": ""
    },
    {
        "code": "print(\"hello, world\")",
        "id": 3,
        "language": "python",
        "linenos": false,
        "style": "friendly",
        "title": ""
    }
]

或者我们可以指定id来获取特定snippet的detail信息:

http http://127.0.0.1:8000/snippets/2/
    
结果如下
HTTP/1.1 200 OK
Content-Length: 120
Content-Type: application/json
Date: Tue, 19 Jan 2021 08:05:11 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.6.7
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
    "code": "print(\"hello, world\")\n",
    "id": 2,
    "language": "python",
    "linenos": false,
    "style": "friendly",
    "title": ""
}

当然,也可以在浏览器中访问这些URL来显示相同的json。

image-20210119160618217

本文作者:博主:
文章标题:DRF-快速入门--序列化
本文地址:https://wouldmissyou.com/archives/40/     
版权说明:若无注明,本文皆为“多点部落”原创,转载请保留文章出处。
最后修改:2021 年 01 月 20 日 01 : 40 PM
如果觉得我的文章对你有用,请随意赞赏