Discourse Lightbox - Magnific popupからの移行

Discourse では、投稿(およびその他の場所)に画像をアップロードできます。有时候、这些图片太大无法直接显示,因此 Discourse 会生成一个缩放版本并添加到帖子中。当您点击该缩放图片时,会弹出一个包含完整尺寸图片的精美覆盖层——这通常被称为“灯箱”(lightbox)。

Discourse 目前使用名为 Magnific Popup 的库来处理灯箱行为。本主题旨在通过从 Magnific Popup 迁移到更符合当今用户和开发者期望的方案,对 Discourse 的灯箱功能进行全面升级。

为什么要做

Magnific 是一个非常棒的库,其 项目页面 上的星标数量充分说明了它多年来为用户和开发者解决了许多问题。

它在易用性和功能提供方面也领先于时代。这一点显而易见:您今天看到的大部分功能早在 2014 年发布的 1.0 版本中就已经存在了。

那么,这对我们意味着什么?我长话短说。Magnific Popup 是为一个完全不同的世界设计的,当时的跨浏览器兼容性远不如今天。这意味着四点:

  1. 它依赖 jQuery
  2. 它包含为古老浏览器编写的代码
  3. 自那时以来,访问网络的主流设备已发生巨大变化
  4. 它的设计优先考虑的不是像 Discourse 这样的单页应用(SPA),而是静态页面。这给单页应用带来了特定的性能问题,我们稍后会详细讨论。

鉴于当前 Web 标准的发展以及如何使用原生 JavaScript 实现各种神奇效果,像 Discourse 这样的大型项目正在摆脱对 jQuery 的依赖是可以理解的。请注意,关于 jQuery 的讨论在此处属于离题内容,这里主要提供背景信息。

所以,这就是为什么要做这件事……减少 jQuery 依赖、减少古老浏览器代码、提升整体性能,并为如今截然不同的主流设备提供更好的支持。

怎么做

市面上不乏各种解决方案,也有关于“不要重复造轮子”的讨论,但……让我们暂且不谈这些,保持简洁。

或许解决这一难点最简短的说法是……无论现成的方案多么契合,都不如量身定制的方案来得完美,我们就此打住。

这里有许多需要考虑的因素,无论是功能过剩(例如涵盖图片、iframe、视频等多种用例的库),还是许可证方面的不明确。

了解 Discourse 需要支持的具体用例,使我们能够避免不必要的代码,从而在不过度增加开销的情况下添加更多针对性功能。

因此,我们现在有了一个相当不错的基础。需求如下:

  1. 不使用 jQuery
  2. 目前仅专注于图片
  3. 支持提升用户体验的功能,例如在移动设备上的滑动操作
  4. 与 Discourse 集成,以便利用当前的主题/插件系统进行进一步的定制和改进。

做什么

Discourse 是一个 Ember 应用,因此新的灯箱必须是一个 Ember 组件,用于处理标记和数据。此外,由于用户期望可以在应用的任何位置设置灯箱,提供一个灯箱服务是非常合理的。这将允许开发者:

  1. 将服务注入到任何组件中,并将服务的设置/清理方法与组件生命周期关联起来
  2. 在应用的任何位置查找服务并调用其设置/清理方法

此外,作为提升用户体验的改进,我们可以进行一些抽象,创建一些可在主题中使用的工具函数。稍后会有更多介绍。

由于目标之一是允许主题/插件扩展功能,灯箱服务将通过 appEvents 通信状态变化。它将发出以下事件:

  • lightbox:opened
  • lightbox:item-will-change
  • lightbox:item-did-change
  • lightbox: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%。当然,这只是故事的一部分,原因有二:

  1. 我们尚未计算 jQuery,Magnific 依赖它。jQuery 3.6 大约有 11k 行代码。
  2. Discourse Lightbox 比 Magnific 增加了更多功能。

让我们详细看看这些功能。

下面视频中出现的所有卡顿都与录制过程有关,而非用户实际体验。所有动画/过渡将以稳定的 60FPS 运行。

那么,不再多言,

基本布局

提议的 Discourse Lightbox 基本布局视频 | video

