动力
之前学习Web 全栈时接触到了很多工具,其中我最喜欢的是 Django REST Framework ,下文将简称其为 DRF。因为框架本身非常小巧,但是功能相当完备,即使在单纯使用都给了我很多启发,于是自然而然我想要阅读它的源码。这也是我第一个系统阅读的开源项目。
概览
DRF 本身是 Django 的一个 App,最初 Django 产生于还没有 React 这些 UI 自动化框架的时代,本身是前后端不分离的,所以它使用模板语法做前端。到后来,前后端分离更加流行,后端只提供一个 API(往往是 JSON 格式的),这时 DRF 应运而生。它基于 Django 和 RESTful 规范,让用户能够用几行程序写出一整套 API。而且还包括序列化、反序列化、用户认证、限流、权限、搜索和过滤、文档生成等功能,这些功能本身都是高度可扩展的,作者写好了默认的实现,用户可以很轻松替换成自己的实现。
其中 View 是最高层次的概念,每个请求的处理多分为三部分:反序列化请求、调用数据库写具体逻辑、序列化响应,这三部分都是由 VIew 来调度的。其中作者在 VIew 的基础上实现的功能我称之为 “API 组件”。比如限流、权限等等都是一个 API 组件,一般都是基于类实现,暴露出方法给外部调用,然后 VIew 来决定什么时候怎么调用。
作者一般把这些 View 基础上的功能称之为 Policy,包括 renderer、parser、authentication、throttle、permission、content negotiation。我所谓的 API 组件范围更广一点,在这个基础上还包括了 serializer(序列化反序列化)。大致可以等同。
此外项目本身还贴心地带有一个调试器,可以把返回的 JSON 数据高亮和格式化,同时提供一个交互的表单。
我们先用 Cloc 软件来分析一下整个项目:
Language | files | blank | comment | code |
---|---|---|---|---|
PO File | 59 | 7039 | 7881 | 17098 |
Python | 67 | 2906 | 3129 | 10242 |
CSS | 9 | 150 | 44 | 2000 |
HTML | 65 | 224 | 11 | 1783 |
JavaScript | 12 | 425 | 594 | 1623 |
SVG | 2 | 0 | 0 | 702 |
其中 PO File 是用来翻译的文件。所以项目的核心只有一万行左右的 Python 程序,用很少的程序实现了非常复杂的功能。当前 DRF 已经进入了稳定期基本没有大的迭代了。我选取的源码版本是3.15,也是我阅读时的最新版本。
通读下来,其实整个项目没有特别奇技淫巧(类似数学中莫名其妙的辅助线或者恒等变换),我的个人感觉是,我和作者的差距主要在两点,一是对于软件工程的扎实理解,如何把整个软件设计得有条不紊,同时高度可扩展,就像搭积木一样,设计出小积木,一层层垒出大积木;二是对于工具的熟悉,比如作者对于 Django 的各种 API 非常熟悉,虽然是一个后端框架,但是也有一个功能完备的可视化调试工具,背后是对于HTML、CSS、Bootstrap 等库的熟练掌握。
如何阅读源码
阅读源码有一些通用的技巧,包括
- 要先了解这个程序的 API,了解行为是了解实现的前置条件。所以最好选取自己常用的工具。如果连一个程序想要做什么都不清楚,是很难阅读内部实现的。
- 自顶向下阅读,整个程序是有一个入口的,从入口这里也能找到比较高层次的调度逻辑,从而了解整个项目的各个部分是什么关系,如何被触发。这里相当于对于整个项目建立一个地图,如果这一步没有做好,很容易迷路。
- DRF 本身有一个社区项目,是为其标注类型然后配合 Mypy 做静态检查和自动补全,这个项目可以用作阅读源码的辅助,帮助我们查看函数的参数和返回值类型,并且快速概览类有哪些方法和属性。
- 勤做笔记。读源码的时候我会在项目里直接写一个 Markdown 作为笔记,有什么灵感就直接记录下来,这样方便后续复习以及整理成博客。
项目入口
整个项目的入口在 View 部分,这里的实现基于 Django 的 VIew,是整个项目的调度逻辑。项目的入口在 APIView 类的 dispatch 方法,我们抽取出其中的核心逻辑:
try:
self.initial(request, *args, **kwargs)
# Get the appropriate handler method
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
response = handler(request, *args, **kwargs)
except Exception as exc:
response = self.handle_exception(exc)
self.response = self.finalize_response(request, response, *args, **kwargs)
return self.response
可以看到,其实项目入口分为三个阶段:
- 初始化 VIew:
self.initial(request, *args, **kwargs)
- 找到 Handler 并且调用它得到响应:
response = handler(request, *args, **kwargs)
- 对响应进行处理:
self.response = self.finalize_response(request, response, *args, **kwargs)
- 其中前两步会尝试捕获其中的异常并且返回:
response = self.handle_exception(exc)
了解项目的入口非常重要,很多时候我们希望调用的方法没有得到调用,或者某个状态值在某一步被修改,这是可以从入口这里入手,通过打日志或者 Debugger 来了解问题到底出在哪里。
值得一提的是 APIView 的方法摆放顺序是有规律的,相似的放到一起,所以找起来很方便:
def get_authenticate_header(self, request: Request) -> str: ...
def get_parser_context(self, http_request: HttpRequest) -> dict[str, Any]: ...
def get_renderer_context(self) -> dict[str, Any]: ...
def get_exception_handler_context(self) -> dict[str, Any]: ...
def get_view_name(self) -> str: ...
def get_view_description(self, html: bool = ...) -> str: ...
def get_format_suffix(self, **kwargs: Any) -> str | None: ...
def get_renderers(self) -> list[BaseRenderer]: ...
def get_parsers(self) -> list[BaseParser]: ...
def get_authenticators(self) -> list[BaseAuthentication]: ...
def get_permissions(self) -> Sequence[_SupportsHasPermission]: ...
def get_throttles(self) -> list[BaseThrottle]: ...
def get_content_negotiator(self) -> BaseContentNegotiation: ...
def get_exception_handler(self) -> Callable: ...
def perform_content_negotiation(self, request: Request, force: bool = ...) -> tuple[BaseRenderer, str]: ...
def perform_authentication(self, request: Request) -> None: ...
def check_permissions(self, request: Request) -> None: ...
def check_object_permissions(self, request: Request, obj: Any) -> None: ...
def check_throttles(self, request: Request) -> None: ...
def determine_version(
self, request: Request, *args: Any, **kwargs: Any
) -> tuple[str | None, BaseVersioning | None]: ...
def initialize_request(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Request: ...
def initial(self, request: Request, *args: Any, **kwargs: Any) -> None: ...
def finalize_response(self, request: Request, response: Response, *args: Any, **kwargs: Any) -> Response: ...
def handle_exception(self, exc: Exception) -> Response: ...
def raise_uncaught_exception(self, exc: Exception) -> NoReturn: ...
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase: ... # type: ignore[override]
def options(self, request: Request, *args: Any, **kwargs: Any): ... # type: ignore[override]
可以看到,这些方法的顺序是获取 API组件、根据 API 组件做检查。比如 get_permissions
就是获取权限相关的组件,然后 check_permissions
和 check_object_permissions
来运行这些权限做检查。
APIView、Viewset、Generic Vew、Generic Viewset 的关系
学习 DRF 的一大难点就是框架提供的众多的 View,这里格外让人困惑。应用知识时我们往往要选择某个场景下使用那个知识,如果某些知识只具备微妙的差异,很容易让人产生决策困难。所以我想在这里争取一次性把众多 View 讲透彻,帮助读者攻克这个难点。
要想理解各种 view 的关系,首选的切入点就是层次以及继承关系,这样最直观:
图中同一水平位置代表同层次,箭头从 A 指向 B 代表 B 继承了 A。只要能理解这幅度,一定能理解各种 View 的关系并轻松驾驭。
其中 APIView 和 ViewSets 其实是同级的关系。APIView 就是一个非常单薄的类,需要用户写好各种 HTTP Handler 来处理请求,本身没有提供什么功能。ViewSets在 APIView 的基础上,让用户可以写 Action Handler,相较于 HTTP Handler 抽象度略微高一些,它的真正作用其实是在写 concrete ViewSet 的时候可以直接生成 URL,不需要去手动填写。总而言之它们两的抽象度是非常近似的。
而 GenericAPIView 是 DRF 真正迈出的一大步,它给 View 引入了 Serializer 和 数据库的 models ,以及各种 Policy。这就导致我们可以基于这些方法来写好固定的 RESTful API ,这些 RESTful API 称为 mixin,比如针对 POST 请求,可以写好创建 models 的类:
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 {}
其中 perform_create
是主方法 create
提供的 Hook,当用户想在 perform_create
时候做一些额外操作,可以直接重载 perform_create
方法。可以看到 create
的逻辑很直观:反序列化、校验、创建数据库中的数据、然后序列化。其实 RESTful 中的 GET、POST、PUT、PATCH、DELETE 方法对应的 mixin 逻辑都很简洁,并没有什么特别复杂、精妙的地方,但就是大道至简,能够给予这样直观的抽象节省大量的繁琐程序。
基于 API 的编程
其实 DRF 带给我最大的启发来自于它的扩展性,我从中提炼出了基于 API 编程这个方法论,写到后来我发现这个章节内容变得太多而且太重要了,于是单独为它写了这篇博客。
通读下来,其实 DRF 本身顶层就是 View 类,这里 View 之间的关系很容易让人迷惑,但是结合文档读完源码的继承关系之后便不再困惑。顶层往下便是作者为各种 Policy 设计的 API,这些 API 就像积木一样组合起来填充了 View 的细节,为它提供了更加丰富的功能。这些细节如果不涉及自己做定制或者调试,其实没有必要去深究。所以到了这一步我就暂时停止了阅读。