教程 1:序列化
# 介绍
在这个教程中将创建一个支持代码高亮展示的 API。我们会介绍组成 REST framework 的各个组件,并且让你明白这些组件是如何相互协作的。
这篇教程会比较深入,所以开始之前你应该准备些啤酒和花生毛豆什么的。如果你仅仅想大概了解,请看快速入门。
提示
这篇教程的源码可以在encode/rest-framework-tutorial (opens new window)找到,同时完整的实现版本也提供了在线版供大家测试,请点击这里 (opens new window)。
# 设置一个新虚拟环境
首先我们使用venv (opens new window)创建一个虚拟环境,这样可以很好的和其他项目隔离所需的依赖库:
python3 -m venv env
source env/bin/activate
2
然后在虚拟环境中安装需要的依赖:
pip install django
pip install djangorestframework
pip install pygments # 这个用于语法高亮
2
3
4
提示
使用deactive
来退出虚拟环境,更多信息请看venv 文档 (opens new window)。
# 开始
首先我们来创建一个新项目:
cd ~
django-admin startproject tutorial
cd tutorial
2
3
接下来创建一个 APP:
python manage.py startapp snippets
然后编辑tutorial/settings.py
,把我们的snippets
和rest_framework
添加到INSTALLED_APPS
:
INSTALLED_APPS = (
...
'rest_framework',
'snippets.apps.SnippetsConfig',
)
2
3
4
5
好了,我们的准备工作做完了。
# 创建 Model
首先我们创建一个Snippet
模型来存储代码片段。注意:写注释是一个好编程习惯,不过为了专注于代码本身,我们在下面省略了注释。编辑snippets/models.py
:
from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles
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']
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
接下来在数据库中建表:
python manage.py makemigrations snippets
python manage.py migrate
2
# 创建 Serializer 类
第一步我们需要为 API 提供一个序列化以及反序列化的方法,用来把 snippet 对象转化成 json
数据格式,创建 serializers 和创建 Django 表单类似。我们在snippets
目录创建一个serializers.py
:
from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
class SnippetSerializer(serializers.Serializer):
pk = 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')
def create(self, validated_data):
"""
如果数据合法就创建并返回一个snippet实例
"""
return Snippet.objects.create(**validated_data)
def update(self, instance, validated_data):
"""
如果数据合法就更新并返回一个存在的snippet实例
"""
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
在第一部分我们定义了序列化/反序列化的字段,creat()
和update()
方法则定义了当我们调用serializer.save()
时如何来创建或修改一个实例。
serializer 类和 Django 的 Form 类很像,并且包含了相似的属性标识,比如required
、 max_length
和default
。
这些标识也能控制序列化后的字段如何展示,比如渲染成 HTML。上面的{'base_template': 'textarea.html'}
和在 Django 的 Form 中设定widget=widgets.Textarea
是等效的。 这一点在控制 API 在浏览器中如何展示是非常有用的,我们后面的教程中会看到。
我们也可以使用ModelSerializer
类来节省时间,后续也会介绍,这里我们先显式的定义 serializer。
# 使用 serializer
继续之前我们先来熟悉一下我们的 serializer 类,打开 Django shell:
python manage.py shell
引入相关的代码后创建 2 个 snippet 实例:
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
3
4
5
6
7
8
9
10
现在我们有了可以操作的实例了,让我们来序列化一个实例:
serializer = SnippetSerializer(snippet)
serializer.data
# {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}
2
3
这里我们把 snippet 转换成了 Python 基本数据类型,接下来我们把其转换成 json 数据:
content = JSONRenderer().render(serializer.data)
content
# b'{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}'
2
3
反序列化也类似,我们先把一个 stream 转换成 python 基本数据类型:
import io
stream = io.BytesIO(content)
data = JSONParser().parse(stream)
2
3
4
然后将其转换为实例:
serializer = SnippetSerializer(data=data)
serializer.is_valid()
# True
serializer.validated_data
# OrderedDict([('title', ''), ('code', 'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
serializer.save()
# <Snippet: Snippet object>
2
3
4
5
6
7
可以看出这和 Django 的 Form 多么相似,当我们使用 serializer 编写 view 时这一特效会更明显。
我们也可以序列化实例的集合,仅需要设置 serializer 的参数many=True
即可:
serializer = SnippetSerializer(Snippet.objects.all(), many=True)
serializer.data
# [OrderedDict([('pk', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('pk', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('pk', 3), ('title', u''), ('code', u'print "hello, world"'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])]
2
3
# 使用 ModelSerializers
我们的SnippetSerializer
类和Snippet
模型有太多重复的代码,如果让代码更简洁就更好了。
Django 提供了Form
类和ModelForm
类,同样的,REST framework 提供了Serializer
类和ModelSerializer
。
让我们使用ModelSerializer
来重构代码,编辑snippets/serializers.py
:
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
2
3
4
我们可以通过打印输出来检查 serializer 包含哪些字段,打开 Django shell 并输入一下代码:
from snippets.serializers import SnippetSerializer
serializer = SnippetSerializer()
print(repr(serializer))
# SnippetSerializer():
# id = IntegerField(label='ID', 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=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')...
# style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')...
2
3
4
5
6
7
8
9
10
11
请记住ModelSerializer
并没有使用任何黑科技,它仅仅是一个创建 serializer 类的简单方法:
- 自动检测字段
- 简单的定义了
create()
和update()
方法。
# 使用 Serializer 编写常规的 Django 视图
现在我们来使用 Serializer 类来编写 API 视图,这里我们不使用任何 REST framewrok 的其他特性,仅使用 Django 的常规方法编写视图。
首先我们创建一个HttpResponse
的子类来返回 json 类型数据。
编辑snippets/views.py
:
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
2
3
4
5
我们 API 的根目录是一个 list view, 用于展示所有存在的 snippet, 或建立新的 snippet:
@csrf_exempt
def snippet_list(request):
"""
展示所以snippets,或创建新的snippet.
"""
if request.method == 'GET':
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return JSONResponse(serializer.data)
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)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意,这里我们为了能简单的在客户端进行 POST 操作而使用了csrf_exempt
,正常情况下你不应该这么做,REST framework 提供了更安全的做法。
我们也需要一个页面用来展示、修改或删除某个具体的 snippet:
@csrf_exempt
def snippet_detail(request, pk):
"""
修改或删除一个snippet.
"""
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)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
接下来修改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),
]
2
3
4
5
6
7
我们还需要在tutorial/urls.py
文件中连接根 urlconf,以包含我们的代码段应用程序的 URL。
from django.urls import path, include
urlpatterns = [
path('', include('snippets.urls')),
]
2
3
4
5
这里我们有许多细节没有处理,比如畸形的 JSON 数据、不支持的 HTTP 请求方法,这里我们都暂时返回 500 错误。
# 测试 API
现在我们可以启动我们的服务器了。
首先使用quit()
退出 shell,然后启动服务:
python manage.py runserver
Validating models...
0 errors found
Django version 1.8.3, using settings 'tutorial.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
2
3
4
5
6
7
8
提示
如果需要内网其他主机可以访问本机信息,我们则需要python manager.py runserver 0.0.0.0:8000
指定监听 host 为0.0.0.0
,同时在防火墙开放相应端口。注意,这个时候我们要访问的 ip 地址应该改为本机(服务所在机器)的 ip 地址而不是127.0.0.1
的地址。
打开另一个终端,我们可以使用 curl 或 httpie 来进行测试。httpie 是一个用 python 编写的易用的 http 客户端,首先来安装 httpie:
pip install httpie
最终,我们得到了一个 snippets 列表:
http http://127.0.0.1:8000/snippets/
HTTP/1.1 200 OK
...
[
{
"id": 1,
"title": "",
"code": "foo = \"bar\"\n",
"linenos": false,
"language": "python",
"style": "friendly"
},
{
"id": 2,
"title": "",
"code": "print \"hello, world\"\n",
"linenos": false,
"language": "python",
"style": "friendly"
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
或者通过 ID 来获取某个特定的 snippets:
http http://127.0.0.1:8000/snippets/2/
HTTP/1.1 200 OK
...
{
"id": 2,
"title": "",
"code": "print \"hello, world\"\n",
"linenos": false,
"language": "python",
"style": "friendly"
}
2
3
4
5
6
7
8
9
10
11
12
同样的,你可以使用浏览器访问那些 URL。
# 我们现在到哪了
到目前为止,我们做的还不错,我们有一个与 Django 的 Forms API 非常相似的序列化 API,还有一些常规的 Django 视图。
我们的 API 视图目前没有做任何特别的事情,除了提供 json 响应,还有一些错误处理的边缘情况我们仍然想要清理,但它是一个功能良好的 Web API。
在本教程的第 2 部分中,我们将看到如何开始改进。