Django RESTful系列教程
真正开始编写代码
https://www.jianshu.com/u/921b316bd515
在上一章我们了解了 REST 和 Mixin 以及 UI 状态的概念、API 设计相关的一些知识,现在我们将会使用这些概念来真正编写一个 REST 项目。在本章,我们将会涵盖以下知识点:
- Mixin 的编写,掌握 Mixin 的最基本编写原则
- Store 与 state 的编写。理解并能应用 UI 的状态概念。
- 了解 API 的基本编写规范和原则
本章的一些代码会涉及到元编程的一点点知识,还有装饰器的知识。本章的完整代码在这里
设计项目
在第一章,不管是在前端还是在后端开发,我们在写代码之前都有设计的过程,同样的,在这里我们也需要设计好我们的项目才可以开始写代码。
需求分析
后端开发的主职责是提供 API 服务,同时,我们不能再把 javascript 写在 html 里了,因为这次的 javascript 代码会有点多,所以我们要提供静态文件服务。一般来说,静态文件服务都是由专门的静态文件服务器来完成的,比如说 CDN ,也可以用 Nginx 。在这一章,我们的项目非常小,所以就使用 Django 来提供静态文件服务。我们计划自己编写一个简易的静态文件服务。
项目结构
我们的项目结构如下:
online_intepreter_project/
frontend/ # 前端目录
index.html
css/
...
js/
...
online_intepreter_project/ # 项目配置文件
settings.py
urls.py
...
online_intepreter_app/ # 我们真正的应用在这里
...
manage.py
大家可以看到,其实这一次,我们还是以后端为主,前端并没有独立出后端的项目结构,就像刚才所说,静态文件,或者说是前端文件,应该尽量由专门的服务器来提供服务,后端专门负责数据处理就可以了。我们将会在之后的章节中使用这种模式,使用 Nginx 作为静态文件服务器。不熟悉 Nginx ? 没关系,我们会有专门的一章讲解 Nginx ,以及有相应的练习项目。 做个深呼吸,开始动手了。
后端开发
在终端中新建一个项目:
$ django-admin startproject online_intepreter_project
$ cd online_intepreter_project && django-admin startapp online_intepreter_project
# 项目结构:
online_intepreter_project/
frontend/ # 前端目录
index.html
css/
bootstrap.css
main.css
js/
main.js
bootstrap.js
jquery.js
online_intepreter_project/ # 项目配置文件
__init__.py
settings.py # 项目配置
urls.py # URL 配置
online_intepreter_app/ # 我们真正的应用在这里
__init__.py
views.py # 视图
models.py # 模型
middlewares.py # 中间件
mixins.py # mixin
manage.py
# settings.py 的配置
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = '=@_j0i9=3-93xb1_9cr)i!ra56o1f$t&jhfb&pj(2n+k9ul8!l'
DEBUG = True
INSTALLED_APPS = ['online_intepreter_app']
MIDDLEWARE = ['online_intepreter_app.middlewares.put_middleware']
ROOT_URLCONF = 'online_intepreter_project.urls'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
INSTALLED_APPS
: 安装我们的应用。Django 会遍历这个列表中的应用,并在使用makemigrations
这个命令时才会自动的搜寻并创建我们应用的模型。
MIDDLEWARE
: 我们需要使用的中间件。由于 Django 不支持对 PUT 方法的数据处理,所以我们需要写一个中间件来给它加上这个功能。之后我们会更加详细的了解中间件的写法。
现在我们先来编写 PUT 中间件,来让 Django 支持 PUT 请求。我们可以使用 POST 方法向 Django 应用上传数据,并且可以使用 request.POST 来访问 POST 数据。我们也想像使用 POST 一样来使用 PUT ,利用 request.PUT 就可以访问到 PUT 请求的数据。
中间件是 django 很重要的一部分,它在请求和响应之间充当预处理器的角色。很多通用的逻辑可以放到这里,django 会自动的调用他们。 在这里,我们写了一个简单的中间件来处理 PUT 请求。只要是 PUT 请求,我们就对它作这样的处理。所以,当你对某个请求都有相同的处理操作时,可以把它写在中间件里。
所以,中间件是什么呢?中间件只是视图函数的公共部分。你把中间件的核心处理逻辑复制粘贴到视图函数中也是能够正常运行的。
打开你的 middlewares.py
:
from django.http import QuerDict
def put_middleware(get_response):
def middleware(request):
if request.method == 'PUT': # 如果是 PUT 请求
setattr(request, 'PUT', QueryDict(request.body)) # 给请求设置 PUT 属性,这样我们就可以在视图函数中访问这个属性了
# request.body 是请求的主体。我们知道请求有请求头,那请求的主体就是
# request.body 了。当然,你一定还会问,为什么这样就可以访问 PUT 请求的相关
# 数据了呢?这涉及到了 http 协议的知识,这里就不展开了,有兴趣的同学可以自行查阅资料
response = get_response(request) # 使用 get_response 返回响应
return response # 返回响应
return middleware # 返回核心的中间件处理函数
QueryDict
是 django 专门为请求的查询字符串做的数据结构,它类似字典,但是又不是字典。
request
对象的 POST
GET
属性都是这样的字典。类似字典,是因为 QueryDict
和 python 的 dict
有相似的 API 接口,所以你可以把它当字典来调用。
不是字典,是因为 QueryDict
允许同一个键有多个直。比如 {'a':[‘1’,‘2’]},a 同时有值 1 和 2,所以,一般不要用 QueryDict[key]
的形式来访问相应 key 的值,因为你得到的会是一个列表,而不是一个单一的值,应该用 QueryDict.get(key)
来获取你想要的值,除非你知道你在干什么,你才能这样来取值。为什么会允许多个值呢,因为 GET
请求中,常常有这种参数http://www.example.com/?action=search&achtion=filter
,
action
在这里有两个值,有时候我们需要对这两个值都作出响应。但是当你用 .get(key)
方法取值的时候,只会取到最新的一个值。如果确实需要访问这个键的多个值,应该用 .getList(key)
方法来访问,比如刚才的例子应该用 request.GET.getList('action')
来访问 action
的多个值。
同理,对于 POST
请求也应该这么做。
接下来要说说 request.body
。做过爬虫的同学一定都知道,请求有请求头,那这个 body
就是我们的请求体了。严格的讲,这个“请求体”应该叫做“载荷”,用英文来讲,这就叫做“payload”。载荷里又有许多的学问了,感兴趣的同学可以自己去了解相关的资料。只需要知道一件很简单的事情,就是把 request.body
放进 QueryDict
就可以把上传的字段转换为我们需要的字典了。
由于原生的 request
对象并没有 PUT 属性,所以我们需要在中间件中加上这个属性,这样我们就可以在视图函数中用 request.PUT
来访问 PUT 请求中的参数值了。
中间件在 1.11 版本里是一个可调用对象,和之前的类中间件不同。既然是可调用对象,那就有两种写法,一种是函数,因为函数就是一个可调用对象;一种是自己用类来写一个可调用对象,也就是包含 __call__()
方法的类。
在 1.11 版本中,中间件对象应该接收一个 get_response
的参数,这个参数用来获取上一个中间件处理之后的响应,每个中间件处理完请求之后都应该用这个函数来返回一个响应,我们不需要关心这个 get _response
函数是怎么写的,是什么东西,只需要记得在最后调用它,返回响应就好。这个最外层函数应该返回一个函数,用作真正的中间件处理。
在外层函数下写你的预处理逻辑,比如配置什么的。当然,你也可以在被返回的函数中写配置和预处理。但是这么做有时候就有些不直观,配置、预处理和核心逻辑分开,让看代码的人一眼就明白这个中间件是在做什么。最通常的例子是,很多的 API 会对请求做许多的处理,比如记录下这个请求的 IP 地址就可以先在这里做这个步骤;又比如,为了控制访问频率,可以先读取数据库中的访问数据,根据访问数据记录来决定要不要让这个请求进入到视图函数中。我们对 PUT 请求并没有什么预处理或者配置操作要进行,所以就什么都没写。
中间件的处理逻辑虽然简单,但是中间件的写法和作用大家还是需要掌握的。
接下来,让我们创建我们的模型,编辑你的 models.py
:
from django.db import models
# 创建 Cdoe 模型
class CodeModel(models.Model):
name = models.CharField(max_length=50) # 名字最长为 50 个字符
code = models.TextField() # 这个字段没有文本长度的限制
def __str__(self):
return 'Code(name={},id={})'.format(self.name,self.id)
在这里要注意一下,如果你是 py2 ,__str__
你需要改成 __unicode__
。我们的表结构很简单,这里就不多说了。
我们的 API 返回的是 json
数据类型,所以我们需要把最基础的响应方式更改为 JsonResponse
。同时,我们还有一个问题需要考虑,那就是如何把模型数据转换为 json
类型。 我们知道 REST 中所说的 “表现(表层)状态转换” 就是这个意思,把不同类型的数据转换为统一的类型,然后传送给前端。如果前端要求是 json
那么我们就传 json
过去,如果前端请求的是 xml
我们就传 xml
过去。这就是“内容协商(协作)”。当然,我们的应用很简单,就只有一种形式,但是如果是其它的大型应用,前端有时请求的是 json
格式的,有时请求的是 xml
格式的。我们的应用很简单,就不用考虑内容协商了。
回到我们的问题,我们该如何把模型数据转换为 json
数据呢? 把其它数据按照一定的格式保存下来,这个过程我们称为“序列化”。“序列化”这个词其实很形象,它把一系列的数据,按照一定的方式给整整齐齐的排列好,保存下来,以便他用。在 Django 中,Django 为我们提供了一些简单的序列化工具,我们可以使用这些工具来把模型的内容转换为 json
格式。
其中很重要的工具便是 serializers
了,看名字我们就这到它是用来干什么的。其核心函数 serialize(format, queryset[,fields])
就是用于把模型查询集转换为 json
字符串。它接收的三个参数分别为 format
,format
也就是序列化形式,如果我们需要 json
形式的,我们就把 format
赋值为 'json'
。 第二个参数为查询集或者是一个含有模型实例的可迭代对象,也就是说,这个参数只能接收类似于列表的数据结构。fields
是一个可选参数,他的作用就和 Django 表单中的 fields
一样,是用来控制哪些字段需要被序列化的。
编辑你的 views.py
:
from django.views import View # 引入最基本的类视图
from django.http import JsonResponse # 引入现成的响应类
from django.core.serializers import serialize # 引入序列化函数
from .models import CodeModel # 引入 Code 模型,记得加个 `.` 哦。
import json # 引入 json 库,我们会用它来处理 json 字符串。
# 定义最基本的 API 视图
class APIView(View):
def response(self,
queryset=None,
fields=None,
**kwargs):
"""
序列化传入的 queryset 或 其他 python 数据类型。返回一个 JsonResponse 。
:param queryset: 查询集,可以为 None
:param fields: 查询集中需要序列化的字段,可以为 None
:param kwargs: 其他需要序列化的关键字参数
:return: 返回 JsonResponse
"""
# 根据传入参数序列化查询集,得到序列化之后的 json 字符串
if queryset and fields:
serialized_data = serialize(format='json',
queryset=queryset,
fields=fields)
elif queryset:
serialized_data = serialize(format='json',
queryset=queryset)
else:
serialized_data = None
# 这一步很重要,在经过上面的查询步骤之后, serialized_data 已经是一个字符串
# 我们最终需要把它放入 JsonResponse 中,JsonResponse 只接受 python 数据类型
# 所以我们需要先把得到的 json 字符串转化为 python 数据结构。
instances = json.loads(serialized_data) if serialized_data else 'No instance'
data = {'instances': instances}
data.update(kwargs) # 添加其他的字段
return JsonResponse(data=data) # 返回响应
需要注意的是,我们先序列化了模型,然后又用 json
把它转换为了 python 的字典结构,因为我们还需要把模型的数据和我们的其它数据(kwargs
)放在一起之后才会把它变成真正的 json
数据类型。
接下来,重头戏到了,我们需要编写我们的 Mixin 了。 在编写 Mixin 之前,我们需要遵循以下几个原则:
- 每个 Mixin 只完成一个功能。这就像是我们在“上”中举的例子一样,一个 Mixin 只会让我们的“Man”类多一个功能出来。这是为了在使用的时候能够更加清晰的明白这个 Mixin 是干什么的,同时能够做到灵活的解耦功能,做到“即插即用”。
- 每个 Mixin 只操作自己知道的属性和方法,还是那我们之前的 “Man” 类来做例子。我们知道我们写的几个 Mixin 最终都是用于
Man
类的,然而Man
类的属性有name
、age
,所以在我们的 Mixin 中也可以像这样来访问这些属性:self.name
,self.age
。因为这些属性都是已知的。当然啦,Mixin 自己的属性当然也是可以自己调用的啦。那在 Mixin 中我们需要用到其它的 Mixin 的属性的时候该怎么办呢?很简单,直接继承这个 Mixin 就好了。 我们的 Mixin 最终是要作用到视图上的,所以我们可以把我们的基础视图的属性当作是已知属性。 我们的APIView
是View
类的子类,所以View
的所有属性和方法我们的Mixin
都可以调用。我们通常用到的属性有:
1. `kwargs`: 这是传入视图函数的关键字参数,我们可以在类视图中使用 `self.kwargs` 来访问这些传入的关键字参数
2. `args`: 传入视图的位置参数
3. `request`: 视图函数的第一个参数,也就是当前的请求对象,它和我们平时写的视图函数中的 request 是一模一样的
编写 Mixin 是为了代码的复用和代码的解耦,所以在正式开始编写之前,我们必须要想好,哪一些 Mixin 是我们需要编写的,哪一些逻辑是必须要写到视图函数中。 首先,凡是对于有查询动作的请求,我们都有一个从数据库中提取查询集的过程,所以我们需要编写一个提取查询集的 Mixin 。
第二,对于查询集来说,有时候我们需要的是整个查询集,有时候只是需要一个单一的查询实例,比如在更新和删除的时候,我们都是在对一个实例进行操作。所以我们还需要编写一个能够提取出单一实例的 Mixin 。
第三,对于 API 的通用操作来说,根据 REST 原则,每个请求都有自己的对应动作,比如 put 对应的是修改动作,post 对应的是创建动作,delete 对应的是删除动作,所以我们需要为这些通用的 API 动作一一编写 Mixin 。
第四,正如第三条考虑到的那样, API 的不同请求是有自己对应的默认动作的。如果我们的视图就是想简单的使用他们的默认动作,也就是 post 是创建动作,put 是修改动作,我们希望视图函数能自己将这些请求自己就映射到这些默认动作上,这样在之后的开发我们就可以什么都不用做了,连最基本的 get post 视图方法都不需要我们编写。所以我们需要编写一个方法映射 Mixin 。
最后,就我们的应用而言,我们应用是为了提供在线解释器服务,所以会有一个执行代码的功能,虽然到目前,这个功能的核心函数执行的代码很简单,但是谁能保证他一直都是这样简单呢?所以为了保持良好的视图解耦性,我们也需要把这部分的代码单独独立出来成为一个 Mixin 。
现在,让我们开始编写我们的 Mixin 。我们编写 Mixin 的活动都会在 mixins.py
中进行。
首先,在顶部引入需要用到的包
from django.db import models, IntegrityError # 查询失败时我们需要用到的模块
import subprocess # 用于运行代码
from django.http import Http404 # 当查询操作失败时返回404响应
IntegrityError
错误会在像数据库写入数据创建不成功时被抛出,这是我们需要捕捉并做出响应的错误。
获取查询集 Mixin 的编写:
class APIQuerysetMinx(object):
"""
用于获取查询集。在使用时,model 属性和 queryset 属性必有其一。
:model: 模型类
:queryet: 查询集
"""
model = None
queryset = None
def get_queryset(self):
"""
获取查询集。若有 model 参数,则默认返回所有的模型查询实例。
:return: 查询集
"""
# 检验相应参数是否被传入,若没有传入则抛出错误
assert self.model or self.queryset, 'No queryset fuound.'
if self.queryset:
return self.queryset
else:
return self.model.objects.all()
可以看到,我们的 Mixin 的设计很简单,只是为子类提供了两个参数 queryset
和model
,并且 get_queryset
这个方法会使用这两个属性返回相应的所有的实例查询集。我们可以这样使用它:
class GETView(APIQuerysetMinx, View):
model = MyModel
def get(self, *args, **kwargs):
return self.get_queryset()
这样我们的视图是不是看起来就方便,清晰了很多,视图逻辑和具体的操作逻辑相分离,这样方便别人阅读自己的代码,一看就知道是什么意思。在之后的 Mixin 使用也是同理的。
编写获取单一实例的 Mixin :
class APISingleObjectMixin(APIQuerysetMinx):
"""
用于获取当前请求中的实例。
:lookup_args: list, 用来规定查询参数的参数列表。默认为 ['pk','id]
"""
lookup_args = ['pk', 'id']
def get_object(self):
"""
通过查询 lookup_args 中的参数值来返回当前请求实例。当获取到参数值时,则停止
对之后的参数查询。参数顺序很重要。
:return: 一个单一的查询实例
"""
queryset = self.get_queryset() # 获取查询集
for key in self.lookup_args:
if self.kwargs.get(key):
id = self.kwargs[key] # 获取查询参数值
try:
instance = queryset.get(id=id) # 获取当前实例
return instance # 实例存在则返回实例
except models.ObjectDoesNotExist: # 捕捉实例不存在异常
raise Http404('No object found.') # 抛出404异常响应
raise Http404('No object found.') # 若遍历所以参数都未捕捉到值,则抛出404异常响应
我们可以看到,获取单一实例的方式是从传入视图函数的关键字参数kwargs
中获取对应的 id
或者 pk
然后从查询集中获取相应的实例。并且我们还可以灵活的配置查询的关键词是什么,这个 Mixin 还很方便使用的。
接下来我们需要编写的是获取列表的 Mixin
class APIListMixin(APIQuerysetMinx):
"""
API 中的 list 操作。
"""
def list(self, fields=None):
"""
返回查询集响应
:param fields: 查询集中希望被实例化的字段
:return: JsonResopnse
"""
return self.response(
queryset=self.get_queryset(),
fields=fields) # 返回响应
我们可以看到,我们只是简单的返回了查询集,并且默认的方法还支持传入需要的序列化的字段。
执行创建操作的 Mixin: