刚开始用 Django 那会儿很多人把模板当成“把 Python 变量塞进 HTML 的字符串替换工具”用着用着才发现,模板系统其实是整个框架里最容易写出“能跑但没法维护”代码的地方。折腾过几个项目之后我对 Django 模板的理解算是彻底转变了——它不是一个简单的渲染层而是衔接后端数据与前端表现的关键枢纽。这篇就围绕“Django 框架中的模板”展开把模板系统的设计逻辑、核心机制、实战用法和那些文档里不常写的坑一次性讲透。1. 模板在 Django 框架中的定位与核心设计思路1.1 模板到底解决了什么问题做 Web 开发本质上就干一件事把数据变成页面。Django 框架的 MTV 架构里模板承担了“V”的角色也就是视图层中负责呈现的那一半。Model 管数据View 管业务逻辑和取数Template 管怎么把数据渲染成用户看到的 HTML。可如果模板只是个“字符串替换工具”为什么 Django 不直接用 Python 拼接 HTML原因很简单职责边界。业务代码不该关心页面长什么样页面代码也不该包含业务逻辑。模板把表现层从业务逻辑里隔离出来前端可以专注写 HTML 结构后端可以专注处理数据。而且模板文件是静态的、可读的、可维护的比起在 Python 代码里拼一堆f-string模板的直观性和可维护性高了一个量级。我个人的理解是Django 模板本质上是一套受限的领域专用语言。它故意限制了你“能写什么”换来的是清晰、安全和团队协作的效率。这套 DSL 只有变量、标签、过滤器三大核心语法。1.2 模板引擎的工作机制当你在视图函数里调用render(request, xxx.html, context)时Django 做的事情远不止“读文件、替换变量”这么简单。完整的流程是根据APP_DIRS和DIRS配置从模板源目录中查找指定名称的模板文件。加载模板文件并解析为模板对象这个过程会做词法分析、语法分析构建节点树。将上下文字典作为数据源对模板节点树进行渲染输出字符串。返回 HttpResponse。这里有一个非常关键的机制模板编译是带缓存的。Django 的模板加载器在第一次加载某个模板时会把编译结果缓存到内存后续请求直接复用。所以不要在生产环境频繁修改模板文件——你可能看到“改了代码不生效”的假象其实是模板缓存没刷新。1.3 为什么选用模板引擎而不是其他方案很多从 Flask 转过来的开发者会问Jinja2 也挺好用Django 为什么坚持用自己的模板系统甚至有人想换掉 Django 的模板引擎。我的建议是项目初期不要动这个主意。Django 模板和框架的耦合虽然不深但 admin 后台、表单渲染、消息框架这些内置功能都依赖默认模板引擎。换引擎意味着重写大量基础组件得不偿失。而且 Django 模板设计得“笨”一点是有意的——它不让模板里写复杂逻辑强制你保持页面层简单。实际项目里我见过最离谱的写法是在模板里塞了上百行复杂逻辑用一堆with和嵌套if模拟 Python 代码结果模板比视图函数还难读。这属于设计思想上的错误不是模板本身的问题。2. 模板语言核心语法深度解析2.1 变量数据从视图到模板的唯一通道模板变量的基本形式是{{ variable }}配合“点号寻址”的取值规则。这是 Django 模板最容易被初学者误解的地方{{ user.profile.name }}并不只是“取字典里嵌套的值”Django 有自己的一套解析优先级先尝试字典下标user[profile][name]再尝试属性访问user.profile.name再尝试列表索引user[0]最后还是不行就抛VariableDoesNotExist渲染为空字符串严格模式下抛异常这个顺序非常关键。很多人以为点号一定代表属性访问实际上字典下标优先。所以当你有一个对象属性恰好和字典键重名时Django 取到的是字典值而不是属性值。踩过这个坑的人会明白、模板变量解析不是直观的“取属性”而是一套特定的查找协议。模板变量还有个容易被忽略的点默认情况下模板里访问不存在的变量不会报错只会渲染成空字符串。这给排查问题带来不少困扰——页面某个位置一片空白可能是变量确实为空也可能是变量名拼错了。建议开发阶段开启DEBUGTrue时配合django.template.context_processors.debug和TEMPLATES_DEBUG相关配置至少能通过工具识别变量名错误。2.2 标签模板里的控制结构标签用{% tag %}包裹是模板里做逻辑处理的唯一手段。常用的内置标签有以下几类条件分支{% if %}、{% elif %}、{% else %}支持and、or、not、in等运算符循环遍历{% for %}支持{% empty %}处理空列表模板嵌套{% include %}引入子模板{% extends %}继承父模板上下文控制{% with %}给复杂表达式起别名{% load %}加载自定义标签库这里要特别说{% for %}标签。Django 的 for 循环自带一个forloop对象提供循环状态信息forloop.counter从1开始计数、forloop.counter0从0开始、forloop.first、forloop.last、forloop.revcounter倒序计数。实际场景里最常用的是给表格加序号以及判断首尾元素做样式区分。还有个性能相关的冷知识forloop.parentloop可以访问嵌套循环的外层状态但这个对象在循环结束会失效不能保存后在循环外使用。我见过不少新手试图在模板里做累加、计数、字符串拼接等操作结果发现模板语言“没有变量赋值”“没有函数调用”根本做不了。这是设计使然——你需要这些逻辑时应该提前在视图函数里处理好把最终结果传到模板。2.3 过滤器数据格式化的最后一公里过滤器语法是{{ value|filter_name }}可以用管道符号链式拼接{{ value|lower|capfirst }}。内置过滤器解决的是“把数据变成适合用户阅读的格式”这个最朴素的需求。我平时使用频率最高的几个过滤器date{{ value|date:Y-m-d H:i }}专门格式化 datetime 对象default{{ value|default:暂无数据 }}处理空值展示length返回列表或字符串长度join用指定分隔符合并列表用法是{{ list|join:, }}truncatechars按字数截断字符串配合truncatechars_html处理带标签的内容safe标记字符串为安全内容不做 HTML 转义linebreaksbr把换行符变成br标签处理用户输入的多行文本特别好用这里必须重点说safe和escape。Django 模板默认开启自动转义所有变量输出时都会把等字符转成 HTML 实体防止 XSS 攻击。但如果你通过后台富文本编辑器存了 HTML 内容页面显示的是源码而不是渲染后的效果这时候就需要{{ content|safe }}。不过safe是把双刃剑——用得不当等于亲手关闭了安全防线。安全原则是只有对可信、已消毒的内容使用 safe用户直接输入的内容绝不能随便 safe。我曾经在一个项目里看到开发者对所有文章内容都加safe结果文章里出现alert(xss)这就是典型的自己给自己挖坑。2.4 模板变量与上下文的细节Django 视图传给模板的 context 是一个字典但真正渲染时模板引擎把数据包了一层Context对象同时还会从context_processors注入额外变量。这一点很多人没意识到——模板里能直接用的变量不只是视图传过来的那些还有全局注入的。默认的django.template.context_processors.debug、request、auth、messages会在每个模板渲染时注入一些常用变量。所以你在模板里能用{{ request.user }}、{{ request.path }}、{{ messages }}不是因为这些变量在视图里传了而是 context processor 帮你做了。自定义 context processor 是给全站模板注入公共数据的标准方案比如站点配置、导航菜单数据。模板变量查找时的优先级问题前面提过还有一个坑是上层作用域覆盖。如果你在视图里传入了变量name同时模板引擎 context processor 里也定义了name哪边生效答案取决于两者在 context 栈中的顺序。Django 的处理方式是“先到先得”不同配置下可能表现不同。为了避免这个问题建议全局注入的变量用统一前缀比如site_name、site_url避免与视图局部变量冲突。3. 模板继承与组件化实战3.1 继承体系的搭建Django 模板继承是复用页面的核心手段。典型做法是建一个base.html里面定义页面的框架结构用{% block %}标记出子模板可以覆盖的区域。基础模板怎么写给出一个生产环境的参考结构!-- base.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{% block title %}默认站点标题{% endblock %}/title {% block head_extra %}{% endblock %} /head body header {% block header %}全站公共头部{% endblock %} /header main {% block content %}这里放默认内容{% endblock %} /main footer {% block footer %}全站底部信息{% endblock %} /footer {% block bottom_scripts %}{% endblock %} /body /html子模板用{% extends base.html %}声明继承然后覆盖对应 block!-- list.html -- {% extends base.html %} {% block title %}用户列表 - 站点名{% endblock %} {% block content %} div classuser-list {% for user in users %} div classuser-card h3{{ user.name }}/h3 p{{ user.email }}/p /div {% empty %} p暂无用户/p {% endfor %} /div {% endblock %}继承的价值在于改动基础模板里的导航栏、页脚、统计代码、公共 CSS 引用所有子页面自动生效。如果你的项目有十几个页面都复制同一份导航栏代码维护起来就是灾难——每次改导航链接都要全站替换。用继承之后只需要改一个文件。3.2 多层嵌套与 include 的取舍项目规模变大后base.html可能不够用会出现多级模板继承。比如base.html全站基础框架含公共整体结构base_inner.html继承base.html增加内页公共元素侧边栏、面包屑product_list.html继承base_inner.html实现具体页面多层继承的思路是把“所有页面共有的”放在最底层把“某一类页面共有的”放在中间层把“单个页面特有的”放在最上层。分层的核心是抽象出共性和特性的边界没有统一标准但原则是避免一条继承链过于深——超过四层理解成本就显著上升。{% include %}则适合语义独立、多处复用的片段分页组件、导航条、文章卡片。它和{% extends %}的区别在于include是被动嵌入子片段没有能力覆盖父模板的 blockextends是主动继承子模板拥有覆盖权。做一个选择能继承就别 include能 include 就别复制粘贴。include有个隐藏性能点每次{% include %}都会触发一次独立的模板查找和渲染。如果页面里 include 了几十个模板渲染开销会明显增加。Django 对此有缓存优化但高频场景下还是要注意。3.3 自定义 block 的高级用法block 不只是“可以被覆盖的区域”它还支持{{ block.super }}——在子模板的 block 里引用父模板中同名 block 的内容。这个功能非常实用{% block content %} {{ block.super }} div classextra-content子页面追加的内容/div {% endblock %}这样你可以在不重写父模板整个 block 的前提下往里面追加内容。Reference 文档里提过但很多人没注意block.super可以解决大量重复代码问题特别是公共页面头部需要根据不同页面追加不同脚本时。block 还可以在子模板里定义新的内容父模板用不了子模板的 block。换句话说block 的作用域是向下的父模板定义“哪里可以覆盖”子模板决定“怎么覆盖”。我早期理解反了总想在base.html里引用子模板 block 的内容结果发现根本拿不到。4. 自定义模板标签与过滤器突破内置能力边界4.1 什么时候需要自定义内置标签和过滤器覆盖了 80% 的场景但总有些页面需要“模板里做不了的事”。典型场景在模板里格式化一个自定义对象对象没有提供可用的方法或属性需要访问数据库或缓存的数据但不想在视图函数里反复查询再传参页面里出现高度复用的 UI 片段包含复杂逻辑和 HTML这时候你有两个选择继续在视图里处理好在模板里输出或者自定义模板标签。我的判断标准是如果这个逻辑在多个视图里都要用到且每个视图都写一遍很繁琐就值得自定义模板标签。如果只是单个页面的临时需求优先在视图里搞定。4.2 编写自定义过滤器的完整步骤自定义过滤器是最简单的扩展方式。在应用目录下建templatetags包然后在里面建一个 Python 模块# 项目结构 # myapp/templatetags/my_filters.py # myapp/templatetags/__init__.py from django import template register template.Library() register.filter def format_phone(value): 手机号脱敏显示138****5678 value_str str(value) if len(value_str) 11: return value_str[:3] **** value_str[-4:] return value_str register.filter def status_label(value): 订单状态映射为中文标签 labels { 1: 待支付, 2: 已支付, 3: 已发货, 4: 已完成, } return labels.get(value, 未知状态)然后在模板里加载{% load my_filters %} p{{ user_phone|format_phone }}/p p{{ order.status|status_label }}/p过滤器函数的签名是function(value, arg)value 是管道左侧的值arg 是冒号后面的参数如果有。注意过滤器只能接受最多两个参数value 和一个 arg参数再多的需求就得用自定义标签解决。4.3 编写自定义标签简单标签与包含标签自定义标签有三种形态简单标签simple_tag、包含标签inclusion_tag、分配标签assignment_tag在较新版本中并入 simple_tag。生产环境里最常用的是前两种。simple_tag用于在模板里调用一段逻辑并输出结果register.simple_tag def current_time(format_string%Y-%m-%d %H:%M:%S): from datetime import datetime return datetime.now().strftime(format_string)模板中用{% current_time %Y-%m-%d %}。inclusion_tag则更进一步它返回一个字典Django 用这个字典渲染一个指定的模板片段再把渲染结果输出到调用位置。这是做“组件化”的最佳工具。举个例子做一个分页组件register.inclusion_tag(components/pagination.html) def render_pagination(current_page, total_pages, extra_query): return { current_page: current_page, total_pages: total_pages, extra_query: extra_query, }对应的components/pagination.htmldiv classpagination {% if current_page 1 %} a href?page{{ current_page|add:-1 }}{{ extra_query }}上一页/a {% endif %} {% for page_num in total_pages|get_range %} a href?page{{ page_num }}{{ extra_query }} class{% if page_num current_page %}active{% endif %} {{ page_num }} /a {% endfor %} {% if current_page total_pages %} a href?page{{ current_page|add:1 }}{{ extra_query }}下一页/a {% endif %} /div然后在其他模板里{% load pagination_tags %}再{% render_pagination page total_pages query %}一行代码就能渲染出完整的分页组件。团队里有人封装好了之后其他人不用关心分页逻辑的细节直接用就行。我的经验是inclusion_tag 是模板层做组件化的终极方案。它把 HTML、CSS 的类名、JavaScript 的初始化代码都封装在一个局部模板里对外只暴露“要用什么数据”。这套方案在多个项目验证过维护成本远低于到处复制粘贴。4.4 与视图函数的数据流配合自定义标签还有个优势可以直接访问模板上下文。通过takes_contextTrue参数标签函数可以拿到当前模板的所有上下文变量register.simple_tag(takes_contextTrue) def site_config(context, key): # context 里有 request 等全局变量 request context.get(request) # 从缓存或数据库读取站点配置 return get_site_config(request, key)这种模式适合做站点配置的全局读取、用户权限的判断、当前页面菜单的高亮等场景。但要注意tag 里不要写过于复杂的业务逻辑更不要在这里面做复杂的数据库查询循环。模板标签的调用频率可能远高于视图函数——列表页渲染几十个卡片每个卡片都调用 tag 查数据库会产生 N1 查询暴击数据库。5. 模板性能优化与安全最佳实践5.1 渲染性能从模板加载到页面输出模板性能优化是一个系统工程。从 Django 的角度看优化点主要有这几个第一减少模板继承的层级和 include 数量。每个模板解析、加载、渲染都有开销。继承链上每增加一层渲染时间都会增加。一个页面如果继承链 4 层且 include 了 10 个组件光模板渲染成本就不可忽略。做优化时需要权衡“组件化维护的收益”和“渲染性能的代价”。第二用{% with %}避免重复计算。在模板里如果写了{{ user.profile.get_full_name }}两次每次都会重新执行点号解析。用{% with %}把这个结果存下来再复用虽然收益微小但积少成多。第三合理使用缓存。Django 提供了模板片段缓存{% cache %}。对变化不频繁的片段推荐列表、侧边栏、公共组件用缓存效果立竿见影{% load cache %} {% cache 3600 sidebar_recent_news %} div classsidebar-news {% for item in recent_news %} a href{{ item.get_absolute_url }}{{ item.title }}/a {% endfor %} /div {% endcache %}这表示缓存 3600 秒缓存键是sidebar_recent_news。需要区分不同用户/不同参数时可以在缓存标签中加额外参数做缓存键。第四页面级缓存。整个页面如果没有个性化内容直接用cache_page(60 * 15)装饰器缓存视图输出。这是成本最低、收益最高的大杀器但前提是页面内容对所有用户一致否则容易串数据。5.2 安全XSS 与 CSRF 在模板层的防御模板系统的安全机制主要体现在两个方面自动转义Auto-escaping默认开启。所有模板变量输出都会被 HTML 转义。这个机制在 99% 情况下保护你免受 XSS 攻击。关键是不要为了“方便”随意关闭转义。需要输出富文本时不要直接用{{ content|safe }}。更好的方案是先用第三方库对 HTML 做清洗比如 bleach 库过滤掉危险标签和属性再标记为安全。或者干脆在富文本编辑器层面就限制可输入的标签。总之safe 是最后一道阀门而不是通行证。CSRF 令牌在 Django 的模板表单中你需要手动加入{% csrf_token %}标签Django 会把一个随机的 CSRF token 嵌入表单后续请求才能通过校验。这个标签必须在form标签里并且表单使用 POST 方法时才会生效。很多新手忘记加结果 POST 请求老是 403排查半天才发现是模板里少了这个标签。再提一个容易忽视的点模板里渲染 URL 参数时务必用urlencode过滤器。在模板里拼接带查询参数的 URL 时如果参数来自用户输入比如关键词搜索直接用?q{{ keyword }}可能造成查询字符串注入或显示问题。正确做法是?q{{ keyword|urlencode }}。5.3 模板组织架构的最佳实践模板文件组织方式直接关系到项目的可维护性。我在多个项目里总结出一套稳妥的目录结构templates/ ├── base.html ├── components/ │ ├── pagination.html │ ├── message_box.html │ └── user_avatar.html ├── accounts/ │ ├── login.html │ ├── register.html │ └── profile.html ├── orders/ │ ├── list.html │ ├── detail.html │ └── confirm.html └── includes/ ├── sidebar.html └── footer.html命名规律components/放可复用的组件模板includes/放被多个模板 include 的片段accounts/、orders/这类按业务模块分目录。约定大于配置项目里每个人都能快速定位模板。一个容易被忽略的问题模板文件的命名冲突。Django 的模板加载器在APP_DIRSTrue时按 INSTALLED_APPS 的顺序搜索每个应用的 templates 目录。如果两个应用都有templates/log_list.html后注册的应用会被先注册的覆盖。很多项目遇到“改了模板不生效”是因为有两个同名文件改了一个另一个没动。解决方案是每个应用在 templates 下再建一层以应用名命名的目录app1/templates/app1/list.html app2/templates/app2/list.html这样绝对不会有冲突这是 Django 官方推荐的做法。6. 模板调试与排查实战6.1 常见错误和异常模板运行期错误常见的有几类TemplateDoesNotExist模板文件不存在。排查方向应用有没有注册到 INSTALLED_APPS目录拼写和大小写是否正确模板文件是否在使用正确的加载器能访问到的目录Invalid block tag on line X: 模板里用了内置没有的标签。常见原因是忘了{% load %}自定义标签库或者标签有拼写错误。VariableDoesNotExist: 模板变量无法解析。生产环境默认静默处理开发调试时可以设置TEMPLATES的OPTIONS中的string_if_invalid为一个显眼的字符串比如[INVALID_VAR]这样模板里所有未定义变量都会显示这个标记排查变量名错误非常快。Too many levels of template inheritance继承层级太深Django 默认限制了继承链深度。我用过最有效的一套排查方法开启DEBUG True页面报错会有详细的模板上下文信息。使用模板调试工具{% debug %}标签可以在页面输出当前上下文的所有变量帮助确认变量是否传递到模板。检查request对象是否在模板中可用如果不可用确认 context_processors 配置是否正确。6.2 本地开发与生产环境的模板差异开发环境里我们对模板渲染的容错要求比较高方便发现问题生产环境则追求稳定性和性能。两者之间有一些配置差异值得注意开发环境TEMPLATES [ { BACKEND: django.template.backends.django.DjangoTemplates, DIRS: [BASE_DIR / templates], APP_DIRS: True, OPTIONS: { context_processors: [ django.template.context_processors.debug, django.template.context_processors.request, django.contrib.auth.context_processors.auth, django.contrib.messages.context_processors.messages, ], debug: True, }, }, ]生产环境建议TEMPLATES [ { BACKEND: django.template.backends.django.DjangoTemplates, DIRS: [BASE_DIR / templates], APP_DIRS: True, OPTIONS: { context_processors: [ django.template.context_processors.request, django.contrib.auth.context_processors.auth, django.contrib.messages.context_processors.messages, ], # 生产环境不开启 debug可以加自动重载关闭 auto_reload: False, }, }, ]auto_reload决定模板文件修改后是否自动重新加载。开发时开True方便调试生产时建议关掉节省一次文件检查的开销。Django 在生产模式DEBUGFalse下默认不会自动重载模板这也是“改了模板没反应”的原因来源。6.3 一个真实排查案例说一个我印象比较深的坑。某天线上订单列表页突然部分样式错乱用户反馈订单金额显示成1,000.00 元但有的显示成1000.00 元格式不一致。排查过程首先怀疑视图层数据问题检查发现数据在模型层都是 DecimalField精度一致。再怀疑模板过滤器问题发现金额字段有时套了floatformat:2有时没套。最终定位两个开发者分别开发了列表页的两个版本一个用了{{ order.amount|floatformat:2 }}另一个直接用{{ order.amount }}合代码时模板片段冲突导致页面部分渲染逻辑错乱。这个案例看起来是低级错误但暴露的真实问题是模板是团队协作里很容易产生冲突的层。这个教训让我在后续项目里定了规矩所有金额字段统一在视图层格式化为字符串模板里只做展示不再做格式化。这样即使模板被重构数据格式也不会乱。7. 模板与前端生态的衔接7.1 在模板中使用静态资源管理Django 的{% static %}标签是引入 CSS、JS、图片的标准方式{% load static %} link relstylesheet href{% static css/app.css %} img src{% static img/logo.png %} altlogo如果用 CDN 部署静态资源Django 官方推荐的方案是django.contrib.staticfiles的ManifestStaticFilesStorage。它会给静态文件名自动加上内容哈希解决浏览器缓存问题。模板里不用改任何代码{% static %}标签会自动生成带哈希的 URL。注意{% static %}标签和直接写路径完全不同。写死/static/css/app.css在本地开发没问题一旦部署时把静态文件移到 CDN或者改了 STATIC_URL写死的路径全部失效。{% static %}则根据 STATIC_URL 动态生成配合 storage 的哈希策略后还能自动做缓存刷新。7.2 模板与 JavaScript 的数据交互模板不只是输出 HTML经常需要把后端数据传给 JavaScript。有几个常见且坑多的场景在 script 标签中输出 JSON最常见但最危险的做法是script var data {{ json_data|safe }}; /script这样有风险。如果 json_data 里面包含/script会提前关闭标签造成 XSS。解决方案是使用 Django 的json_script过滤器它会把 JSON 数据放到一个带特定 id 的 script 标签里并且转义掉危险字符{{ json_data|json_script:my-data }} script var data JSON.parse(document.getElementById(my-data).textContent); /script这是我个人强烈推荐的方案Django 从 2.1 开始提供json_script过滤器但很多人还在用旧的safe方案。安全无小事。传递上下文变量到 JS 的替代思路如果只是需要后端 URL、少量配置值可以不用 JSON 传输直接在 HTML 元素的>{# 示例: 渲染一个分页组件 #} {% render_pagination page_obj total_pages query %} {# 示例: 渲染一个消息提示框 #} {% render_message message_typesuccess content操作成功 %}组件库命名的前缀我习惯统一用render_开头看到render_就知道是通用组件不是业务视图。还有一个经验模板层也应当做回归测试。可以写 Django TestCase 测试模板渲染结果from django.test import TestCase from django.template import Template, Context class TemplateTests(TestCase): def test_phone_filter(self): template Template({% load my_filters %}{{ phone|format_phone }}) context Context({phone: 13812345678}) rendered template.render(context) self.assertEqual(rendered, 138****5678)这套测试虽然简单但能防止过滤器和标签在重构时被改坏。很多团队只测 API 不测模板等到线上页面显示异常了才发现。建议至少对自定义模板标签和过滤器覆盖一轮基础测试成本很低但收益稳定。8. 模板开发中值得坚持的工作习惯最后聊聊我用了这些年 Django 模板之后沉淀下来的一些工作习惯每一条都是真金白银的教训换来的。习惯一不到万不得已不在模板里写 Python 逻辑。如果发现自己在模板里使用了{% if %}嵌套了七八层或者试图用{% with %}模拟变量赋值赶紧停下来。把逻辑搬到视图处理模板只负责展示永远不要让模板去做业务决策。习惯二把格式化规则统一收敛。日期格式、金额格式、状态显示标签统一用过滤器或自定义过滤器处理不要看到一处写一处。否则改一个“日期格式从 2024-1-1 改成 2024年1月1日”要在十几个模板里找。统一收敛后改一个过滤器就全站生效。我甚至会专门开一个common_filters.py模块放这类格式化的过滤器作为团队通用的规范。习惯三建立模板命名和目录的规范文档。这个可能听起来过于“工程化”但当项目超过 30 个模板文件后没有规范就意味着灾难。团队里约定每个页面的模板文件对应哪个视图、组件放哪个目录、include 片段如何命名项目整体质量会上一个台阶。习惯四开发时保留一份“模板验收清单”。页面上渲染的数据是不是经过转义的用户输入的富文本是否清洗过表单有没有加 CSRF 标签静态资源是不是用{% static %}引入的有没有循环里查数据库的情况这些虽然是老生常谈但每次上线前过一遍清单能挡掉绝大多数低级错误。我个人在实际操作中的一个体会是Django 模板是一个上限很高、下限也很低的东西。它内置的机制并不复杂复杂的是你如何组织它。用好了它是团队协作的加速器页面维护成本极低用乱了它就是代码里最难看的一坨。做模板开发时多想想“这个模板一年后别人来看能不能一眼看懂”基本就不会走偏。