参考文章:
阮一峰《RESTful API 设计指南》,原文链接:http://www.ruanyifeng.com/blog/2014/05/restful_api.html。
刘清政《RESTful规范》,原文链接:https://www.cnblogs.com/liuqingzheng/articles/9762022.html。
一、RESTful API
1.1、前言
据闻REST,最初只是英文单词rest即休憩的意思,后作者翻字典根据四个字母拼凑出所谓的Representational State Transfer,表征性状态转移,中文翻译我都不知道什么意思。。。Representational作者自己在查字典之前根本不认识这个词,ful也可能只是形容词后缀而已。
让我想起了在网上看到的关于交通大学校名的一则趣谈:有人将交通大学校名牵强附会称其取决于《易经》“天地交而万物通”,分明是国家交通部出钱办的。。。好像周星驰的大话西游系列电影,当年被骂成垃圾电影;多年后爆火,世人为其套上光环:后现代主义,怕不是连周星驰本人都懵圈了!
以上仅为笑谈,真假与否并不重要,重要的是这篇论文的重要性是实实在在影响到了全世界的Web开发领域!
1.2、REST简述
REST奉行资源至上原则,将一切数据视之为资源,资源即实体!它规定了一套web开发规范,才有了那句“如果一个架构符合REST的约束条件和原则,我们就称它为RESTful架构!”
1.3、API简述
此前我的Django学习过程一直是前后端不分离,比如模仿博客园写的网站,后端使用Django开发,前端使用Django模板语法DTL(Django Template Language)将后端与HTML、CSS、JavaScript等串联起来。
但据我了解与此前工作经历,前后端不分离的项目比较少它可称之为全栈开发,大多数应用于企业内部对前端页面要求不高的情况;而大多数项目都是前后端分离的,即后端专注于写后端接口,通过接口给前端返回JSON或XML数据,而前端拿到数据后尽情渲染丰富多彩的页面展示给用户!
因此API可以说是前后端数据交互的接口!API有一个特点就是返回的数据是跨语言的、通用的格式,比如:JSON、XML。
1.4、动静态简述
时常会听到动态页面与静态页面的说法,这里简单阐述一下!
1.4.1、动静态区别
简单来说,动态页面即数据经过后端渲染后再传给前端渲染并展示,根据输入的不同展示给用户的内容可能是不同的!比如Django开发中用render函数将数据处理好后通过locals传递给前端,在前端HTML中使用模板语法将其渲染展示!
静态就更简单了,就是直接写死了的东西,比如一个HTML页面里面就一行哈哈哈哈,这就是一个静态页面,因为它里面的内容是固定不变的,任凭雨打风吹,它始终是它!
1.4.2、高并发
很容易看出来,动态页面因为需要通过后端处理完数据后再传递给前端渲染数据,速度远比直接返回一个静态页面的固定内容要慢得多。
在访问量庞大、频率高的场景下,实时处理动态页面是制约网站承受力的主要因素之一!针对这种情况可以将动态页面静态化,什么意思呢?就是提前将页面渲染好,缓存下来供用户访问,比如电商平台首页等等!
1.5、RESTful规范
因为RESTful是一种Web API接口开发规范,因此本文通篇都是应该云云!
1.5.1、简述
RESTful规范了Web API接口的设计风格,尤其适合前后端分离的项目应用中!该理念人为后端就是提供数据的,对外则是暴露数据资源的访问接口,那么用户在访问时的键入的URL路径即要操作的数据资源!
它是一种规范,而不是某种编程语言的独有特性,因此我们可以使用任意一门语言任意一种Web框架来开发出符合该规范的API接口!
1.5.2、规范
1.协议规范
API接口与用户之间的通信协议,应该使用HTTPS协议来增强数据的安全性。
2.路径规范
应该将API接口部署在子域名之下,比如GitHub,这样可以见名知意!
https://api.domain.com
如API较为简单,且基本不会有扩展,可以考虑将其放在域名后面作为路径。
https://domain.com/api/
3.版本规范
应该保证数据的多版本共存,并将API版本号放入URL以作区分;比如:
https://api.domain.com/v3/
4.路径规范
RESTful中一切皆资源,因此URL中不应该存在动词,而是应该用名词,且该名词是同种数据的集合体,所以它们应该使用复数的名词!
比如此前写的博客网站有文章、文章标签、文章分类等,我们应该使用如articles这种复数来代表所有文章,如下所示:
https://api.domain.com/v3/articles
https://api.domain.com/v3/tags
https://api.domain.com/v3/classes
5.method规范
客户端对于资源的请求的操作类型应该是以下几种:
常用方法:
- GET:从服务器获取一个或多项资源。
- POST:在服务器新建一项资源。
- PUT:在服务器更新资源,用户需提供完整的改变后的数据资源。
- 比如文章名是docker基础,分类docker;我想改文章名为docker入门,分类不动,提交时改后的文章名和分类全部要提交。
- PATCH:同样是在服务器更新资源,但用户仅需提供需要改变的数据属性即可。
- 还是上面的例子,我只改文章名,那么我只需要提交新文章名即可,分类等其它的没改就不传。
- DELETE:从服务器删除数据资源。
不常用方法:
- HEAD:获取资源的元数据,即meta。
- OPTIONS:获取资源的信息,即资源的哪些属性是可修改的。
6.过滤信息规范
一般服务器数据都非常多,服务器不会都将它们返回给用户,而是应该提供参数,用参数作为条件将过滤后的结果返回。比如:
- ?limit=6,指定返回的资源数量。
- ?offset=10,指定返回的资源的开始位置。
- ?page=2&per_page=100,指定第几页,及每页显示的数量。
- ?sortby=name&order=asc,指定返回结果按照某种属性排序及其排序的顺序是正序还是反序。
- ?animal_type_id=1,指定筛选条件。
7.服务器响应
比如以前我们是用get_book这种动词,现在用book,有多个就是books
5、资源操作由请求方式决定,重点
6、过滤,通过在url上传参的形式传递搜索条件,就是筛选条件
8.状态码规范
服务器在向用户返回结果时,应该带有提示信息与相应状态码,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。
200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE]:用户删除数据成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
9.异常处理
如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。
{
"error": "Invalid API key"
}
10.Hypermedia
RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。
比如键入https://api.github.com,会返回所有可用API的目录及其对应链接,我们按照它的链接去访问对应资源即可。
二、drf实践
drf,即Django REST Framework,是Django对RESTful规范的一种具体实现,下面来开始学习它的简单使用!
2.1、drf的安装
pip3 install djangorestframework
2.2、序列化器
我们说过RESTful规范我们应该向前端返回JSON或XML数据,以往我们需要自己写templates和序列化数据,而现在只需要使用rest_framework提供的序列化器serializers即可!
我这次使用的是django 2.2.16,不再是之前的1.11.29了!且经过rest_framework后,原生django中的request等已经全部被rest_framework处理过。
2.2.1、查询单条数据
models定义
from django.db import models
# Create your models here.
class Article(models.Model):
title = models.CharField(max_length=16, null=True)
create_time = models.DateTimeField(auto_now_add=True)
site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True) # django 2.X版本要手动指定级联删除
def __str__(self):
return self.title
class Site(models.Model):
title = models.CharField(max_length=16, null=True)
def __str__(self):
return self.title
创建序列化器
在APP下创建任意名称的py文件,书写以下代码;想序列化models哪个字段就往这序列化器里一一对应即可,序列化器必须要继承Serializer!
注意:视图类必须继承View,而在rest_framework中APIView继承了View,因此我们不需要手动继承!
from rest_framework import serializers
class ArticleSeriallizer(serializers.Serializer):
title = serializers.CharField() # 和models字段对应
create_time = serializers.DateTimeField()
site = serializers.CharField()
class SiteSerializer(serializers.Serializer):
title = serializers.CharField()
注册序列化器
在settings的APP中注册rest_framework
INSTALLED_APPS = [
'rest_framework'
]
urls配置
urlpatterns = [
path('admin/', admin.site.urls), # 1.X用url,2.X用path,path不支持正则
re_path('article/(?P<pk>\d+)', views.ArticleView.as_view()) # re_path支持正则
]
views自定义类
序列化的数据全部存放在.data字典中。
from django.shortcuts import render
from study.serializer import ArticleSeriallizer
from rest_framework.views import APIView
from rest_framework.response import Response
from study import models
# Create your views here.
class ArticleView(APIView): # 必须继承APIView
def get(self, request, pk):
article = models.Article.objects.filter(pk=pk).first() # 先获取数据
article_obj = ArticleSeriallizer(instance=article) # 将数据序列化得到一个Seriallizer对象
return Response(article_obj.data) # 返回对象中序列化好的数据
注意事项
本人用django 2.2.16实验时,在迁移数据库时报错如下:
packages/django/db/backends/mysql/operations.py", line 146, in last_executed_query
query = query.decode(errors='replace')
AttributeError: 'str' object has no attribute 'decode'
需要去报错里显示的operations.py里面,将query = query.decode(errors='replace')里面的decode改为encode
query = query.encode(errors='replace')
测试结果
浏览器输入:http://127.0.0.1:8000/article/1/,得到结果如下所示:
2.2.2、查询所有数据
序列化多条数据时,需要在视图类里指定many参数为True
urls代码
urlpatterns = [
path('articles/', views.ArticlesView.as_view())
]
views代码
class ArticlesView(APIView):
def get(self, request):
article = models.Article.objects.all()
article_obj = ArticleSeriallizer(instance=article, many=True) # 序列化多条数据时必须指定many为True
return Response(article_obj.data)
当朝:127.0.0.1:8000/articles/ 发起GET请求后,得到结果:
[
{
"title": "测试文章",
"create_time": "2013-03-03T03:03:03",
"site": "小破站"
},
{
"title": "瞎写一通",
"create_time": "2014-04-04T00:00:00",
"site": "小破站"
}
]
2.2.3、通用参数
read_only
表明该字段仅用于序列化输出,true即get可以看到,修改时不需要也看不到
当发起PUT请求时,
{
"title": "测试文章",
"create_time": "2013-03-03 03:03:03",
"site": "1"
}
结果为
{
"status": 201,
"msg": "sucess",
"data": {
"title": null,
"create_time": "2013-03-03T03:03:03"
}
}
write_only
表明该字段仅用于序列化输入,GETq请求时是看不到的,PUT请求提交时必须带有该字段。
class ArticleSeriallizer(serializers.Serializer):
site = serializers.CharField(write_only=True)
当朝着:127.0.0.1:8000/article/1/发起GET请求时,得到以下结果:
{
"title": "喝茶",
"create_time": "2013-03-03T03:03:03" // 说明字段用了write_only后在GET请求时看不到该字段的
}
2.3、反序列化
2.3.1、数据修改与校验
rest_framework也支持forms组件功能,使用方法也基本一致。
序列化器接口定义子类的行为,没有这个行为就报错,所以要重写update方法
views代码
在上面的例子的基础上,添加一个put方法。
def put(self, request, pk):
callback_dict = {'status': 0, 'msg': 'sucess'}
article = models.Article.objects.filter(pk=pk).first() # 获取修改的那个对象
article_obj = ArticleSeriallizer(instance=article, data=request.data) # 把原来的数据改成请求里的数据
if article_obj.is_valid(): # 先判断请求修改的数据是否合法
article_obj.save() # 合法则保存数据,直接这样修改会报错,必须要在序列化器里重写update方法!
callback_dict['data'] = article_obj.data # 返回数据
else:
callback_dict['status'] = 1
callback_dict['msg'] = 'invaild data'
callback_dict['data'] = article_obj.errors
return Response(callback_dict)
序列化器代码
from study.models import Site
from rest_framework import serializers
class ArticleSeriallizer(serializers.Serializer):
title = serializers.CharField(label='文章标题', min_length=2, max_length=16) # 跟forms类似的校验规则
create_time = serializers.DateTimeField()
site = serializers.CharField()
def update(self, instance, validated_data): # 必须在序列化器中重写update方法前端才能修改数据
instance.title = validated_data.get('title') # 一一获取对应的数据
instance.create_time = validated_data.get('create_time')
instance.site = Site.objects.filter(pk=validated_data.get('site')).first() # 外键字段必须传入对象
instance.save() # 写入数据
return instance # 返回数据
class SiteSerializer(serializers.Serializer):
title = serializers.CharField()
2.3.2、钩子函数
rest_framework提供的校验规则较少,只有以下几种:
max_length | 最大长度 |
---|---|
min_lenght | 最小长度 |
allow_blank | 是否允许为空 |
trim_whitespace | 是否截断空白字符 |
max_value | 最小值 |
min_value | 最大值 |
如果字段的校验规则不够用,可以自定义钩子函数!
局部钩子
def validate_title(self, attrs):
taboo_list = ['开车', '修车', '喝茶']
for key in taboo_list:
if not attrs.find(key):
return attrs
else:
raise ValidationError('查水表警告!')
我在postman发起PUT请求
{
"title": "喝茶",
"create_time": "2013-03-03 03:03:03",
"site": "1"
}
返回结果
{
"status": 1,
"msg": "invaild data",
"data": {
"title": [
"查水表警告!"
]
}
}
全局钩子
在序列化器中需要同时对多个字段进行比较验证时,可以定义validate方法来验证;比较少用,容易乱套。
def validate(self, attrs):
article = attrs.get('title')
tag = attrs.get('tag')
if article == tag:
raise ValidationError('文章标题与标签不能一致')
else:
return attrs
第三种校验方式
可以使用字段的validators=[],列表里面写函数名,函数里配置校验。
def title_form(field):
if not field.find('python'):
raise serializers.ValidationError('不是Python差评!')
class ArticleSeriallizer(serializers.Serializer):
title = serializers.CharField(label='文章标题', min_length=2, max_length=16, validators=[title_form,])
create_time = serializers.DateTimeField()
site = serializers.CharField()
2.3.3、新增数据
新增跟修改一样,必须要重写方法,新增是重写create保存数据,要return
views代码
def post(self, request):
callback_dict = {'status': 201, 'msg': 'sucess'}
article = ArticleSeriallizer(data=request.data)
if article.is_valid():
article.save()
callback_dict['data'] = article.data
else:
callback_dict['status'] = 400
callback_dict['msg'] = 'invaild data'
callback_dict['data'] = article.errors
return Response(callback_dict)
Seriallizer代码
def create(self, validated_data):
instance = Article.objects.create(**validated_data)
return instance
postman测试POST请求
127.0.0.1:8000/article/
{
"status": 201,
"msg": "sucess",
"data": {
"title": "docker",
"create_time": "2021-03-25T13:47:34.553944",
"author": "三溪"
}
}
2.2.4删除
直接删除即可
def delete(self, request, pk):
delete_obj = models.Article.objects.filter(pk=pk).delete()
return Response({'status': 201, 'msg': 'deleted sucess'})
2.4、自定义Response对象
在前面我们自定义字典,每个函数每次判断都要手写代码,造成代码冗余,因此可以考虑将其封装成对象来调用。
按照之前所说,此文件应该放在utils里面,然后views导入该文件直接return即可
from rest_framework import status
from rest_framework.response import Response
class CallBackDict(Response):
def __init__(self, status_code=200, msg='sucess', data=None, status=None, headers=None, **kwargs):
dict = {'status_code': status_code, 'msg': msg}
dict.update(kwargs)
if data:
dict = {'status_code': status_code, 'msg': msg, 'data': data}
super().__init__(data=dict, status=status, headers=headers)
2.5、模型序列化器
上面的演示中,我们需要手写序列化器代码,其实rest_framework给我们提供了模型序列化器可以自动序列化表,且无需再重写update和create方法。
2.X版本后write_only_fields不能用了,得使用extra_kwargs!
class ArticleModelSeriallizer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__'
# fields = ('title', 'tag', 'create_time', 'author') # 仅序列化指定字段
# exclude = ('name') # 与上相反,写谁就排除谁,且不能与fields同时存在。
# read_only_fields = ('title')
# extra_kwargs = {
# 'price': {'write_onlu': True}
# }
2.6、serializer参数补充
source
source有点像起别名,它可以是属性也可以是方法;如果是字段则显示字段;如果是方法则执行且不用加括号;它还可以用点实现跨表,一起来看看。
models代码
class Article(models.Model):
title = models.CharField(max_length=16, null=True)
tag = models.CharField(max_length=8, null=True)
create_time = models.DateTimeField(auto_now_add=True)
site = models.ForeignKey(to='Site', on_delete=models.CASCADE())
def __str__(self):
return self.title
class Site(models.Model):
title = models.CharField(max_length=16)
def __str__(self):
return self.title
序列化器代码
class ArticleSeriallizer(serializers.Serializer):
title = serializers.CharField(max_length=16)
create_time = serializers.DateTimeField()
site = serializers.CharField(source='Site.title', default='三溪')
postman测试GET请求:127.0.0.1:8000/article/1/,结果如下:
{
"title": "测试文章",
"create_time": "2013-03-03T03:03:03",
"site": "三溪"
}
SerializerMethodField
因为不想让显示出来的字段名跟数据库的一样,隐藏数据库真实字段信息;我们可以使用SerializerMethodField字段,它有个配套方法,get_字段名,return什么就显示什么。