作为对比,以下是当前 Magnific 实现中的相同效果

当前 Discourse Lightbox 基本布局视频 | video

关于上述视频的几个要点:

  1. 新灯箱将使用图片作为背景,而不是通用的半透明深色背景。图片本身自然会与其背景相得益彰。请注意,这不会增加任何网络开销。背景中使用的图片来自帖子正文,这意味着当用户打开灯箱时,该图片已经被缓存。

  2. Discourse Lightbox 采用固定 UI。关闭按钮、标题和图片元数据不再附着在图片上,而是拥有各自预留的空间。这将有助于在导航不同尺寸的图片时减少跳动。

  3. 图片之间的导航可以通过灯箱中的箭头或键盘快捷键完成。 为下一张, 为上一张。在 RTL 语言环境中,快捷键会相应翻转。

移动端的布局非常相似,只是不显示箭头,因为添加了用于导航的滑动手势。

提议的 Discourse Lightbox 在移动端的布局视频 | video

在移动端向左滑动为下一张,向右滑动为上一张。在 RTL 语言环境中,滑动手势会相应翻转。

缩放

新灯箱有一个专用的缩放按钮,当查看的图片尺寸大于视口尺寸时会显示。点击缩放按钮将放大图片,再次点击将缩小图片。此外,现在有了用于放大/缩小的键盘快捷键 Z。最后,如果图片可缩放,点击它也会产生相同的效果。

以下是演示这三种方法的视频

演示新灯箱中三种缩放方法的视频 | video

请注意,虽然上述视频未演示,但当鼠标悬停在可缩放图片上时,光标会发生变化以反映这一点。当悬停在已缩放的图片上时,光标也会变为 zoom-out 图标。

新灯箱中的缩放方式有所不同。在桌面端,缩放的图片区域将跟随鼠标光标。

移动端没有悬停效果;它使用常规触摸滚动在缩放后的图片中移动。

旋转

新灯箱添加了一个专用的旋转按钮,可按 90 度增量旋转图片。旋转也有键盘快捷键,即 R 键。效果如下

旋转和缩放可以组合使用。

全屏模式

新灯箱添加了一个全屏按钮。该按钮将使浏览器窗口进入全屏模式。键盘快捷键是 M

它将跟踪其状态,并在退出全屏或关闭灯箱时恢复到常规模式。

下载

当前灯箱在图片下方添加了一个“下载”链接。新灯箱也做了同样的事情,但将其改为图标并添加到灯箱页脚中。它仍然遵循相同的权限设置。如果…

prevent_anons_from_downloading_images

已启用,且用户未登录,则不会显示下载图标。

新标签页

当前灯箱在图片下方添加了一个“原始”链接,在新标签页中打开。新灯箱也做了同样的事情,但将其作为图标添加到灯箱页眉中。它也将遵循与下载图标相同的权限设置。

图片标题

新灯箱主要聚焦于图片。图片标题默认截断为一行,但支持展开。示例如下

有一个键盘快捷键用于展开/折叠标题 T,且当图片处于缩放/旋转状态时,标题将不会显示。

轮播

新推出的功能是在图库中以轮播形式显示所有图片。布局将取决于设备屏幕;可以是水平或垂直的,效果如下。

有一个键盘快捷键用于切换轮播:A

在移动端的效果如下

在移动端向下滑动将切换轮播的开启/关闭。

关闭

Esc 在桌面端仍然像以前一样用于关闭灯箱。移动端现在增加了一个额外的滑动手势,您可以向上滑动以关闭灯箱。

无障碍性

除了基本的按钮标签外,新灯箱还在屏幕外添加了一个屏幕阅读器公告元素。当您在灯箱内导航到图片时,它会根据以下格式朗读其索引和标题。

image %{current} of %{total}: %{title}

这是一个简短的示例

新灯箱还通过 aria-hidden 移除了所有对屏幕阅读器无用的按钮。

请记住,无障碍性是一项持续的任务,这远未完成。总有改进的空间,但为了保持 v1 的简洁性,我在此止步。

这涵盖了新灯箱的所有功能。

让我们进入一些开发者术语。

事件监听器

