Architecture 2026-05-05·Ievgenii Svyryd

Django admin 是整个 Web 开发领域中最被低估的框架特性

Django admin 仅凭模型定义就能渲染出完整的 CRUD 界面。大多数框架连想都不敢想。这就是它为何改变了我对 UI 的思考方式、如何启发了一个生产级的 schema 驱动表单引擎,以及业界为何对 Web 开发中最好的点子视而不见。

Django admin 是整个 Web 开发领域中最被低估的框架特性

豁然开朗的那一刻

当我第一次运行 python manage.py createsuperuser、打开 /admin/,看到项目中每一个模型都拥有一个功能完备的 CRUD 界面——带有搜索、筛选、分页、内联编辑和关系处理——的时候,我简直不敢相信眼前所见。

我写了个模板、个表单类、行 JavaScript。Django 只是看了看我用 Python 写的模型定义,然后说:“我掌握的信息已经足够为你构建整个 UI 了。”

就在那一刻,我领悟到了大多数 Web 开发者似乎从未真正内化的一件事:如果你的数据是有结构的,那么你的 UI 就可以从这个结构中推导出来。模型就是规范。其余的一切都只是渲染而已。

Django Admin 实际上做了什么(以及它为什么令人抓狂)

让我把 Django admin 免费提供给你的东西一一说清楚,因为那些从没用过它的人并不了解它的覆盖范围:

  • 自动生成表单 —— 模型上的每个字段都会变成对应正确的控件:CharField 变成文本输入框,BooleanField 变成复选框,ForeignKey 变成可搜索的下拉框,ManyToManyField 变成双栏选择器。
  • 来自模型的校验 —— max_lengthblank=Falsechoices、自定义校验器 —— 全部在表单中自动执行。你只需在模型上定义一次约束,它就会传播到各处。
  • 带搜索、筛选和排序的列表视图 —— 在 ModelAdmin 上声明 list_displaylist_filtersearch_fields,你就得到了一个生产级别的数据浏览器。
  • 内联编辑 —— 关联模型可以在同一个页面上编辑。给发票添加明细项?内联。给联系人添加地址?内联。无需额外的视图。
  • 权限与审计日志 —— 每一次创建、更新和删除都会被记录。基于分组的权限控制谁能看到什么。这是内建的,而非外挂上去的。
  • 历史记录与回退 —— 每一次变更都会被追踪。你可以看到谁在什么时候改动了什么。

而最厉害的一点是:所有这一切都是可定制的。你并没有被锁死在默认设置里。你可以覆盖模板、添加自定义操作、注入 JavaScript、替换控件,并构建完全自定义的视图 —— 同时保留那些行之有效的部分。

# 一个完整的后台管理界面所需要的全部代码就在这里
from django.contrib import admin
from .models import Campaign, Donation, Designation


class DonationInline(admin.TabularInline):
    model = Donation
    extra = 0
    readonly_fields = ["created_at", "processed_at"]


@admin.register(Campaign)
class CampaignAdmin(admin.ModelAdmin):
    list_display = ["name", "goal", "raised", "donor_count", "is_active"]
    list_filter = ["is_active", "created_at"]
    search_fields = ["name", "description"]
    inlines = [DonationInline]
    readonly_fields = ["raised", "donor_count"]

# 就这样。现在你已经拥有:
# - 一个可搜索、可筛选的活动列表
# - 带有合适控件、点击即可编辑的详情页
# - 每个活动上的内联捐款管理
# - 权限、审计日志和历史记录追踪
# - 所有表单都会依据你的模型约束进行校验

为什么行业其余部分对此视而不见

让我百思不得其解的是:Django 早在 2005 年 就有了这个功能。它从第一天起就随框架一起发布。然而二十年过去了,主流 Web 开发圈子仍然把从零构建管理面板当作理所当然。

