动力

之前学习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 软件来分析一下整个项目:

Languagefilesblankcommentcode
PO File597039788117098
Python672906312910242
CSS9150442000
HTML65224111783
JavaScript124255941623
SVG200702

其中 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_permissionscheck_object_permissions 来运行这些权限做检查。

APIView、Viewset、Generic Vew、Generic Viewset 的关系

学习 DRF 的一大难点就是框架提供的众多的 View,这里格外让人困惑。应用知识时我们往往要选择某个场景下使用那个知识,如果某些知识只具备微妙的差异,很容易让人产生决策困难。所以我想在这里争取一次性把众多 View 讲透彻,帮助读者攻克这个难点。

要想理解各种 view 的关系,首选的切入点就是层次以及继承关系,这样最直观:

views

图中同一水平位置代表同层次,箭头从 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 的细节,为它提供了更加丰富的功能。这些细节如果不涉及自己做定制或者调试,其实没有必要去深究。所以到了这一步我就暂时停止了阅读。