DRF渲染器全解析:原理、配置与自定义实战
发布时间:2026/10/17 23:18:00 锦皓数字建站

第一次接触 Django REST framework 的人十有八九会被 Renderers 渲染器绕晕。明明视图里写的是return Response({...})结果浏览器打开是一个可交互的调试页面用 requests 请求拿到的又是 JSON同一个接口凭什么能自动切换格式答案就在渲染器这套机制里。我把 DRF 的渲染器读了又用、用了又踩坑之后才彻底搞清楚它到底管到哪一步、怎么配、什么时候需要自己写一个。这篇文章按我自己的理解把整条链路拆开讲一遍从内部机制到配置方式再到自定义渲染器实战和几个容易踩的坑适合刚接触 DRF 或者对渲染器一知半解的读者。1. 别被名字骗了渲染器管的是“序列化之后、传输之前”的那段1.1 先分清三个容易混淆的组件DRF 里有三个组件名字长得很像职责却完全不同序列化器 Serializer、渲染器 Renderer、解析器 Parser。很多同学把渲染器当成序列化器用或者以为渲染器能把 ORM 对象直接变成 JSON这些理解都会在动手改造接口时出问题。序列化器 Serializer把 User 对象、QuerySet 转换成普通的数据类型dict、list、字符串核心工作是字段映射、校验、规范化。它解决的是“Python 对象怎么变成 JSON 能放进去的结构”。渲染器 Renderer把序列化器输出的 data在 HTTP 响应阶段变成真正的字节流bytes同时决定 Content-Type 是什么。它解决的是“这份数据以什么面孔出现在网络上”。解析器 Parser是渲染器的反向过程把请求体里的 JSON/表单数据解析成 dict再交给序列化器校验。一句话区分Serializer 做数据清洗Parser 管请求进门Renderer 管响应出门。渲染器并不负责把 QuerySet 变成 JSON这件事在序列化器阶段已经做完了渲染器只是拿着已经“洗干净”的数据按照你要求的格式打包发出去。1.2 渲染器的输入输出长什么样DRF 的渲染器都继承自BaseRenderer核心方法只有几个。你自己写一个最简渲染器时实现下面这个render方法就够了from rest_framework.renderers import BaseRenderer class MyRenderer(BaseRenderer): media_type text/plain format txt def render(self, data, accepted_media_typeNone, renderer_contextNone): return str(data).encode(utf-8)这里有个非常关键的点render方法收到的data参数是视图返回的Response里携带的数据。如果视图老老实实返回了serializer.data那data就是已经规范化过的 dict 或 list如果视图偷懒直接返回 QuerySet 甚至模型对象那渲染器能不能处理就得看它自己的本事了。JSONRenderer的内部实现大致是这样的思路用json.dumps把 data 变成字符串再按charset编码成 bytes。它之所以能处理部分非标准类型是因为 DRF 给它配了一个扩展的 JSONEncoder能对付 Date、Decimal、UUID 等类型加几个常见的场景。但记住自定义渲染器没有这个默认加成遇到非基础类型时最好在视图层就把数据洗干净让渲染器专心做格式转换。2. 渲染链路拆解从视图 return 到 Response 字节流的完整旅程2.1 关键选择发生在 finalize_response先回顾一个最简单的 DRF 视图执行流程。APIView 的 dispatch 大体做三件事先做initial完成用户认证、权限、限流检查然后调用具体 handler比如get拿到一个 Response 对象最后进入finalize_response对 Response 做最后的加工。内容协商和渲染器选择就发生在finalize_response这一步。DRF 在这个阶段会拿着视图上登记的 renderer_classes结合请求信息做一次内容协商从中选出一个最合适的渲染器然后把它绑定到 Response 上。大致逻辑是这样的# DRF 源码里的关键动作做了删减便于理解 def finalize_response(self, request, response, *args, **kwargs): # 1. 获取渲染器集合 renderers self.get_renderers() # 2. 内容协商选择渲染器 content_negotiator self.get_content_negotiator() accepted_renderer, accepted_media_type content_negotiator.select_renderer( request, renderers ) # 3. 把选中的渲染器绑定到 Response 上 response.accepted_renderer accepted_renderer response.accepted_media_type accepted_media_type # 4. 构建渲染上下文后续 render 会用到 response.renderer_context self.get_renderer_context() return response注意这只针对 DRF 自己的Response不是 Django 原生的HttpResponse。如果你在视图里返回的是HttpResponse或者JsonResponseDRF 不干预原样放行内容协商和渲染器都轮不到它干活。这其实是很多人写混的一个点原生 Django 的 JsonResponse 是自己在视图里把数据转成 JSON 的DRF 的 Response 则把这个职责推迟到了渲染器阶段。2.2 Response.render() 怎么把选择变成实际内容DRF 的 Response 是 Django TemplateResponse 的子类这意味着它会搭上 Django 响应机制里“模板渲染”这趟车。在请求处理收尾时Django 会对 TemplateResponse 调用render()把响应内容真正生成出来。DRF 重写了这个方法所以当你对 Response 调用render()时实际发生的是def render(self): # 如果已经有 rendering 结果直接返回 if self._is_rendered: return self.rendered_content # 调用渲染器把 data 变成 bytes ret self.accepted_renderer.render( dataself.data, accepted_media_typeself.accepted_media_type, renderer_contextself.renderer_context, ) # 再交给 Django 的 Content-type 处理逻辑 self.rendered_content ret self[Content-Type] self.accepted_media_type self._is_rendered True return self所以整条链路可以这样概括视图拿到序列化好的 data包进 Responsefinalize_response 阶段内容协商选中一个渲染器把它绑定到 Response 上Django 响应机制触发 Response.render()渲染器的 render 方法把 data 变成字节流最后 Django 把这段字节流发回客户端。打个比方序列化器是把馅料切好装盘渲染器是选择外带打包盒和标签纸。同一个菜你用塑料盒装就是快餐外卖用漂亮的礼盒装就成了伴手礼。客户端最终拿到的“包装样式”完全由渲染器决定。3. 换格式的三种“指挥”方式内容协商究竟听谁的3.1 URL 后缀和 format 参数显式指定优先级最高客户端想让接口返回特定格式最常见也最明确的方式是在 URL 上直接写/api/users.json或者/api/users?formatcsv。这种形式优先级最高因为它是显式指定不需要服务端猜。DRF 对 URL 后缀的支持来自format_suffix_patterns用起来非常简单from django.urls import path from rest_framework.urlpatterns import format_suffix_patterns urlpatterns format_suffix_patterns([ path(users/, views.UserList.as_view()), path(users/int:pk/, views.UserDetail.as_view()), ])加上这层包装后/api/users.json会命中同一个视图同时视图的format会被解析成json。内容协商时DRF 会优先根据这个 format 值去匹配视图的 renderer_classes 里 format 属性相同的渲染器。如果没有匹配的渲染器协商就会失败。如果没配format_suffix_patterns也可以改用查询参数/api/users?formatcsv。DRF 的内容协商机制依旧会先从request.query_params.get(format)里读取只要渲染器列表里有 format 属性等于csv的渲染器它就会被选中。同一个接口同时支持多种格式时我一般优先推荐 URL 后缀因为语义更清晰也方便后续做接口版本规划。3.2 Accept 头隐藏在请求里的默认命令大多数客户端不会在 URL 上写格式后缀它们靠 HTTP 的Accept请求头告诉服务端自己想要什么。内容协商的兜底逻辑就是读Accept头逐个尝试匹配 renderer_classes 里的 media_type。打个比方浏览器地址栏直接访问/api/users/时浏览器默认带上的 Accept 头里有text/html而且排在最前面所以 DRF 会自动选中 media_type 为text/html的渲染器——默认配置下就是 BrowsableAPIRenderer所以你看到的是一个调试页面而不是一串 JSON。再用 requests 访问同一个地址requests 默认 Accept 是*/*不会优先匹配 text/html内容协商时通常会按 renderer_classes 顺序选择第一个能匹配的渲染器。默认配置中 JSONRenderer 排在前面所以拿到的是 JSON。用 curl 显式指定想要的格式也可以curl -H Accept: application/json http://127.0.0.1:8000/api/users/这就是为什么同一个视图能“自动”输出不同格式的本质。协商优先级可以汇总成一张表优先级依据示例说明1URL 后缀/api/users.json最明确不会产生歧义2format 查询参数/api/users?formatcsv显式指定适合不便改 URL 的场景3Accept 请求头Accept: text/csv依赖客户端声明服务端尝试匹配URL 后缀和查询参数同时出现时DRF 源码里是先看后缀再看查询参数不过正常场景没人会同时写两个了解优先级顺序即可。3.3 协商失败时会发生什么如果客户端要求的格式在 renderer_classes 里根本不存在内容协商会抛出NotAcceptable异常最终返回 406 Not Acceptable。举个例子你视图只配了 JSONRenderer客户端偏要curl -H Accept: text/xml http://127.0.0.1:8000/api/users/DRF 会返回 406响应体是 DRF 默认的异常格式{ detail: Could not satisfy the request Accept header }很多新手第一次看到 406 就懵了以为自己接口写错了。其实不是接口的错是内容协商按规则拒绝了“它给不了的东西”。这个行为也提醒我们接口设计时要先想清楚对外承诺支持哪些格式别让客户端的 Accept 头和服务端的 renderer_classes 长期处于“一个要北一个往南”的状态。4. 默认渲染器与配置引路用最稳的方式配出多格式 API4.1 全局配置与视图级覆盖DRF 默认的渲染器配置在DEFAULT_RENDERER_CLASSES你可以在 settings.py 里覆盖REST_FRAMEWORK { DEFAULT_RENDERER_CLASSES: [ rest_framework.renderers.JSONRenderer, rest_framework.renderers.BrowsableAPIRenderer, ], }这是全局配置影响所有视图。但实际项目中不同接口想要的格式往往不一样列表页可以同时支持 JSON 和 CSV上传接口只需要 JSON某些内部接口甚至只需要自定义格式。这时候就要在视图级别覆盖了。APIView 里用renderer_classes属性from rest_framework.views import APIView from rest_framework.renderers import JSONRenderer class UserList(APIView): renderer_classes [JSONRenderer]函数视图用装饰器from rest_framework.decorators import api_view, renderer_classes api_view([GET]) renderer_classes([JSONRenderer]) def user_list(request): pass配置粒度从小到大有三种全局配置、类属性、装饰器。具体落在哪个粒度取决于你对“这个接口的格式边界”的定义。我的习惯是全局只留 JSON 和开发用的 Browsable对内接口默认就这个组合遇到确需导出的接口再单独挂上 CSV 或者其他自定义渲染器避免全局配置把接口格式撑得太大。4.2 DRF 自带渲染器各自的适用场景DRF 内置的渲染器不算少很多人只见过 JSONRenderer 和 BrowsableAPIRenderer。下面这张表把它们一次性列清楚渲染器media_typeformat典型用途JSONRendererapplication/jsonjson最常用API 默认响应格式BrowsableAPIRenderertext/htmlapi浏览器调试页开发期利器AdminRenderertext/htmladmin类 Django admin 的可视化界面HTMLFormRenderertext/htmlform只渲染表单适合嵌入页面MultiPartRenderermultipart/form-datamultipart处理文件上传类请求TemplateHTMLRenderertext/htmlhtml用 Django 模板渲染数据页面XMLRendererapplication/xmlxmlXML 输出需要额外 XML 库支持几个容易被误解的点AdminRenderer 不是让你用 Django admin它只是提供一种更像 admin 的后台式可视化表单TemplateHTMLRenderer 适合那种一个接口直接渲染整页 HTML 的场景但它返回的是页面而不是单纯的数据XMLRenderer 默认配置里并不启用因为 DRF 没有内置 XML 库要真用得自己准备 XML 序列化库并做适配。对绝大多数项目生产环境用 JSONRenderer 就够撑起天下了。XML、CSV、PDF 这些格式如果客户端真有需求优先通过自定义渲染器按需实现而不是一股脑全堆到全局配置里。5. 自建渲染器从一个 CSV 导出需求说起5.1 定义媒体类型与 format 标识DRF 渲染器的扩展点非常清爽一个自定义渲染器通常就是一个几十行的类。以导出 CSV 这个最经典的需求为例先建一个renderers.pyfrom rest_framework.renderers import BaseRenderer class CSVRenderer(BaseRenderer): media_type text/csv format csv def render(self, data, accepted_media_typeNone, renderer_contextNone): if data is None: return b rows data.get(results, data) if isinstance(data, dict) else data if not rows: return b if isinstance(rows[0], dict): headers list(rows[0].keys()) else: headers None ...media_type用于内容协商时匹配 Accept 头format用于匹配 URL 后缀和 format 查询参数。这两者是内容协商识别你这个渲染器的“身份证”一个都不能少。render方法返回的是 bytesDRF 会直接把它放到响应体里。5.2 在 render 方法里组装字节流接着把 render 方法补完整。组装 CSV 字符串不复杂但有几个细节决定导出的文件好用不好用def render(self, data, accepted_media_typeNone, renderer_contextNone): if data is None: return b rows data.get(results, data) if isinstance(data, dict) else data if not rows: return b first rows[0] if isinstance(first, dict): headers list(first.keys()) lines [headers] for row in rows: lines.append([row.get(h, ) for h in headers]) else: lines [[value]] [[row] for row in rows] csv_text \n.join(,.join(str(cell) for cell in row) for row in lines) response renderer_context[response] response[Content-Disposition] attachment; filenameexport.csv return csv_text.encode(utf-8-sig)这里有两个非常值得记住的坑。第一个Content-Disposition一定要在 render 方法里通过renderer_context[response]设置而不是在视图里设置。因为视图返回 Response 后DRF 的 finalize_response 会覆盖一部分响应头你在 render 阶段改才来得及。第二个CSV 编码我用的是utf-8-sig而不是utf-8。原因很朴素绝大多数用 Excel 打开 CSV 的用户遇到 UTF-8 无 BOM 的文件会中文乱码utf-8-sig会在文件头部写入 BOM 标记Excel 就能正确识别编码。这个细节不亲自给需求方传几次文件很难意识到它的重要性。5.3 内容协商如何匹配自定义渲染器自定义渲染器写好后在视图上登记就能用from rest_framework.renderers import JSONRenderer class UserExportView(APIView): renderer_classes [JSONRenderer, CSVRenderer]这样/api/users/export.csv会命中 CSVRenderer普通请求/api/users/export/则默认返回 JSON因为 JSONRenderer 在列表前面而普通请求的 Accept 头没指定 text/csv 时协商结果会落到第一个可用的 JSONRenderer 上。如果想用查询参数触发访问/api/users/export/?formatcsv一样会走 CSVRenderer。这里我建议统一走 URL 后缀因为?formatcsv容易被业务参数混在一起排查时会多一层干扰。另外提醒一句给 CSV 渲染器配Content-Disposition: attachment后浏览器直接访问该 URL 会触发文件下载而不是在内页打开这是导出接口通常想要的效果。如果你只是想在浏览器里预览 CSV 内容就别加这个响应头改成默认text/csv展示就行。6. Browsable API 是怎么“画”出来的6.1 HTML 页面其实是渲染器的作品BrowsableAPI 是 DRF 极具辨识度的一个功能很多初学者还以为它是某个中间件或者额外的调试工具。其实它就是一个渲染器media_type 是text/htmlformat 是api。它的内部做法是先把同一份 data 用 JSONRenderer 渲染成 JSON 字符串再把这个字符串连同表单、请求选项一起塞进一个 HTML 模板最后输出成一个完整页面。所以你在浏览器里看到的那个能填表、能发请求、能看 JSON 的调试面板本质是“页面型渲染器”对同一份数据做了另一层包装。这也解释了为什么同一个视图既可以输出 JSON又可以输出调试页——两者只是选择了不同的渲染器而已。在实际调试时如果你想确认某个响应到底走的是哪个渲染器可以在 renderer_context 里做文章。renderer_context 会带上 view、request、response、args、kwargs 这些信息你在自定义渲染器里打印一下就能看到完整上下文def render(self, data, accepted_media_typeNone, renderer_contextNone): print(renderer_context[view].__class__.__name__) print(renderer_context[request])我在排查“为什么浏览器看到的是页面而接口返回的是 JSON”这种问题时就是靠这招定位的——无非是内容协商激活了不同的 renderer。6.2 开发与生产环境如何取舍BrowsableAPI 很好用但生产环境一定不要保留。原因有三个第一调试页面会暴露接口字段结构、OPTIONS 信息、表单选项对正经业务来说这些信息没必要展示给调用方第二渲染 HTML 页面会有额外的模板渲染开销虽然单个请求不大但高并发下这成本毫无意义第三BrowsableAPI 页面自带可交互能力等于把一部分调试入口暴露给了网络访客怎么想都不稳妥。生产配置我通常收敛成极简组合REST_FRAMEWORK { DEFAULT_RENDERER_CLASSES: [ rest_framework.renderers.JSONRenderer, ], }开发环境则可以放心用默认配置保留 HTML 调试页能大幅提升调试速度。用环境变量区分配置是最简单的做法settings 里写个判断即可。7. 实战中绕不开的边界场景7.1 大文件导出流式渲染与分页干扰自定义渲染器虽然灵活但它有个软肋走 DRF 的 Response 时渲染结果是在内存里一次性生成并返回的。CSV 数据量大到十几万行时整块 CSV 字符串在内存里占用的空间相当可观一不小心就把接口拖垮。大型导出我建议绕开渲染器直接用 Django 原生的 StreamingHttpResponse 做流式输出import csv from django.http import StreamingHttpResponse def csv_export(request): response StreamingHttpResponse(content_typetext/csv) response[Content-Disposition] attachment; filenameusers.csv writer csv.writer(response) writer.writerow([id, name, email]) for user in User.objects.iterator(): writer.writerow([user.id, user.name, user.email]) return response流式写入的要点是生产者逐行取数客户端逐块接收内存占用不随数据总量线性增长。多用户导出场景下这个方案的稳定性远高于“先拼完整 CSV 再返回”。另一个容易被忽略的坑是分页。如果导出视图继承了带分页的 ListAPIView那么视图传给渲染器的 data 是分页外壳也就是{count, next, previous, results}这样的结构。如果你直接拿 data 做 CSV第一行就会变成count、next这些字段。上面自定义渲染器里我用data.get(results, data)就是为了剥离这层外壳这也是做导出类渲染器时必要的一步。7.2 异常响应也会走渲染器视图抛出校验错误、权限错误时DRF 的 exception_handler 会生成一个异常 Response。关键是这个 Response 在 finalize_response 阶段同样会做内容协商也就是说异常响应也会被渲染器包装。这就带来一个非常隐蔽的坑如果你的视图只挂了 BrowsableAPIRenderer 和 CSVRenderer客户端用 Accept: application/json 请求这个接口正常返回没问题但如果接口抛异常内容协商找不到 JSONRenderer客户端拿到的很可能是 406 而不是预期的错误 JSON。我第一次遇到这个现象时排查了很久最后才意识到是渲染器列表把 JSONRenderer 挤掉了。从那以后凡是自定义渲染器多的项目我在全局配置或视图配置上都会保留 JSONRenderer 作为兜底确保异常响应在任意 Accept 组合下都能稳定输出 JSON 错误信息。这是渲染器相关坑里性价比最高的一条经验。7.3 换掉 JSONRenderer 用 orjson 的注意点追求 JSON 序列化性能的项目会考虑用 orjson 替换默认 JSONRenderer。自定义方式很简单import orjson from rest_framework.renderers import JSONRenderer class ORJSONRenderer(JSONRenderer): media_type application/json format json def render(self, data, accepted_media_typeNone, renderer_contextNone): return orjson.dumps(data)但这里有个大坑如果视图直接返回模型对象或者包含 datetime、Decimal、UUID 的 dictorjson 会直接抛TypeError因为 orjson 默认不做栈外类型转换。默认 JSONRenderer 不会报错是因为 DRF 的 JSONEncoder 兜了底。解决方案有两个。最推荐的是规则化视图层保持“视图返回 serializer.data”的好习惯在序列化器输出阶段就把类型洗干净如果实在要处理原始数据那就给 orjson 配置一个转换函数import orjson from datetime import date, datetime from decimal import Decimal from uuid import UUID def default_converter(value): if isinstance(value, (datetime, date)): return value.isoformat() if isinstance(value, Decimal): return str(value) if isinstance(value, UUID): return str(value) raise TypeError class ORJSONRenderer(JSONRenderer): media_type application/json format json def render(self, data, accepted_media_typeNone, renderer_contextNone): return orjson.dumps(data, defaultdefault_converter)换 orjson 前最好用接口自测脚本把常见返回结构都打一遍确认数据边界。只测 happy path 就上生产迟早会有某个字段在某个分支里悄悄引入非标准类型到时候线上报错比本地复现要痛苦得多。我自己在项目里把 JSON、CSV 几种渲染器混用之后才真正理解 DRF 渲染器存在的意义——它把“数据怎么呈现”这件事从视图逻辑里完全拆了出来。以后业务方再提“还要加一个 Excel 导出”我只需要新增一个 Renderer 类视图代码一行都不用改。这个内核设计带来的维护体验比在视图里写一堆 if format xlsx 舒服太多了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。