看看人们转而在做什么:

  • 为内部工具打造定制的 React 仪表盘——花上数周时间去做 Django 用 20 行代码就能给你的东西
  • 为 SaaS 管理面板生成器付费,而它生成的样板代码你之后还得自己维护
  • 搭建 Laravel Nova、Filament 或类似工具,它们本质上都是在试图重现 Django admin 原生就能做到的事
  • 写上数百行本可以从数据模型推导出来的表单组件

原因既简单又令人惋惜:大多数框架的模型自省能力都不足以驱动一套 UI。Django 的 Options API(Model._meta)暴露了每一个字段、每一个关系、每一个约束、每一个选项。这个 ORM 不只是一个查询构建器——它是一个 schema 注册表。

# Django 的模型内省能力非常强大
from myapp.models import Donation

# 获取所有字段及其类型
for field in Donation._meta.get_fields():
    print(f"{field.name}: {field.__class__.__name__}")
    # amount: DecimalField
    # donor: ForeignKey
    # campaign: ForeignKey
    # designation: ForeignKey
    # created_at: DateTimeField

# 获取选项、验证器、约束——一应俱全
amount_field = Donation._meta.get_field("amount")
print(amount_field.max_digits)    # 10
print(amount_field.decimal_places) # 2
print(amount_field.validators)    # [MinValueValidator(0.01)]

Django Admin 如何启发了一套生产级表单引擎

使用 Django admin 不仅让我提高了效率——它重塑了我对构建界面的思考方式。核心洞见看似简单:

如果你的数据结构是已知的,那么你的 UI 结构就是可以推导出来的。

我把这个想法带进了一个完全不同的技术栈——一个 Laravel/PHP 的筹款平台——它成了整个表单系统的基石。这个平台需要渲染捐款表单、订阅表单、活动报名表单和调查问卷表单。每个客户需要不同的字段、不同的布局、不同的校验规则。传统做法是:为每一种表单编写一个 Blade 模板和一个 Form Request 类。而在有 200 多个客户的情况下,这将是一场维护噩梦。

相反,我们构建了一个 schema 驱动的表单引擎——直接借鉴了 Django admin 从模型定义推导 UI 的方式。

Schema:面向 UI 的 JSON 契约

平台中的每个表单都以 JSON 配置的形式存储在数据库中。该 schema 定义了 pages、blocks、columns、groups、rows 和 fields——一个完整描述表单布局与行为的层级结构:

{
  "pages": [
    {
      "label": "Gift",
      "blocks": [
        {
          "columns": [
            {
              "width": "16",
              "items": [
                {
                  "type": "group",
                  "mode": "single",
                  "rows": [
                    {
                      "fields": [
                        {
                          "label": "Amount",
                          "name": "ask_amounts",
                          "type": "button-group",
                          "properties": {
                            "options": [
                              {"text": "10", "value": ""},
                              {"text": "50", "value": ""},
                              {"text": "100", "value": ""}
                            ],
                            "prefix": "$"
                          },
                          "rules": [],
                          "validation": []
                        }
                      ]
                    }
                  ]
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

这是把 Django admin 的洞见应用到另一个领域:模式就是表单。渲染器读取配置,为每种字段类型生成正确的控件——文本输入框、下拉菜单、按钮组、指派选择器、日期选择器——完全不需要为每个表单编写模板代码。

字段迭代器:遍历 Schema

这种方法催生的最优雅的模式之一是字段迭代器。由于表单配置是一个深度嵌套的结构,我们需要一种能够遍历每个字段的方法,无论它嵌套在页面、块、列和行中有多深:

// 一个 PHP 生成器,逐个产出表单配置中的每一个字段
public function fieldIterator()
{
    foreach ($this->config['pages'] as $page) {
        foreach ($page['blocks'] as $block) {
            foreach ($block['columns'] as $column) {
                foreach ($column['items'] as $item) {
                    foreach ($item['rows'] as $row) {
                        foreach ($row['fields'] as $field) {
                            yield $field;
                        }
                    }
                }
            }
        }
    }
}

// 现在,针对表单的任何操作都变得轻而易举
public function hasDesignationField()
{
    foreach ($this->fieldIterator() as $field) {
        if ($field['name'] === 'designation_id') {
            return true;
        }
    }
    return false;
}

这与 Django 的 Model._meta.get_fields() 直接类比。框架给了你一种遍历结构的方式,于是突然间每一项操作——校验、搜索、导出、权限检查——都变成了对字段的一次简单循环。

动态选项:让 Schema 与数据库对话

静态 schema 有用但有局限。真正的威力来自于 schema 能够引用在运行时才被解析的数据——就像 Django admin 中表单字段上的 queryset 方法一样。

在我们的表单引擎中,字段可以声明 dynamicOptions,告诉渲染器在渲染时从数据库获取数据:

// The form engine resolves dynamic options at render time
private function processDynamicOptions(&$field, $constituent)
{
    $dynamicOptions = array_get($field, 'properties.dynamicOptions');

    if ($dynamicOptions == 'designations') {
        // Fetch all designations from the database
        $designations = $this->getDesignationOptions();
        array_set($field, 'properties.options', $designations);
    }
    else if ($dynamicOptions == 'countries') {
        $countries = $this->getCountryOptions();
        array_set($field, 'properties.options', $countries);
    }
    else if ($dynamicOptions == 'previous_values') {
        // Show the donor's previous entries for this field
        $previous = $this->getPreviousValues($constituent, $field);
        array_set($field, 'properties.options', $previous);
    }
}

一个 JSON 中的单个字段定义可以表达“我需要一个 designation 下拉框”,而引擎会处理数据库查询、格式化和注入。表单作者永远不必编写 SQL。渲染代码永远不会改变。这与让 Django admin 的 ForeignKey 字段自动显示相关对象下拉框的原理相同——schema 知道这个关系,框架便会解析它。

转换管线:输入 Schema,输出 UI

表单引擎的核心是 transformConfig 方法。它接收一份存储的 JSON 配置,遍历每一个节点,在前端渲染之前用运行时数据对其加以丰富:

public function transformConfig($config, $formId, $constituent)
{
    foreach ($config['pages'] as &$page) {
        $page = $this->processRules($page);
        $page['visible'] = true;

        foreach ($page['blocks'] as &$block) {
            foreach ($block['columns'] as &$column) {
                foreach ($column['items'] as &$item) {
                    $item['visible'] = true;
                    foreach ($item['rows'] as &$row) {
                        foreach ($row['fields'] as &$field) {
                            $field['visible'] = true;
                            $field = $this->processRules($field);
                            $this->processDynamicOptions(
                                $field, $constituent, $formId
                            );
                        }
                    }
                }
            }
        }
    }
    return $config;
}

这与 Django admin 渲染 ModelForm 时所做的事一模一样:遍历模型的字段,解析外键,应用权限,注入控件属性,然后生成 HTML。我们只是用 JSON 而非 Python 类,用 Vue 组件而非 Django 模板来完成它。

规则引擎:无需代码的条件逻辑

Django admin 允许你定义 readonly_fieldsexclude 以及自定义的 has_change_permission 方法,用来控制字段的可见性和可编辑性。我们的表单引擎在此基础上更进一步,用一套嵌入在 schema 中的声明式规则系统来实现:

{
  "name": "state",
  "label": "State/Province",
  "type": "dropdown",
  "rules": [
    {
      "triggers": [
        {"field": "country", "event": "change"}
      ],
      "conditions": [
        {"field": "country", "value": "US"}
      ],
      "actions": [
        {"action": "show"},
        {"action": "setOptions", "value": "us_states"}
      ]
    }
  ]
}

当"country"字段变为"US"时,州下拉框出现并加载美国的各州。当它变为"CA"时,则加载加拿大的各省份。所有这些都在 schema 中声明——没有 JavaScript 事件处理器,也没有条件模板逻辑。前端规则引擎读取配置并作出响应。

这是将"配置优先于代码"理念推向其逻辑终点的做法。一位非开发人员可以打开表单构建器,添加一条条件规则并部署它——就像一位 Django admin 用户可以在不碰 Python 的情况下配置筛选器和展示字段一样。

表单版本控制:Schema 拥有历史记录

由于表单是以 JSON 配置的形式存储在数据库中,版本管理变得轻而易举。每次编辑都会创建一条新的 FormVersion 记录。你可以发布某个版本、回滚到之前的版本,或者保留一个只有内部用户能看到的草稿版本。这与 Wagtail 的页面修订系统以及 Django 自身用于记录后台变更的 LogEntry 模型如出一辙。

更重要的是,表单提交会与渲染它的表单版本关联起来。如果一个表单在周二和周三之间发生了变化,你能准确地知道每一条提交对应的是哪个版本。这份 schema 不只是渲染指令——它更是一条审计轨迹。

为什么 Django Admin 思维方式的意义远不止于后台管理面板

这篇文章的重点并不是「一切都用 Django admin」。重点在于 Django admin 体现了一种业界其余部分几乎尚未探索的设计哲学

  1. 模式优先开发——严谨地定义你的数据模型,让 UI 成为模式的一个函数。而不是反过来。
  2. 内省优于生成——不要从模式生成代码(脚手架)。相反,在运行时读取模式并动态渲染。生成的代码是一种维护负担。内省则始终是最新的。
  3. 配置优于代码——当你能够把行为表达为数据(一个字段名列表、一组规则、一份 JSON 配置)时,你就可以改变行为,而无需部署、无需 PR、完全不用碰代码。
  4. 渐进式定制——从合理的默认值开始(Django admin 在这方面做得极为出色),然后只让开发者覆盖那些需要有所不同的部分。你永远不应该为了改变一个控件而重写整个系统。

这些想法并不新颖。它们甚至算不上特别巧妙。但在 Django 生态之外,它们被严重地低估和忽视了

那个应让其他框架汗颜的对比

以下是在不同框架中搭建一个基础管理界面所需的成本:

  • Django:注册模型。完成。3 行代码。你就得到了列表视图、详情视图、搜索、过滤器、内联编辑、权限以及审计日志。
  • Rails:安装 ActiveAdmin 或 RailsAdmin gem。配置资源。与 DSL 搏斗。祈祷它支持你的用例。
  • Laravel:安装 Nova(付费)或 Filament。定义资源。编写表单 schema。配置表格列。它能用,但你写的是 Django 早已从你的模型中知道的 schema。
  • Express/Next.js:没有内置管理后台。自己构建,或者用一个无头 CMS。预留两个 sprint。

Django admin 不只是一个功能。它证明了一个框架可以对你的数据模型理解得足够深入,从而为你构建 UI。其他每个框架的管理方案都在试图外挂上 Django 早已刻在 DNA 里的东西。

我希望更多开发者去借鉴什么

如果你要从这篇文章里带走一样东西,那就让它是这个:在你动手写一个表单、一张表格、一个筛选面板或一个后台页面之前,先问问自己,数据模型里是否已经包含了足以生成它的全部信息。

答案几乎总是肯定的。你所需要的只是一个框架 —— 或者一种思维方式 —— 认真对待模型内省。Django 在 2005 年就证明了这一点。我们的生产表单引擎在一套完全不同的技术栈上再次证明了它。这个模式放之四海而皆准:

  • 严谨地定义你的数据结构
  • 构建一个读取该结构并生成 UI 的渲染器
  • 加上一个用于条件行为的规则层
  • 让配置而非代码来驱动定制

别再手写表单了。别再从零搭建后台面板了。别再把 UI 当作每个功能都必须手工打造的东西了。你的数据早已知道 UI 该长什么样。让它开口说话。

Django admin 不是一个图省事的便利功能。它是一种设计哲学,宣示你的数据模型才是唯一的真理来源——用于存储、用于校验,也用于界面本身。业界的其余部分至今仍在追赶。

联系我 →