自定义启动画面 HTML 生成器

:information_source: 摘要 一个 Discourse 插件,允许使用管理员定义的 HTML 和 CSS 来自定义启动屏幕(Splash Screen)。
:hammer_and_wrench: 仓库链接 https://github.com/VaperinaDEV/custom-splash-html-builder
:heart: 觉得有用? > ./support --coffee
:open_book: 安装指南 如何在 Discourse 中安装插件

你好 :waving_hand:

我开发了一个小型 Discourse 插件,允许使用管理员定义的 HTML 和 CSS 来自定义启动屏幕,而无需维护 Discourse 核心启动模板的修改副本。

创建这个插件的原始动机实际上是移动端性能

我想创建一个更复杂的动画启动屏幕,但我发现当前 Discourse 核心启动实现所支持的 SVG 动画在移动设备上可能会产生意想不到的问题。

在桌面上,动画可能看起来非常流畅,但在移动设备上,它可能会明显卡顿、掉帧、延迟,甚至在动画过程中看起来像是停止了。

在尝试了不同的方法后,我发现将动画从 SVG 本身转移到周围的 HTML 元素(例如 <div>)上,产生了非常显著的效果

通过不再连续动画化 SVG 内容,而是让 SVG 保持静态,同时由浏览器使用 CSS 变换来动画化包含它的 HTML 层。

这为浏览器提供了更好的机会,利用设备的图形硬件将动画作为合成操作来处理。

结果是移动设备上的动画变得流畅得多,不再出现我在基于 SVG 的方法中看到的卡顿和冻结现象。

这就是创建该插件的主要原因。

之前(动画 SVG:动画出现延迟和停止)

之后(动画 HTML:流畅的动画)


动画化 SVG 的问题

原始的启动实现对于简单的 Logo 或相对轻量级的动画来说是完全没问题的。

然而,一旦动画变得更加复杂,SVG 渲染可能会变得非常昂贵。

例如,直接应用于 SVG 或其内部元素的动画可能需要浏览器在动画过程中反复处理或重绘 SVG 的部分区域。

在移动设备上,这种情况可能尤为明显。

在测试过程中,我观察到动画出现以下情况:

  • 明显卡顿
  • 暂时冻结
  • 看起来停止了
  • 表现远不如桌面端

有趣的是,相同的视觉动画可能会因为实际被动画化的对象不同而表现出截然不同的行为。


将动画移至 HTML 层

效果更好的方法是保持 SVG 本身静态,并将其放入普通的 HTML 元素中。

例如:

<div class="logo-layer">
  <svg viewBox="0 0 500 500">
    ...
  </svg>
</div>

不是动画化 SVG,而是将动画应用于容器:

#d-splash .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_html
  • splash_custom_css

编辑器只是管理它们的一个更便捷的界面。

这也意味着,每当需要更改启动动画时,插件都不需要修改插件源文件。


示例

自定义启动可以包含多个独立的层:

<div class="ring-layer">
  <svg viewBox="0 0 500 500">
    ...
  </svg>
</div>

<div class="logo-layer">
  <svg viewBox="0 0 500 500">
    ...
  </svg>
</div>

并且每层都可以有自己的动画:

#d-splash .ring-layer {
  animation: rotate 2.2s linear infinite;
  will-change: transform;
}

#d-splash .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

例如:

#d-splash .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 本身,从而在移动端获得更好的性能。

8 个赞

你好 :waving_hand:

我已将 data-color-scheme 添加到 #d-splash 部分,因此你可以轻松设置浅色和深色模式的启动页徽标。DEV: Implement dynamic color scheme for splash section · VaperinaDEV/custom-splash-html-builder@ba6641b · GitHub

<%- splash_forced_scheme = (dark_color_scheme? || forced_dark_mode?) ? "dark" : (forced_light_mode? ? "light" : nil) %>

<section id="d-splash"<%= " data-color-scheme=\"#{splash_forced_scheme}\"".html_safe if splash_forced_scheme %>>

因此,当明确强制使用浅色或深色模式时,#d-splash 会获得 data-color-scheme="dark" / "light" 属性;仅当两种模式都启用且由操作系统决定时,该属性才会被省略。

修复方法是让该属性在自定义 CSS 中优先于媒体查询生效,并且仅在属性不存在时才回退到 prefers-color-scheme

示例:

自定义 HTML

<!-- LIGHT MODE -->
<div class="custom-splash-light">
  <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>

<!-- DARK MODE -->
<div class="custom-splash-dark">
  <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>

自定义 CSS

#d-splash .custom-splash-dark {
  display: none;
}

/* 由操作系统决定——仅在没有强制模式时 */
@media (prefers-color-scheme: dark) {
  #d-splash:not([data-color-scheme]) .custom-splash-light {
    display: none;
  }
  #d-splash:not([data-color-scheme]) .custom-splash-dark {
    display: block;
  }
}

/* 强制模式始终优先,无论操作系统设置如何 */
#d-splash[data-color-scheme="light"] .custom-splash-dark {
  display: none;
}
#d-splash[data-color-scheme="dark"] .custom-splash-light {
  display: none;
}
#d-splash[data-color-scheme="dark"] .custom-splash-dark {
  display: block;
}

由于一旦属性存在,:not([data-color-scheme]) 就不再匹配,因此这两组规则之间不会发生特异性冲突,强制模式始终优先。

2 个赞

太酷了!这正是我多年前就建议/寻找的功能,用于在网络连接缓慢时控制品牌/图像,让用户保持方向和定位。

乍一看,这比我想象的还要强大。期待有机会试用一下。干得漂亮!

2 个赞