当前灯箱为每个烹饪帖子中的独立灯箱图片添加点击事件监听器。这意味着包含 20 张图片的帖子将有 20 个用于灯箱的点击事件监听器。

新灯箱利用事件委托,仅向帖子本身添加一个事件监听器。

以下是匿名用户在隐身窗口中强制垃圾回收后,包含 20 张图片的帖子上当前灯箱的事件监听器计数

以下是同一帖子使用新灯箱的情况

此外,目前在灯箱内导航时会添加事件监听器,这些监听器最终会成为孤儿并干扰垃圾回收。以下是图表:

  1. 加载包含一个帖子(含 20 张图片)的主题页面。
  2. 打开灯箱并连续三次遍历所有 20 张图片。
  3. 关闭灯箱
  4. 强制浏览器垃圾回收

当前灯箱:

新灯箱:

至于灯箱本身的事件监听器,它们目前在 Magnific 中似乎没有被清理(请记住,它并非为单页应用而构建)。

关于上述测试的一个简短说明:它们非常基础,数字并非旨在“科学”,目标在于确定方向而非精确数值。

开发者说明

让我们谈谈如何使用新的 Discourse Lightbox 设置和清理灯箱。

设置和清理灯箱

开发者有两种选择。

  1. 通过以下方式将灯箱服务注入组件:

    import { inject as service } from "@ember/service";
    
    //...
    
    @service lightbox
    

    然后您可以调用

    this.lightbox.setupLightboxes({
      container: yourContainer // DOM 节点
      selector: ".css-selector" // 您希望作为灯箱的元素的选择器字符串
    })
    

    之后,当您想要清理时,只需调用

    this.lightbox.cleanupLightboxes()

    就这样。

  2. 如果您不想注入灯箱服务,可以像这样导入 setupLightboxescleanupLightboxes

    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:opened
  • lightbox:item-will-change
  • lightbox:item-did-change
  • lightbox:closed

lightbox:opened

此事件在灯箱打开时触发,包含两个对象。

  1. items:这是当前灯箱中所有图片的数组。每个对象都是一个对象。
  2. 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。

路线图

  1. PR 审查
  2. PR 合并
  3. 一般反馈窗口(1-2 周)
  4. 从核心中移除 Magnific Popup 并移除实验性站点设置的 PR。
  5. TBD:扩展目标,例如探索不同布局的主题组件(由于新灯箱使用 CSS Grid 进行布局,这应该很简单)

致谢

  • 这项工作由 CDCK 慷慨赞助 :pray:
  • 向 Magnific Popup 的创作者 Dmytro Semenov 致以深深的敬意 :heart:,他创造了一个远远领先于时代的作品。
  • 上述演示中使用的图片由 Irina Iriser @pexels 提供
「いいね!」 38

YouTubeやその他のデスクトップアプリケーションで慣れているので、Fでフルスクリーン表示になります。
ショートカットに習慣的な記憶を使用しない理由がありますか?

「いいね!」 4

これは特に良いですね。 :raised_hands:

矢印キーで次の投稿の画像に移動できる機能があればよかったと思います。「かわいい猫」のようなトピックで、矢印キーを使って次の投稿の画像をシームレスに移動できれば、とても便利です。考慮すべき点はいくつかありますが、それは非常に良いでしょう。現時点では、ライトボックスを閉じて次の投稿に移動し、最初の画像をクリックする必要があります…

「いいね!」 16

bootstrap 3もjqweryを必要とします。Discourseはbootstrap 3を使用しています。したがって、jqweryはフォーラムの依存関係としてインストールされます。それは本当ですか?

「いいね!」 2

EmberJS も以前は jQuery を必要としており、Discourse は EmberJS アプリです。私たちは、Discourse 自体、公式プラグイン、およびコンポーネントでの jQuery の使用を削除するために、数年間取り組んできました。

多くの作業ですが、私たちは近づいています。このトピック自体が、その多くの取り組みの 1 つです。

「いいね!」 11

実装されました。 :partying_face:

しばらくの間、このトピックでフィードバックを収集しますので、ぜひチェックしてご意見をお聞かせください。 :slight_smile: :+1:

「いいね!」 6