33 Django 的第三方插件库
今天我们会介绍在 Django 生态中比较火的一些第三方插件。正是这些插件,让我们开发网站变得如此简单。同时我会就一个热门的第三方框架谈一谈如何学习该框架的源码。接下来,让我们一起开始今天的学习吧。
1. Django 中常用的插件库
1.1 Django Rest Framework
Django Rest Framework 是一个强大且灵活的工具包,用以快速构建 Web API。为啥要使用它来构建 Web API呢?除了 DRF 可以在 Django 的基础上迅速实现 API 外,它自身还带有 WEB 的测试页面,可以方便的测试自己的 API。这点非常类似于 Java Web 开发中的 Swagger 插件,对于我们测试自己的 API 接口时非常有帮助的。
我们来总结 DRF 框架具备的一些特点:
- 提供了定义序列化器 Serializer 的方法,可以快速根据 Django ORM 或者其他库自动序列化/反序列化;
- 提供了丰富的类视图 和 Mixin 扩展类,可以进一步简化视图的编写;
- 多种身份认证和权限认证方式的支持;
- 内置了限流系统;
- 直观的 API Web界面;
- 可扩展性 , 插件丰富;
- 完备的文档以及良好的社区支持
- 诸多大厂(如 Mozilla, Red Hat 等)使用和点赞;
最后作为一个开源框架,查看下其项目 Github 地址时十分必要的。一个原则是:stars 越多,项目越火,可以放心使用。
项目的地址和文档如下:
可以看到,无论是从 star 数还是 contributor 人数上看, DRF 都可以称为是热门框架了。大部分做 Django 开发的都会使用 DRF 这个插件,用于开发高质量的 Web API,这个框架也是我们后面学习和研究的重点。
1.2 Django Celery
首先介绍下 Celery 模块,Celery 是一款非常简单、灵活、可靠的分布式系统,可用于处理大量消息,并且提供了一整套操作此系统的一系列工具。另外,Celery 是一款消息队列工具,可用于处理实时数据以及任务调度。官网给出了 Celery 如下四个特点:
- 简单:开箱机用,维护简单,不需要使用配置文件;
- 高可靠性:如果连接丢失或者出现故障,客户端进程会自动重试。一些 broker 会以主/主或者主/备的方式维持系统的高可靠性;
- 快速:单个 Celery 进程每分钟内可以处理数百万个任务,往返的延迟在毫秒级别(使用RabbitMQ 做中间件,再加上一些优化的设置);
- 灵活:几乎 Celery 的每个部分都可以自行扩展,如使用自定义池、序列化器,压缩方案、日志记录,调度程序,消费者,生产者,代理传输等等。
注意到基于 Django 框架构建的 Web 系统实际上是一个同步服务。这意味着当客户端发起一个请求时,后端只有在视图函数处理完后才会返回结果。如果这个请求背后要做的工作比较耗时,或者因为某种原因导致非常耗时,那么此时客户端会一直等待请求的响应,这非常影响用户体验。对于一个优秀的网站而言,良好的用户体验十分重要,这也说明了一个支持异步功能的第三方插件的重要性。为了能让 Django 搭建的 Web 系统支持这样的异步功能,于是 django-celery 便应运而生。
django-celery项目之后也被移到 celery 下进行统一管理。它相比原 celery 项目在 star 数和贡献者数上要逊色不少。
可以看到,Celery 模块火热程度可以媲美 DRF。而它不仅仅用在 Django 项目中,其它框架甚至个人项目中也常常使用 Celery 来完成异步场景的需求。
1.3 Django Guardian
上一节中我们介绍了 Django 的权限管理, 知道 Django 仅仅提供的是一种全局权限。这种简单的全局权限控制机制在很多场景下并不适用,因此需要引入另一种更细的权限机制:对象权限 (object permission)。所谓的 Object Permission 其实是一种对象颗粒度上的权限机制,它允许为每个具体对象授权 ,在 Django 中其实已经包含了 object permission 的模块,但没有具体实现,必须要使用第三方的插件完成相应的功能。django-guardian 是目前比较活跃的一个 django extension,提供了一种有效的 object permission 控制机制,与 django 原生机制一脉相承,而且能快速整合到 django-admin 中,十分推荐使用。
接下来继续看下它在 Github 上的表现:
django-guardian 项目的地址和文档如下:
从更新的频率上看,django-guardian 项目还是非常活跃的,更新速度可能没有 Django 这样的大型框架频繁,但是还是保持着一定的迭代速度,是比较值得使用并花心思研究的。
1.4 Django Xadmin
Django Xadmin 是国内程序员 sshwsfc 基于 Django 打造的一款简单易用而且又好看的后台管理系统。官网宣传语便是:打造管理系统从未如此简单。其宣称的几个核心特点如下:
- 基于 Bootstrap3,适合多种屏幕显示;
- 内置功能丰富,除了基本的 CURD 功能外,还有丰富的插件功能,如数据的导出、书签、图表、图片相册等多种扩展功能;
- 强大的插件系统,通过制作 Xadmin 插件可以扩展系统的任何一个功能点;
- 完善的权限系统,可配置、可定制,安全可靠;
来看一看它的 Github 地址, 主要关注 star 和 contributor 人数。可以看到这个项目的火热程度和 DRF 项目相比还有点距离,在国产框架中已经算很不错的了。但是截止到2020年5月4日,看到的上次 commit 记录好像还是2019年4月份,项目似乎是停滞更新了。如果是生产环境的话,需要慎重考虑,但是作为学习使用,还是十分推荐的。
2. 如何深入插件源码学习?
我们以 DRF 框架为例,聊一聊如何深入 DRF 框架的源码学习。首先肯定是下载稳定版本为 DRF 源码到本地,这是为了方便自己阅读代码。截止到2020年5月10日,DRF 的 Github 官方地址发布的最新版本为3.11.0,我们会用该版本的代码来进行相关的演示和说明。以下是 DRF-3.11.0 源代码截图,里面的代码量还是比较大的,不过相对于 Django 的代码而言就会少很多,我们前面能学习并跟踪 Django 框架的源码,拿下 DRF 源码自然也不在话下。
一般而言,推荐学习一个 Django 第三方插件源码的过程如下:
- 第一步:熟练使用 Django 框架以及熟悉 Django 框架源码。所有的 Django 第三方插件代码里会大量调用 Django 源码的类或者方法,并在其基础上进行扩展或者进一步创新。以我们必须先掌握 Django 的源码,才能继续学习 DRF 的源码;
- 第二步:仔细阅读官方文档手册进行学习,掌握框架的基本用法;
- 第三步:通过官方文档,实战 DRF 框架;每次在用熟练 DRF 提供的类或者方法后,就可以对应地查看源码,并分析 DRF 背后所做的工作。每掌握一个模块的基本用法,就可以深入学习对应模块的源码,同时在源码中我们还可以发现该模块中的更多用法,然后再次实践,以加深对源码的理解。
我们按照上面的过程来简单走一遍。首先我们前面对 Django 的几大模块的源码都有涉猎,算是满足了第一步要求。接下来我们用官方给的快速入门教程完成我们的第一次 Django REST framework 框架的初体验。
模型序列化器:给会员表 member 添加一个序列化器类,放到新建的 serializers.py 文件中。
from rest_framework import serializers from hello_app.models import Member class MemberSerializer(serializers.ModelSerializer): class Meta: model = Member fields = ("id", "name", "age", "sex", "occupation", "phone_num", "email", "city", "vip_level_id")
准备 View 视图:添加一个对会员表操作的视图类,我们用最简单的形式即可。
# 代码位置:hello_app/views.py # ... from rest_framework import viewsets from rest_framework import permissions # ... class MemberViewSet(viewsets.ModelViewSet): # 设置queryset queryset = Member.objects.all().order_by('-register_date') # 设置序列化器 serializer_class = MemberSerializer # 设置认证器 permission_classes = [permissions.IsAuthenticated]
编写 URLConf 配置:Django REST framework 框架改良了 URLConf 配置的写法,后面会研究这种写法,先直接使用官方的示例即可。
# 代码位置:hello_app/urls.py # ... from rest_framework import routers router = routers.DefaultRouter() router.register(r'members', views.MemberViewSet) urlpatterns = [ # ... path('', include(router.urls)) ]
另外,由于我们对
MemberViewSet
视图加上了认证,所以必须要在入口的 urls.py 中上如下的 URLConf 的配置。# 代码位置: first_django_app/urls.py # ... urlpatterns = [ # ... path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) ]
注意:不添加和添加这行 URLConf 配置的效果图如下所示。
接下来,最后一步是设置视图的相关配置以及注册 rest_framework 应用。
# 代码位置:first_django_app/settings.py # ... INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 注册第三方应用 'rest_framework', # 注册应用 'hello_app' ] REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 5 } # ...
最后我们启动服务,来一起看看效果。我们之前创建过一个超级用户admin/admin.1234!,接下来会用这个通过 DRF 的认证。
从上面的演示中,我们看到了 Django REST framework 框架给我们做的接口测试页面,我们只需要简单继承下MemberViewSet
即可,然后添加相关属性即可立即拥有这样一个完整的接口测试页面。后台服务主要提供接口数据,我们也可以使用 curl 命令来获取和操作相应的模型表。
[root@server ~]# curl -H 'Accept: application/json; indent=4' -u admin:admin.1234! http://127.0.0.1:8888/hello/members/?page=3
{
"count": 103,
"next": "http://127.0.0.1:8888/hello/members/?page=4",
"previous": "http://127.0.0.1:8888/hello/members/?page=2",
"results": [
{
"id": 9,
"name": "spyinx-5",
"age": "39",
"sex": 0,
"occupation": "product",
"phone_num": "18015702646",
"email": "225@qq.com",
"city": "shanghai",
"vip_level_id": null
},
{
"id": 10,
"name": "spyinx-6",
"age": "26",
"sex": 0,
"occupation": "ops",
"phone_num": "18790082215",
"email": "226@qq.com",
"city": "beijing",
"vip_level_id": null
},
{
"id": 11,
"name": "spyinx-7",
"age": "23",
"sex": 0,
"occupation": "security",
"phone_num": "18354491889",
"email": "227@qq.com",
"city": "guangzhou",
"vip_level_id": null
},
{
"id": 12,
"name": "spyinx-8",
"age": "26",
"sex": 1,
"occupation": "ui",
"phone_num": "18406891676",
"email": "228@qq.com",
"city": "wuhan",
"vip_level_id": null
},
{
"id": 13,
"name": "spyinx-9",
"age": "26",
"sex": 0,
"occupation": "ops",
"phone_num": "18036496230",
"email": "229@qq.com",
"city": "wuhan",
"vip_level_id": null
}
]
}
在上面这个过程走通之后,我们可以看到其实这个例子中已经涉及到了 DRF 中的许多类,比如用于序列化的类ModelSerializer
、视图类 ModelViewSet
、分页类 PageNumberPagination
等等。从这个案例中,我们可以找到许多学习 DRF 源码的切入点。首先看用到的视图类 ModelViewSet
:
# 源码位置:rest_framework/viewsets.py
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
"""
pass
通过学习 Django 的视图,我们了解了 Mixin 这个概念,所以容易理解这里的代码,视图继承 GenericViewSet
,同时也继承了数个 Mixin。这些 Mixin 从命名上就很容易知道其功能用法。进一步翻看其实现类,也能发现其具体含义 。以 mixins.CreateModelMixin
类为例:
# rest_framework/mixins.py
class CreateModelMixin:
"""
Create a model instance.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
serializer.save()
def get_success_headers(self, data):
try:
return {'Location': str(data[api_settings.URL_FIELD_NAME])}
except (TypeError, KeyError):
return {}
CreateModelMixin
的主要功能就是提供了 create()
方法,让视图拥有新增记录的功能。其他的 Mixin 会提供类似的函数,让视图具有某一特定的功能。接下来我们的重点放到 GenericViewSet
类的学习上。
# 源码位置:rest_framework/viewsets.py
class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
"""
The GenericViewSet class does not provide any actions by default,
but does include the base set of generic view behavior, such as
the `get_object` and `get_queryset` methods.
"""
pass
这里又是多继承,一个 ViewSetMixin
类,另一个 generics.GenericAPIView
类。先追后面的 View 类,实现代码如下:
从这里我们看到了一些熟悉的属性,如 queryset
,serializer_class
以及用于分页的 pagination_class
。这个继承的 APIView
类同样也是 Django REST framework 框架自己定义的类,我们继续追进 APIView
类的实现代码:
最后 APIView
这个类继承的 View
正是 Django 中我们学过的 View 视图类。
from django.views.generic import View
那这样子,我们也算清楚了一些事情。Django REST framework 框架中定义的视图是在 Django 的 View 视图类上封装和改进来的。现在一个疑问就来了,看我们前面使用 Django 的视图中,URLConf 配置如下:
urlpatterns = [
path('test-cbv/', views.TestView.as_view(), name="test-cbv"),
]
我们也分析过对应的 View 类以及 as_view()
方法,它将 GET 请求映射到视图类的 get()
方法,POST 请求则映射到 post()
方法;然而我们这里一路走下来并有没有看到对应的 get()
或者 post()
方法。但是视图类继承的多个 Mixin 中提供了 create()
、list()
等这样的方法,那么他们是如何和 URLConf 配置对应上的呢?我们现在要通过代码去找出前面配置 URLConf 代码的内部原理:
from django.conf.urls import include
from rest_framework import routers
router = routers.DefaultRouter()
router.register(r'members', views.MemberViewSet)
urlpatterns = [
# ...
path('', include(router.urls))
]
来看看上面的 URLConf 配置。这个时候,我们需要去看 Django REST Framework 中的 DefaultRouter
类,包括注册方法 register
() 以及 urls
属性值的获取。最后还要看 Django 中的 include() 方法的代码,才能理清楚 URL 和视图的映射关系。
先追踪 Django REST Framework 中的 DefaultRouter
类实现,该类继承自 SimpleRouter
,SimpleRouter
又继承自 BaseRouter
。为了加快速度,我们直接定位到基类 BaseRouter,可以看到 register()
方法和 urls
属性的定义,如下:
# 源码位置:rest_framework/routers.py
class BaseRouter:
def __init__(self):
self.registry = []
def register(self, prefix, viewset, basename=None):
if basename is None:
basename = self.get_default_basename(viewset)
self.registry.append((prefix, viewset, basename))
# invalidate the urls cache
if hasattr(self, '_urls'):
del self._urls
def get_default_basename(self, viewset):
"""
If `basename` is not specified, attempt to automatically determine
it from the viewset.
"""
raise NotImplementedError('get_default_basename must be overridden')
def get_urls(self):
"""
Return a list of URL patterns, given the registered viewsets.
"""
raise NotImplementedError('get_urls must be overridden')
@property
def urls(self):
if not hasattr(self, '_urls'):
self._urls = self.get_urls()
return self._urls
可以看到,在执行 router.register(r'members', views.MemberViewSet)
后其实等同于给 registry
数组添加一个元组元素,用于存储映射关系。而 urls
属性值则是调用 get_urls()
方法得到的。
class DefaultRouter(SimpleRouter):
"""
The default router extends the SimpleRouter, but also adds in a default
API root view, and adds format suffix patterns to the URLs.
"""
# ...
def get_urls(self):
"""
Generate the list of URL patterns, including a default root view
for the API, and appending `.json` style format suffixes.
"""
urls = super().get_urls()
if self.include_root_view:
view = self.get_api_root_view(api_urls=urls)
root_url = url(r'^$', view, name=self.root_view_name)
urls.append(root_url)
if self.include_format_suffixes:
urls = format_suffix_patterns(urls)
return urls
可以看到它先是调用了父类的 get_urls()
方法,另外又添加了一些映射规则。我们添加如下一行 print()
语句:
class DefaultRouter(SimpleRouter):
def get_urls(self):
"""
Generate the list of URL patterns, including a default root view
for the API, and appending `.json` style format suffixes.
"""
urls = super().get_urls()
print('父类调用得到的urls={}'.format(urls))
# ...
然后启动服务,可以看到如下的结果:
(django-manual) [root@server first_django_app]# python manage.py runserver 0:8888
Watching for file changes with StatReloader
Performing system checks...
父类调用得到的urls=[<URLPattern '^members/$' [name='member-list']>, <URLPattern '^members/(?P<pk>[^/.]+)/$' [name='member-detail']>]
System check identified no issues (0 silenced).
May 15, 2020 - 13:30:04
Django version 2.2.12, using settings 'first_django_app.settings'
Starting development server at http://0:8888/
Quit the server with CONTROL-C
可以看到,这个 ^members/$
的URL 配置是由父类的 get_urls()
方法得到的。在父类 SimpleRouter
中的get_urls()
方法中,我已经做好了相关的注释,最关键的代码就在最后的 append()
部分,那里添加的便是最后 URL 和 视图函数的关系。
class SimpleRouter(BaseRouter):
# ...
def get_urls(self):
# ...
# 前面介绍过这个 registry 属性,就是通过 register() 方法得到的
for prefix, viewset, basename in self.registry:
# ...
for route in routes:
# Only actions which actually exist on the viewset will be bound
mapping = self.get_method_map(viewset, route.mapping)
if not mapping:
continue
# Build the url pattern
regex = route.url.format(
prefix=prefix,
lookup=lookup,
# 尾部加上"/"
trailing_slash=self.trailing_slash
)
# 处理一些简单情况
if not prefix and regex[:2] == '^/':
regex = '^' + regex[2:]
initkwargs = route.initkwargs.copy()
initkwargs.update({
'basename': basename,
'detail': route.detail,
})
# 最最核心的部分代码,这里得到视图函数
view = viewset.as_view(mapping, **initkwargs)
# 视图名称
name = route.name.format(basename=basename)
# 添加映射规则
ret.append(url(regex, view, name=name))
return ret
我们可以看到最后添加的映射规则就是这一句:ret.append(url(regex, view, name=name))
,我们继续看看这个 url()
方法,它调用的正是 Django 中的 url()
方法,内容如下:
# 源码路径:django/conf/urls.py
# ...
def url(regex, view, kwargs=None, name=None):
return re_path(regex, view, kwargs, name)
这个 url()
方法和我们之前在 Django 中用 repath()
以及 path()
差不多一致的。第一个参数是 url 规则,第二个便是视图函数。比较重要的就是这里得到 view
的函数了,它便是真正的视图函数。它和前面 Django 中的一样,通过 as_view()
得到的。那么这个 as_view()
方法在哪呢,通过父类追踪,可知 Django 的父类中本身就有 as_view()
方法,但是在前一个继承的Mixin 中重写了该方法,因此调用的便是该 Mixin 中的 as_view()
方法:
class ViewSetMixin:
"""
This is the magic.
Overrides `.as_view()` so that it takes an `actions` keyword that performs
the binding of HTTP methods to actions on the Resource.
For example, to create a concrete view binding the 'GET' and 'POST' methods
to the 'list' and 'create' actions...
view = MyViewSet.as_view({'get': 'list', 'post': 'create'})
"""
@classonlymethod
def as_view(cls, actions=None, **initkwargs):
"""
Because of the way class based views create a closure around the
instantiated view, we need to totally reimplement `.as_view`,
and slightly modify the view function that is created and returned.
"""
# ...
def view(request, *args, **kwargs):
self = cls(**initkwargs)
# We also store the mapping of request methods to actions,
# so that we can later set the action attribute.
# eg. `self.action = 'list'` on an incoming GET request.
self.action_map = actions
# Bind methods to actions
# This is the bit that's different to a standard view
for method, action in actions.items():
handler = getattr(self, action)
setattr(self, method, handler)
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
self.request = request
self.args = args
self.kwargs = kwargs
# And continue as usual
return self.dispatch(request, *args, **kwargs)
# take name and docstring from class
update_wrapper(view, cls, updated=())
# and possible attributes set by decorators
# like csrf_exempt from dispatch
update_wrapper(view, cls.dispatch, assigned=())
# We need to set these on the view function, so that breadcrumb
# generation can pick out these bits of information from a
# resolved URL.
view.cls = cls
view.initkwargs = initkwargs
view.actions = actions
return csrf_exempt(view)
和 Django 中的一样,这里最后的 as_view()
方法最后返回的便是视图函数。那么对应的 /hello/members/
请求进来后,有 view()
方法进行处理,最后调用的和 Django 中的一样:
return self.dispatch(request, *args, **kwargs)
我们去 Django 中看这个 dispatch()
方法的源码:
# 源码位置:django/views/generic/base.py
class View:
"""
Intentionally simple parent class for all views. Only implements
dispatch-by-method and simple sanity checking.
"""
# ...
def dispatch(self, request, *args, **kwargs):
# Try to dispatch to the right method; if a method doesn't exist,
# defer to the error handler. Also defer to the error handler if the
# request method isn't on the approved list.
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
return handler(request, *args, **kwargs)
那么执行 /hello/members/
请求到这里是,handler 是哪个?我们继续翻看前面的 Mixin 类,有这样一段代码:
class ViewSetMixin:
# ...
@classonlymethod
def as_view(cls, actions=None, **initkwargs):
def view(request, *args, **kwargs):
# ...
for method, action in actions.items():
handler = getattr(self, action)
setattr(self, method, handler)
# ...
# ...
# ...
这里就非常明显了,我们大概也能猜到一些。就是设置 (get|post|put|delete) 请求对应的方法,比较好的方式时我们在这里打印下请求,并在前端进行下请求测试,看看这里到底设置了啥?
class ViewSetMixin:
# ...
@classonlymethod
def as_view(cls, actions=None, **initkwargs):
def view(request, *args, **kwargs):
# ...
for method, action in actions.items():
print('请求处理view视图函数:method={}, action={}'.format(method, action))
handler = getattr(self, action)
setattr(self, method, handler)
# ...
# ...
# ...
我们启动服务请求以下路径 /hello/members/
,可以得到如下输出结果:
(django-manual) [root@server first_django_app]# python manage.py runserver 0:8888
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
May 15, 2020 - 17:07:38
Django version 2.2.12, using settings 'first_django_app.settings'
Starting development server at http://0:8888/
Quit the server with CONTROL-C.
请求处理view视图函数:method=get, action=list
请求处理view视图函数:method=post, action=create
[15/May/2020 17:07:43] "GET /hello/members/ HTTP/1.1" 200 14426
结合 Django 中的 dispatch()
方法,我们终于知道了 get 请求最后会调用视图类中的 list()
方法去处理,而这个 list()
方法正是 ListModelMixin 中的。另外 post 请求则对应着视图类中的 create()
方法,而这个属性则来自 CreateModelMixin。这样我们总算理解了前面的 URLConf 的映射流程以及对应的真正视图处理函数。
带着问题去追源码是我比较推荐的一个学习方式。完成一个模块的学习就要去思考,去追踪这个案例背后的执行过程,这样才能更好的掌握这个模块。今天的分享就到此结束了,DRF 中还有很多代码等着你们去探索,去实践,祝大家学习愉快!
3. 小结
本小节中,我们介绍了多种流行的基于 Django 的第三方插件,好用又好玩,而且它们的组合能帮我们迅速开发一个完整而又美妙的网站。当然,流行的第三方插件远不止这些,我们在掌握好一定的开发基础后,也可以自行制作第三方插件。然而长路漫漫,还需要静下心来仔细学习和研究代码,才有可能在未来成为别人眼中的大神。