| 摘要 | 允许使用管理员定义的 HTML 和 CSS 自定义启动画面的 Discourse 插件。 | |
| 代码仓库链接 | https://github.com/VaperinaDEV/custom-splash-html-builder | |
| 安装指南 | 如何在 Discourse 中安装插件 |
你好 ![]()
我创建了一个小型 Discourse 插件,允许使用管理员定义的 HTML 和 CSS 自定义启动画面,而无需维护 Discourse 核心启动模板的修改副本。
创建此插件的原始动机实际上是移动设备性能。
我想创建一个更复杂的动画启动画面,但我发现当前 Discourse 核心启动实现支持的 SVG 动画在移动设备上可能会变得相当棘手。
在桌面上,动画可能看起来非常流畅,而在移动设备上,它可能会变得明显卡顿、掉帧、滞后,甚至在动画过程中停止。
在尝试了不同的方法后,我发现将动画从 SVG 本身移动到周围的 HTML 元素(例如 <div>)上,产生了非常显著的效果。
SVG 内容保持静态,而浏览器使用 CSS 变换动画化包含它的 HTML 层。
这使浏览器有机会更好地将动画处理为使用设备图形硬件的合成操作。
结果是移动设备上的动画更加流畅,没有我在使用基于 SVG 的方法时看到的卡顿和冻结。
这就是创建此插件的主要原因。
之前(动画 SVG:动画滞后并停止)
之后(动画 HTML:流畅动画)
动画 SVG 的问题
原始启动实现对于简单徽标或相对轻量级的动画来说完全没问题。
然而,一旦动画变得复杂,SVG 渲染就会变得昂贵。
例如,直接应用于 SVG 或其内部元素的动画可能需要浏览器在动画过程中重复处理或重绘 SVG 的部分内容。
在移动设备上,这可能会变得特别明显。
在测试期间,我看到了动画出现以下情况的情况:
- 变得明显卡顿
- 暂时冻结
- 似乎停止
- 表现明显差于桌面端
有趣的是,相同的视觉动画根据实际动画化的内容可能会有非常不同的表现。
将动画移动到 HTML 层
效果更好的方法是保持 SVG 本身静态,并将其放入普通 HTML 元素中。
例如:
<div class="logo-layer">
<svg viewBox="0 0 500 500">
...
</svg>
</div>
不动画化 SVG,而是将动画应用于容器:
.logo-layer {
animation: pulse 1.8s ease-in-out infinite;
will-change: transform;
}
@keyframes pulse {
0%,
100% {
transform: scale(0.8);
}
50% {
transform: scale(0.85);
}
}
SVG 本身不发生变化。
因此,浏览器可以更高效地处理 HTML 层的变换,并在支持的情况下将其提升为由图形硬件处理的合成层。
这在移动设备上产生了明显更流畅的效果。
因此,重要的区别是:
核心方法:
SVG
└── SVG 动画
└── SVG 内容被动画化
对比:
自定义方法:
HTML 层
└── SVG
└── HTML 层上的 CSS 变换
└── 对合成器友好的动画
这并不能保证每个动画都会进行 GPU 加速。浏览器最终决定如何合成动画,但在我的测试中,差异非常明显。
为什么创建 Custom Splash HTML Builder
一旦这种方法奏效,我还需要一种实际构建围绕它的启动画面的方法。
标准启动模板无法提供足够的灵活性来实现这种类型的实现。
对于更复杂的动画,我可能需要:
- 多个 SVG 层
- 多个 HTML 容器
- 独立动画化的元素
- 自定义 CSS 关键帧
- 不同的动画计时
- 自定义定位
- 主题感知的颜色
- 与默认启动完全不同的标记
因此,我没有创建另一个硬编码的启动实现,而是决定通过两个站点设置暴露视觉部分。
插件添加了:
splash_custom_html
在启动画面中渲染的 HTML/SVG 标记。
splash_custom_css
自定义启动使用的 CSS,包括动画、关键帧、定位和响应式行为。
这使得启动画面实际上可自定义,而无需在每次更改动画时修改插件源代码。
内置管理员编辑器
插件还提供了一个小型内置管理员编辑器,用于管理自定义启动画面。
它在 Discourse 管理员界面中添加了一个专用的 Splash HTML Builder 部分,具有用于以下内容的单独编辑器:
- 自定义 HTML
- 自定义 CSS
更改可以直接从管理员界面保存,而无需手动编辑相应的站点设置。
底层设置仍然是:
splash_custom_htmlsplash_custom_css
编辑器只是管理它们的更便捷界面。
这也意味着,每当需要更改启动动画时,插件都不需要修改插件源文件。
示例
自定义启动画面可以包含多个独立层:
<div class="splash-logo-container">
<div class="ring-layer">
<svg viewBox="0 0 500 500">
...
</svg>
</div>
<div class="logo-layer">
<svg viewBox="0 0 500 500">
...
</svg>
</div>
</div>
每个层可以有自己的动画:
.ring-layer {
animation: rotate 2.2s linear infinite;
will-change: transform;
}
.logo-layer {
animation: pulse 1.8s ease-in-out infinite;
will-change: transform;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%,
100% {
transform: scale(0.8);
}
50% {
transform: scale(0.85);
}
}
SVG 保持静态,而周围的 HTML 层被动画化。
这使得在保持昂贵的动画工作远离 SVG 本身的同时,创建相当复杂的启动动画成为可能。
为什么不简单地覆盖核心启动模板?
另一个重要目标是避免维护 Discourse 核心启动模板的副本。
一种直接的方法是覆盖:
app/views/common/_discourse_splash.html.erb
并将当前的 Discourse 实现复制到插件中。
问题在于这会带来维护负担。
如果 Discourse 在未来的版本中更改其启动实现,插件仍将包含旧版本。
这可能会导致:
- 缺少新的核心更改
- 缺少性能改进
- Discourse 更新后行为中断
- 每次更新后必须手动比较插件模板和核心
我想完全避免这种情况。
核心回退
因此,插件支持带有核心回退的自定义启动画面。
配置了自定义 HTML
如果:
SiteSetting.splash_custom_html.present?
则插件渲染自定义启动画面。
自定义 HTML 为空
如果未配置自定义启动画面,插件将回退到当前 Discourse 核心启动模板。
插件从正在运行的 Discourse 安装中定位实际的核心文件:
Rails.root/app/views/common/_discourse_splash.html.erb
并渲染该实现。
概念上:
core_splash_path = Rails.root.join("app", "views", "common", "_discourse_splash.html.erb")
if File.exist?(core_splash_path)
render inline: File.read(core_splash_path), type: :erb
end
这意味着插件不携带核心启动模板的第二份副本。
性能考虑
插件并不声称每个 CSS 动画都会神奇地变成 GPU 加速。
浏览器仍然决定如何渲染和合成单个动画。
目标反而是为硬件加速合成提供更有利的结构:
- 保持 SVG 内容静态
- 隔离独立动画化的元素
- 动画化 HTML 层
- 优先使用
transform进行移动/缩放/旋转 - 避免不必要昂贵的重绘操作
- 在适当的地方使用
will-change
例如:
.ring-layer {
will-change: transform;
animation: rotate 2.2s linear infinite;
}
这种方法在我的用例中特别有效,并消除了我在原始 SVG 动画中看到的移动设备卡顿。
启用或禁用自定义启动画面
插件还提供了一个 custom_splash_html_builder_enabled 站点设置。
禁用时,无论是否配置了自定义 HTML 或 CSS,都使用标准 Discourse 启动画面。
这提供了一个额外的安全开关,用于临时禁用自定义启动画面而无需删除保存的 HTML/CSS。
仅当同时满足以下条件时,才渲染自定义启动画面:
custom_splash_html_builder_enabled = true
splash_custom_html 不为空
否则,使用当前 Discourse 核心启动画面。
最重要的是,它提供了一种通过动画化围绕静态 SVG 内容的 HTML 层而不是直接动画化 SVG 本身来构建在移动设备上性能更好的自定义动画启动画面的方法。