Discourse では、投稿(およびその他の場所)に画像をアップロードできます。有时候、这些图片太大无法直接显示,因此 Discourse 会生成一个缩放版本并添加到帖子中。当您点击该缩放图片时,会弹出一个包含完整尺寸图片的精美覆盖层——这通常被称为“灯箱”(lightbox)。
Discourse 目前使用名为 Magnific Popup 的库来处理灯箱行为。本主题旨在通过从 Magnific Popup 迁移到更符合当今用户和开发者期望的方案,对 Discourse 的灯箱功能进行全面升级。
为什么要做
Magnific 是一个非常棒的库,其 项目页面 上的星标数量充分说明了它多年来为用户和开发者解决了许多问题。
它在易用性和功能提供方面也领先于时代。这一点显而易见:您今天看到的大部分功能早在 2014 年发布的 1.0 版本中就已经存在了。
那么,这对我们意味着什么?我长话短说。Magnific Popup 是为一个完全不同的世界设计的,当时的跨浏览器兼容性远不如今天。这意味着四点:
- 它依赖 jQuery
- 它包含为古老浏览器编写的代码
- 自那时以来,访问网络的主流设备已发生巨大变化
- 它的设计优先考虑的不是像 Discourse 这样的单页应用(SPA),而是静态页面。这给单页应用带来了特定的性能问题,我们稍后会详细讨论。
鉴于当前 Web 标准的发展以及如何使用原生 JavaScript 实现各种神奇效果,像 Discourse 这样的大型项目正在摆脱对 jQuery 的依赖是可以理解的。请注意,关于 jQuery 的讨论在此处属于离题内容,这里主要提供背景信息。
所以,这就是为什么要做这件事……减少 jQuery 依赖、减少古老浏览器代码、提升整体性能,并为如今截然不同的主流设备提供更好的支持。
怎么做
市面上不乏各种解决方案,也有关于“不要重复造轮子”的讨论,但……让我们暂且不谈这些,保持简洁。
或许解决这一难点最简短的说法是……无论现成的方案多么契合,都不如量身定制的方案来得完美,我们就此打住。
这里有许多需要考虑的因素,无论是功能过剩(例如涵盖图片、iframe、视频等多种用例的库),还是许可证方面的不明确。
了解 Discourse 需要支持的具体用例,使我们能够避免不必要的代码,从而在不过度增加开销的情况下添加更多针对性功能。
因此,我们现在有了一个相当不错的基础。需求如下:
- 不使用 jQuery
- 目前仅专注于图片
- 支持提升用户体验的功能,例如在移动设备上的滑动操作
- 与 Discourse 集成,以便利用当前的主题/插件系统进行进一步的定制和改进。
做什么
Discourse 是一个 Ember 应用,因此新的灯箱必须是一个 Ember 组件,用于处理标记和数据。此外,由于用户期望可以在应用的任何位置设置灯箱,提供一个灯箱服务是非常合理的。这将允许开发者:
- 将服务注入到任何组件中,并将服务的设置/清理方法与组件生命周期关联起来
- 在应用的任何位置查找服务并调用其设置/清理方法
此外,作为提升用户体验的改进,我们可以进行一些抽象,创建一些可在主题中使用的工具函数。稍后会有更多介绍。
由于目标之一是允许主题/插件扩展功能,灯箱服务将通过 appEvents 通信状态变化。它将发出以下事件:
lightbox:openedlightbox:item-will-changelightbox:item-did-changelightbox:closed
每个事件触发时都会携带开发者通常预期的数据类型。例如,lightbox:opened 事件将包含所有项目的列表以及灯箱打开时的当前项目。稍后会有更多介绍。
话已至此,让我们继续。
在过去几周里,我一直在开发一个引入 Discourse Lightbox 的草案 PR。
乍一看,它可能显得有点大,让我们分解一下。
测试依赖大量用户交互的功能颇具挑战性,因此灯箱测试套件包含了 63 个测试和 291 个断言。
在解决了测试规模的问题后,让我们快速比较一下它与 Magnific Popup 的大小(两者均为未压缩版本)
| Discourse Lightbox 代码行数 | Magnific Popup 代码行数 | 差异 | |
|---|---|---|---|
| JavaScript | 1197 | 1860 | (35%) |
| CSS | 813 | 351 | 131% |
| 模板 | 401 | 0 | - |
| 总计 | 2411 | 2211 | 9.1% |
因此,Discourse Lightbox 的代码量比 Magnific 多约 9%。当然,这只是故事的一部分,原因有二:
- 我们尚未计算 jQuery,Magnific 依赖它。jQuery 3.6 大约有 11k 行代码。
- Discourse Lightbox 比 Magnific 增加了更多功能。
让我们详细看看这些功能。
下面视频中出现的所有卡顿都与录制过程有关,而非用户实际体验。所有动画/过渡将以稳定的 60FPS 运行。
那么,不再多言,
基本布局
作为对比,以下是当前 Magnific 实现中的相同效果
关于上述视频的几个要点:
-
新灯箱将使用图片作为背景,而不是通用的半透明深色背景。图片本身自然会与其背景相得益彰。请注意,这不会增加任何网络开销。背景中使用的图片来自帖子正文,这意味着当用户打开灯箱时,该图片已经被缓存。
-
Discourse Lightbox 采用固定 UI。关闭按钮、标题和图片元数据不再附着在图片上,而是拥有各自预留的空间。这将有助于在导航不同尺寸的图片时减少跳动。
-
图片之间的导航可以通过灯箱中的箭头或键盘快捷键完成。→ 或 ↓ 为下一张,← 或 ↑ 为上一张。在 RTL 语言环境中,快捷键会相应翻转。
移动端的布局非常相似,只是不显示箭头,因为添加了用于导航的滑动手势。
在移动端向左滑动为下一张,向右滑动为上一张。在 RTL 语言环境中,滑动手势会相应翻转。
缩放
新灯箱有一个专用的缩放按钮,当查看的图片尺寸大于视口尺寸时会显示。点击缩放按钮将放大图片,再次点击将缩小图片。此外,现在有了用于放大/缩小的键盘快捷键 Z。最后,如果图片可缩放,点击它也会产生相同的效果。
以下是演示这三种方法的视频
请注意,虽然上述视频未演示,但当鼠标悬停在可缩放图片上时,光标会发生变化以反映这一点。当悬停在已缩放的图片上时,光标也会变为 zoom-out 图标。
新灯箱中的缩放方式有所不同。在桌面端,缩放的图片区域将跟随鼠标光标。
移动端没有悬停效果;它使用常规触摸滚动在缩放后的图片中移动。
旋转
新灯箱添加了一个专用的旋转按钮,可按 90 度增量旋转图片。旋转也有键盘快捷键,即 R 键。效果如下
旋转和缩放可以组合使用。
全屏模式
新灯箱添加了一个全屏按钮。该按钮将使浏览器窗口进入全屏模式。键盘快捷键是 M
它将跟踪其状态,并在退出全屏或关闭灯箱时恢复到常规模式。
下载
当前灯箱在图片下方添加了一个“下载”链接。新灯箱也做了同样的事情,但将其改为图标并添加到灯箱页脚中。它仍然遵循相同的权限设置。如果…
prevent_anons_from_downloading_images
已启用,且用户未登录,则不会显示下载图标。
新标签页
当前灯箱在图片下方添加了一个“原始”链接,在新标签页中打开。新灯箱也做了同样的事情,但将其作为图标添加到灯箱页眉中。它也将遵循与下载图标相同的权限设置。
图片标题
新灯箱主要聚焦于图片。图片标题默认截断为一行,但支持展开。示例如下
有一个键盘快捷键用于展开/折叠标题 T,且当图片处于缩放/旋转状态时,标题将不会显示。
轮播
新推出的功能是在图库中以轮播形式显示所有图片。布局将取决于设备屏幕;可以是水平或垂直的,效果如下。
有一个键盘快捷键用于切换轮播:A
在移动端的效果如下
在移动端向下滑动将切换轮播的开启/关闭。
关闭
Esc 在桌面端仍然像以前一样用于关闭灯箱。移动端现在增加了一个额外的滑动手势,您可以向上滑动以关闭灯箱。
无障碍性
除了基本的按钮标签外,新灯箱还在屏幕外添加了一个屏幕阅读器公告元素。当您在灯箱内导航到图片时,它会根据以下格式朗读其索引和标题。
image %{current} of %{total}: %{title}
这是一个简短的示例
新灯箱还通过 aria-hidden 移除了所有对屏幕阅读器无用的按钮。
请记住,无障碍性是一项持续的任务,这远未完成。总有改进的空间,但为了保持 v1 的简洁性,我在此止步。
这涵盖了新灯箱的所有功能。
让我们进入一些开发者术语。
事件监听器
当前灯箱为每个烹饪帖子中的独立灯箱图片添加点击事件监听器。这意味着包含 20 张图片的帖子将有 20 个用于灯箱的点击事件监听器。
新灯箱利用事件委托,仅向帖子本身添加一个事件监听器。
以下是匿名用户在隐身窗口中强制垃圾回收后,包含 20 张图片的帖子上当前灯箱的事件监听器计数
以下是同一帖子使用新灯箱的情况
此外,目前在灯箱内导航时会添加事件监听器,这些监听器最终会成为孤儿并干扰垃圾回收。以下是图表:
- 加载包含一个帖子(含 20 张图片)的主题页面。
- 打开灯箱并连续三次遍历所有 20 张图片。
- 关闭灯箱
- 强制浏览器垃圾回收
当前灯箱:
新灯箱:
至于灯箱本身的事件监听器,它们目前在 Magnific 中似乎没有被清理(请记住,它并非为单页应用而构建)。
关于上述测试的一个简短说明:它们非常基础,数字并非旨在“科学”,目标在于确定方向而非精确数值。
开发者说明
让我们谈谈如何使用新的 Discourse Lightbox 设置和清理灯箱。
设置和清理灯箱
开发者有两种选择。
-
通过以下方式将灯箱服务注入组件:
import { inject as service } from "@ember/service"; //... @service lightbox然后您可以调用
this.lightbox.setupLightboxes({ container: yourContainer // DOM 节点 selector: ".css-selector" // 您希望作为灯箱的元素的选择器字符串 })之后,当您想要清理时,只需调用
this.lightbox.cleanupLightboxes()就这样。
-
如果您不想注入灯箱服务,可以像这样导入
setupLightboxes和cleanupLightboxes:import { cleanupLightboxes, setupLightboxes, } from "discourse/lib/lightbox";其余部分与注入服务相同。这两个函数会为您查找服务。所以
setupLightboxes({ container: yourContainer // DOM 节点 selector: ".css-selector" // 您希望作为灯箱的元素的选择器字符串 }) //.... cleanupLightboxes()
请注意,无论是直接从服务调用还是通过工具函数调用,也接受节点列表以向后兼容,但不推荐这样做。
关于这一点还有一个说明:您也可以让非图片元素作为触发器来打开您设置的灯箱。例如
<div class="my-container">
<img class="my-selector" src="foo">
<img class="my-selector" src="bar">
....
<button>Open Lightbox</button>
</div>
我会像这样设置上述 div 的基本灯箱。
import {
cleanupLightboxes,
setupLightboxes,
} from "discourse/lib/lightbox";
//...
setupLightboxes({
container: document.querySelector(".my-container"),
selector: ".my-selector"
})
要让按钮打开灯箱,只需添加 data-lightbox-trigger 属性,如下所示
<button data-lightbox-trigger>Open Lightbox</button>
其余部分会自动处理。
最后,当您想要清理时,调用
cleanupLightboxes()
清理实际上并不是关键,因为灯箱服务会在应用中的 dom:clean 事件触发时(在路由转换时)自动清理。
监听灯箱事件
新灯箱将触发事件,正如我们之前讨论的那样。这些事件是:
lightbox:openedlightbox:item-will-changelightbox:item-did-changelightbox:closed
lightbox:opened
此事件在灯箱打开时触发,包含两个对象。
items:这是当前灯箱中所有图片的数组。每个对象都是一个对象。currentItem:这是灯箱打开时的当前项对象。
项对象如下所示:
{
"fullsizeURL": "https://d11a6trkgmumsb.cloudfront.net/original/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff.jpeg",
"smallURL": "https://d11a6trkgmumsb.cloudfront.net/optimized/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff_2_600x750.jpeg",
"downloadURL": "/uploads/short-url/56rKTvkvmL6c2C8OFQtMo3D8sIn.jpeg?dl=1",
"title": "Close-up Photo of a Black Microphone on Stand",
"fileDetails": "1200×1500 88.9 KB",
"dominantColor": "793C6D",
"aspectRatio": "400 / 500",
"index": 0,
"cssVars": "--dominant-color: #793C6D;--aspect-ratio: 400 / 500;--small-url: url(https://d11a6trkgmumsb.cloudfront.net/optimized/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff_2_600x750.jpeg);",
"isLoaded": true,
"hasLoadingError": false,
"width": 1200,
"height": 1500,
"canZoom": true
}
lightbox:item-will-change
此事件在灯箱中的当前项即将更改之前触发。它将包含 currentItem(即将更改的那一项)。
lightbox:item-did-change
此事件在灯箱中的项更改并加载完成后立即触发,并将 currentItem 作为参数。
lightbox:closed
此事件在灯箱关闭后立即触发,没有参数。
有了上述事件,一个理论上的主题组件可以轻松地为灯箱添加分析功能,如下所示
api.onAppEvent('lightbox:opened', ({items, currentItem}) => {
console.log({items});
console.log({currentItem});
// 您的分析代码在这里
});
或其他类似的想法。
CSS
新灯箱使用 BEM 命名约定作为 HTML 类。以下是您可以使用的选择器完整列表:
html.has-lightbox {
// 灯箱打开时 HTML 元素的 CSS
}
.d-lightbox {
&--is-visible {
// 主灯箱元素
}
&__content {
// 灯箱内部内容包装器
}
}
.d-lightbox {
&__content__header {
// 灯箱页眉
}
}
.d-lightbox {
&__content__body {
// 灯箱主体(包含主图片)
&__backdrop {
// 灯箱背景
}
&__main-image {
// 灯箱主图片
}
&__error-message {
// 灯箱错误消息
}
&__previous-button,
&__next-button {
// 主灯箱上一张/下一张按钮
}
}
}
.d-lightbox {
&__content__footer {
// 灯箱页脚
&__main-title {
// 灯箱图片标题
&__item-file-details {
// 灯箱文件详情,例如 "1000x582 183KB"
}
}
}
}
.d-lightbox {
&__content__carousel {
// 灯箱轮播容器
&__previous-button,
&__next-button {
// 灯箱轮播上一张/下一张按钮
}
}
}
.d-lightbox {
&__content__carousel {
&__carousel-items {
// 灯箱轮播项容器
&__item,
&__item--is-current {
// 灯箱轮播项
}
&__item--is-current {
// 灯箱轮播当前项
}
}
}
}
.d-lightbox {
&--is-vertical &__content__carousel {
// 灯箱轮播垂直样式
}
}
.d-lightbox {
&--is-horizontal &__content__carousel {
// 灯箱轮播水平样式
}
}
.d-lightbox {
.btn-flat {
// 所有灯箱按钮的样式
}
}
.d-lightbox {
&__content {
&__focus-trap,
&__screen-reader-announcer {
// 灯箱焦点陷阱和屏幕阅读器公告器。这些位于屏幕外
}
}
}
/* 状态样式 */
// 轮播
.d-lightbox {
&--has-carousel {
// 轮播打开时的灯箱样式
}
}
// 展开的标题
.d-lightbox {
&--has-expanded-title {
// 标题展开时的灯箱样式
}
}
// 缩放
.d-lightbox {
&--can-zoom {
// 图片可缩放时的灯箱样式
}
&--is-zoomed {
// 图片已缩放时的灯箱样式
}
}
// 旋转
.d-lightbox {
&--is-rotated {
// 图片已旋转时的灯箱样式
}
}
// 全屏
.d-lightbox {
&--is-fullscreen {
// 图片全屏时的灯箱样式
}
}
既然我们已经涵盖了做什么,现在终于可以进入…
何时实施
目前,PR 已准备好进行审查,这是首先需要完成的步骤。审查通过后,它将可供更新后的站点使用。PR 添加了一个新的临时站点设置,作为从 Magnific Popup 过渡到 Discourse Lightbox 的一部分。该设置的名称是:
enable_experimental_lightbox
如果该设置被禁用,PR 将没有任何效果,一切将继续像以前一样使用 Magnific Popup 运行。
当该设置开启时,Discourse Lightbox 将替换烹饪帖子、聊天消息和图片上传组件中的 Magnific Popup。
路线图
- PR 审查
- PR 合并
- 一般反馈窗口(1-2 周)
- 从核心中移除 Magnific Popup 并移除实验性站点设置的 PR。
- TBD:扩展目标,例如探索不同布局的主题组件(由于新灯箱使用 CSS Grid 进行布局,这应该很简单)
致谢
- 这项工作由 CDCK 慷慨赞助

- 向 Magnific Popup 的创作者 Dmytro Semenov 致以深深的敬意
,他创造了一个远远领先于时代的作品。 - 上述演示中使用的图片由 Irina Iriser @pexels 提供




