- CMS
- 后端
【免费下载链接】django-cms
The easy-to-use and developer-friendly enterprise CMS powered by Django
django CMS 站点由三类构建块拼装而成:内容对象(content object)、插件(plugin)与Apphook。本文以 docs/explanation/composition.rst 为主线,系统讲解三者各自的职责边界、它们之间的组合规则,并给出开发者最常遇到的"插件还是 Apphook"决策指南,同时结合本仓库源码(cms/下的模型、插件基类、Apphook 池与 URL 解析器)印证底层实现。读完你将能根据"内容该放在哪里"这一核心问题,在站点架构层面做出正确的技术选型。
三大构建块:各自的职责
把三者放在一起对比,职责划分一目了然:
| 构建块 | 它是什么 | 它拥有什么 |
|---|---|---|
| 内容对象(Content object) | 持有可编辑内容、向编辑者暴露占位符(placeholder)的东西。页面(Page)是最常见也最强大的一种——它存在于页面树中、携带 URL、驱动菜单、承载权限。其他种类还包括别名(Alias)(通过 Alias 插件嵌入的可复用内容)以及由附加应用定义的内容对象(如博客文章、商品目录条目) | 它的占位符、它的翻译版本,以及(对于页面而言)它的 slug、模板和它在页面树中的位置 |
| 插件(Plugin) | 编辑者可以拖入占位符的可复用内容组件 | 编辑者通过其模型填入的数据,以及渲染它的模板 |
| Apphook | 把一个 Django 应用挂载到页面树上的标准方式。应用的 URL(以及它定义的内容对象)以锚定它的 CMS 页面路径为前缀 | 所挂载页面及其下层的全部 URL |
一句话概括三者关系:内容对象是可编辑内容的单元,插件是它内部的组合单元,Apphook 是其他内容加入页面树的方式。
从源码看,这个职责划分落实在模型层非常具体。Page模型(cms/models/pagemodel.py)持有的是site、parent、is_home、login_required、application_urls(即 Apphook 绑定)等长期身份字段;而标题、slug、模板、占位符、in_navigation、soft_root、meta_description等可编辑状态全部放在PageContent模型上(cms/models/contentmodels.py)。Page的 docstring 说得非常直白:"APageis an abstract entity. It does not have any content associated with it, nor does it provide any slugs to build a URL."——页面是抽象实体,内容与 URL 分别由PageContent与PageUrl承担。
这种"grouper(分组器)+content(内容)"的拆分是 django CMS 内容对象体系的底层模式,详见 内容对象与 grouper/content 模式:grouper 保存长期身份(一件事一行记录),content 保存按"语言 × 版本"切分的可编辑状态(一件事多行记录)。一个同时存在英文和德文的页面,在数据库里就是1 行Page加2 行PageContent(每种语言一行);装上djangocms-versioning后,每种语言还会按草稿/已发布/未发布/归档版本继续膨胀出更多行。
三者如何拼装在一起
只用文字分别描述三者很容易混淆,把它们放进一张组合图里就清楚了:
页面树 │ └── 页面 (内容对象:拥有占位符,存在于树中) │ ├── 占位符 (由页面模板声明) │ ├── 插件 │ │ └── 插件(嵌套,若父插件允许) │ └── 插件 │ └── Apphook (可选——在此 URL 挂载一个 Django 应用) │ └── 应用定义的内容对象 │ (例如博客文章、商品、投票) └── 占位符 └── 插件 页面树之外: 别名(Alias) (通过 Alias 插件嵌入的可复用内容对象) └── 占位符 └── 插件支配这张图的几条规则
- 内容对象拥有占位符,占位符容纳插件,插件之间可以嵌套组合。这条规则对所有内容对象一视同仁——页面、别名、应用自定义对象都一样。
- 页面树是页面的 URL 来源,让页面出现在菜单中,并承载按页面的权限模型。非页面内容对象要获得 URL,要么通过 Apphook 挂到树上,要么自己实现
get_absolute_url()。 - 插件只活在占位符里,从不单独存在。即使同一个插件出现在许多内容对象上,每一次出现都是一个独立的插件实例,拥有各自的配置。
- 父插件声明
allow_children = True后,插件可以包含其他插件。行、列、手风琴这类复合布局就是这样搭出来的。源码层面,cms/plugin_base.py 中allow_children默认False,开启后插件模板可通过instance.child_plugin_instances拿到全部子插件实例,再用{% render_plugin %}模板标签逐个渲染;child_plugin_instances在 cms/models/pluginmodel.py 定义,子插件被预取并可直接使用。相关的child_classes、parent_classes、require_parent属性则进一步约束"谁可以嵌进谁"。 - 没有页面可挂的 Apphook 毫无意义。它不是一项设置,而是在 CMS 页面的高级设置(Advanced settings)中建立的绑定:页面提供 URL 前缀,应用提供前缀之下的一切。
Apphook 的"绑定"在模型上就是Page的application_urls字段(cms/models/pagemodel.py),配合application_namespace记录实例命名空间。运行时,cms/appresolver.py 的_get_app_patterns()会扫描所有设置了 Apphook 的页面,把应用 urlconf 里的每个 pattern 递归改写(recurse_patterns)成"页面路径 + 原 pattern"的前缀形式,再构建成AppRegexURLResolver并入 CMS 的 URLconf——这正是"像在urls.py里 include 一个 URLconf,只是基础路径由编辑者通过页面树决定"的实现机制。
插件还是 Apphook?决策指南
换个角度看,问题实质是:这份内容到底住在哪里?如果它能塞进别人页面上的某个现有占位符里,它就是插件;如果它是自己的一类事物——拥有自己的列表视图、详情视图和 URL——它就是一个应用,通过 Apphook 挂载。
| 你的诉求是… | 选它 | 为什么 |
|---|---|---|
| 一个可复用的内容组件,编辑者能拖进任意占位符 | 插件 | 插件是占位符内部的组合单元 |
| 一个完整的子应用(博客、商品目录、投票、搜索),带列表和详情视图 | Apphook | Apphook 拥有 URL 前缀,并自带内容对象 |
| 一个完全由编辑者拼装内容的页面 | 页面 + 插件 | 页面内容对象的默认流程 |
| 一个主体由 Django 视图驱动的页面(列表、详情、搜索结果) | 页面 + Apphook | 页面提供 URL,应用提供视图和(如果有)自己的内容对象 |
| 编辑者要挑选哪些记录显示在页面内嵌的列表里 | 插件(模型引用你的记录) | 组件住在页面上,只有数据住在你应用的模型里 |
| 编辑者要能通过移动页面把子站点挪到不同 URL | Apphook | 视图和内容对象是你的,URL 属于页面 |
首页放"即将到来的活动"预告,同时/events/有完整活动子站 | 两者都要——预告用插件,子站用 Apphook | 常见组合:插件渲染小摘要,Apphook 拥有/events/及其下层 |
从源码看插件与 Apphook 的边界
插件侧的边界在 cms/plugin_base.py 一目了然:CMSPluginBase继承自 Django 的admin.ModelAdmin,因此exclude、fields、fieldsets、form、inlines、readonly_fields等 ModelAdmin 选项对插件同样有效;而list_display、list_filter、search_fields、actions、ordering等仅服务于 changelist 的选项在 CMS 插件中无效。插件render()方法(默认只注入instance与placeholder到上下文)加上render_template即构成完整的"模型-视图-模板"闭环。
Apphook 侧的边界在 cms/app_base.py:CMSApp基类通过_urls(urlconf 列表)、_menus(菜单类列表)、name(必填的人类可读名称)、app_name(Django namespace)、app_config(可选配置模型)等属性声明一个可挂载应用。注册由 cms/apphook_pool.py 的discover_apps()完成——默认autodiscover_modules('cms_apps')自动发现各应用的cms_apps.py,也可通过CMS_APPHOOKS设置显式指定类路径。未继承CMSApp的类会在注册时被ImproperlyConfigured拒绝。
当边界变得模糊:几个易踩坑的真实场景
以下场景常让人困惑,但它们都没有破坏上述模型,只是看起来像边界情况。
插件需要自己的详情 URL
"产品卡片"插件要链接到/products/42/,它依然是插件(它活在占位符里),但详情 URL 必须来自某个地方。惯用答案是:把详情视图放进挂载于 CMS 页面的 Apphook,这样 URL 由编辑者控制。如果改在项目根urls.py里接视图,URL 也能工作,但被写死在代码里,编辑者无法控制。
一页上有许多"同一个"插件
每一个都是独立的插件实例,各自携带配置。它们共享同一个模型和模板,但不共享数据——这正是插件作为"占位符内组合单元"的体现。
一个页面同时有占位符和 Apphook
页面自身 URL(/events/)正常渲染它的占位符;URL之下(/events/2026-summit/)则交给 Apphook。Apphook 页面下的子 CMS 页面无法可靠访问——因为 Apphook 拥有那段 URL 空间:请求可能被解析到应用而不是子页面,子页面甚至完全不可达。这一点在 cms/appresolver.py 的applications_page_check()中也有印证:应用解析器对路径的优先级高于普通 CMS 页面。这与 Apphook 文档 中"Apphook 会吞掉页面之下全部 URL"的警告一致。
同一个 Django 应用挂在多个页面上
每个挂载点是独立的。如果编辑者希望每个挂载点配置不同(不同分类、不同 feed),应用需要提供Apphook 配置(apphook configuration)机制。配置体系涉及三个容易混淆的概念:Apphook 类(开发者定义,连接页面与应用 URLconf)、Apphook 配置类(开发者定义,描述"编辑者能选什么")、Apphook 配置实例(编辑者创建,为某个挂载点选定具体配置数据)。源码中CMSApp的get_configs()、get_config(namespace)、get_config_add_url()就是配置化 Apphook 必须实现的三个方法(cms/app_base.py),而__new__中通过app_config.cmsapp强制"一个 Apphook 配置类只能绑定一个 Apphook"。
不复制内容地跨页面复用
这正是**别名(Alias)**的用途:一个活在页面树之外的内容对象,通过 Alias 插件嵌入到占位符中。页脚、促销横幅、"当前营业时间"条都是典型用例。别名的数据模型同样遵循 grouper/content 拆分——Alias是 grouper,AliasContent是 content——因此也能像页面一样参与翻译与版本化。
组合模型与发布、菜单的联动
理解组合关系后,两条联动规则值得单独强调:
- 发布状态是沿内容对象传播的。Apphook 附着在页面上,因此继承页面的发布状态:页面未发布时 Apphook 不对外服务,取消发布页面也会让 Apphook 下线;同时路径上的父页面也必须已发布。核心 CMS 本身没有独立的"发布"动作——没有版本化包时"编辑即发布"——装上
djangocms-versioning后每个内容行才获得草稿/已发布/未发布/归档四种状态,而PageContent.objects默认管理器此时只返回每种语言下已发布的那一行,admin_manager则是返回全部行的逃生门(详见 发布模型 与 内容对象)。未发布的页面不仅隐藏其插件,也会把它的 Apphook 一起带下线。 - 菜单由页面、Apphook 与自定义菜单代码共同产出。页面通过
cms.cms_menus.CMSMenu生成器进入菜单节点列表,NavExtender把 Apphook 应用的菜单并进来,SoftRootCutter负责按软根(soft root)裁掉多余的深层菜单项(详见 菜单系统工作原理)。由于 Apphook 挂载的页面拥有真实 URL,其应用页面可以正常参与导航,这是"在urls.py里直接 include"做不到的。
下一步阅读
- 插件详解——插件到底是什么、由哪些文件组成、插件模型该如何设计,以及
djangocms-frontend提供的模板组件与自定义组件两种轻量路径。 - Apphook 详解——Apphook 是什么、它如何改变 URL 处理,以及 Apphook 配置(configuration)机制。
- Apphook 实战指南——Apphook 挂载与配置的完整操作步骤。
- 内容对象与 grouper/content 模式——每个内容对象遵循的底层拆分模式。
- 发布模型——发布状态如何作用于页面上每个内容对象,以及版本化包的接入契约。
- 菜单系统工作原理——页面、Apphook 与自定义菜单代码如何组合成编辑者看到的导航。
- CMS
- 后端
【免费下载链接】django-cms
The easy-to-use and developer-friendly enterprise CMS powered by Django
相关推荐
django CMS多站点管理:共享内容如何独立部署多个品牌站点
django CMS多站点管理:共享内容如何独立部署多个品牌站点 在运营矩阵式品牌、集团官网或地区分站时,django CMS 的多站点(multi site)
CMS后端django-cms 3.0.3 升级要点:Alias 插件、上下文菜单扩展 API 与 Apphook 权限继承
django cms 3.0.3 升级要点:Alias 插件、上下文菜单扩展 API 与 Apphook 权限继承 本篇升级指南围绕 django cms 3.
CMS后端ArchiveBox Chrome 插件整合设计解析:一个插件一个 ArchiveResult 的插件架构
ArchiveBox Chrome 插件整合设计解析:一个插件一个 ArchiveResult 的插件架构 本指南围绕 ArchiveBox 仓库中的设计文档
后端数据工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考