自定义 PWA 标题栏的窗口控件叠加层

使用窗口控件旁边的标题栏区域,使您的 PWA 感觉更像一个应用。

还记得我的文章让 PWA 感觉更像一个应用的文章,可能还记得我前面提到过,自定义应用的标题栏作为打造更类似应用的体验的策略。下面的示例展示了 macOS 播客应用的显示效果。

macOS 播客应用标题栏,其中显示了媒体控件按钮以及有关当前正在播放的播客的元数据。
自定义标题栏会让您的 PWA 感觉更像是一个平台专用应用。

现在您可能会反对以下说法:播客是平台专用 macOS 应用,不能在浏览器中运行,因此可以执行所需的操作,而无需遵守浏览器的规则。正确,但好消息是,借助本文所述的“窗口控制叠加层”功能,您很快就能为 PWA 创建类似的界面。

窗口控件叠加层组件

窗口控件叠加层由四个子功能组成:

  1. Web 应用清单中 "display_override" 字段的 "window-controls-overlay" 值。
  2. CSS 环境变量 titlebar-area-xtitlebar-area-ytitlebar-area-widthtitlebar-area-height
  3. 将先前专有的 CSS 属性 -webkit-app-region 标准化为 app-region 属性,以定义 Web 内容中的可拖动区域。
  4. 一种通过 window.navigatorwindowControlsOverlay 成员查询和解决窗口控件区域的机制。

什么是窗口控件叠加层

标题栏区域是指窗口控件(即最小化、最大化、关闭等的按钮)左侧或右侧的空间,通常包含应用的标题。借助窗口控件叠加层,渐进式 Web 应用 (PWA) 可将现有的全宽标题栏替换为包含窗口控件的小叠加层,从而提供更接近应用的感觉。这样一来,开发者便可以将自定义内容放置在以前由浏览器控制的标题栏区域中。

当前状态

步骤 状态
1. 创建铺垫消息 完成
2. 创建规范的初始草稿 完成
3. 收集反馈并不断改进设计 进行中
4. 源试用 全面完整
5. 发布 完成(在 Chromium 104 中)

如何使用窗口控件叠加层

window-controls-overlay 添加到 Web 应用清单

渐进式 Web 应用可以通过在 Web 应用清单中将 "window-controls-overlay" 添加为主要 "display_override" 成员来选择窗口控件叠加层:

{
  "display_override": ["window-controls-overlay"]
}

仅当满足以下所有条件时,窗口控件叠加层才会显示:

  1. 应用不是在浏览器中打开,而是在单独的 PWA 窗口中打开。
  2. 该清单包含 "display_override": ["window-controls-overlay"]。(此后允许使用其他值。)
  3. PWA 在桌面操作系统上运行。
  4. 当前来源与安装 PWA 的来源一致。

这将产生一个空白标题栏区域,常规窗口控件位于左侧或右侧,具体取决于操作系统。

带有空白标题栏的应用窗口,窗口控件位于左侧。
一个空的标题栏,可在其中显示自定义内容。

将内容移入标题栏

现在标题栏中有空间,您可以将内容移到此处。在本文中,我构建了维基媒体精选内容 PWA该应用的一项实用功能可能是搜索文章标题中的字词。搜索功能的 HTML 代码如下所示:

<div class="search">
  <img src="logo.svg" alt="Wikimedia logo." width="32" height="32" />
  <label>
    <input type="search" />
    Search for words in articles
  </label>
</div>

如需将此 div 移到标题栏中,需要使用一些 CSS:

.search {
  /* Make sure the `div` stays there, even when scrolling. */
  position: fixed;
  /**
   * Gradient, because why not. Endless opportunities.
   * The gradient ends in `#36c`, which happens to be the app's
   * `<meta name="theme-color" content="#36c">`.
   */
  background-image: linear-gradient(90deg, #36c, #131313, 33%, #36c);
  /* Use the environment variable for the left anchoring with a fallback. */
  left: env(titlebar-area-x, 0);
  /* Use the environment variable for the top anchoring with a fallback. */
  top: env(titlebar-area-y, 0);
  /* Use the environment variable for setting the width with a fallback. */
  width: env(titlebar-area-width, 100%);
  /* Use the environment variable for setting the height with a fallback. */
  height: env(titlebar-area-height, 33px);
}

您可以在下面的屏幕截图中看到此代码的效果。标题栏可完全自适应。当您调整 PWA 窗口的大小时,标题栏看起来就像是由常规 HTML 内容组成(事实上就是这样)。

一个在标题栏中有一个搜索栏的应用窗口。
新的标题栏有效且响应迅速。

确定标题栏的哪些部分可拖动

虽然上面的屏幕截图表明您已完成操作,但操作尚未完成。PWA 窗口再不可拖动(在非常小的区域之外),因为窗口控件按钮不是拖动区域,而标题栏的其余部分由搜索 widget 组成。请使用值为 dragapp-region CSS 属性来解决此问题。在具体的情况下,可以将 input 元素以外的所有内容设置为可拖动。

/* The entire search `div` is draggable… */
.search {
  -webkit-app-region: drag;
  app-region: drag;
}

/* …except for the `input`. */
input {
  -webkit-app-region: no-drag;
  app-region: no-drag;
}

添加此 CSS 后,用户可以通过拖动 divimglabel 像往常一样拖动应用窗口。只有 input 元素是互动元素,因此用户可以输入搜索查询。

功能检测

可以通过测试是否存在 windowControlsOverlay 来检测是否支持窗口控件叠加层:

if ('windowControlsOverlay' in navigator) {
  // Window Controls Overlay is supported.
}

使用 windowControlsOverlay 查询窗口控件区域

到目前为止,这段代码有一个问题:在某些平台上,窗口控件位于右侧,而在其他平台上,窗口控件位于左侧。更糟糕的是,“三点”Chrome 菜单也会根据平台改变位置。这意味着,线性渐变背景图片需要动态调整为从 #131313maroonmaroon#131313maroon 运行,以便与标题栏的 maroon 背景颜色(由 <meta name="theme-color" content="maroon"> 确定)混合在一起。这可以通过在 navigator.windowControlsOverlay 属性上查询 getTitlebarAreaRect() API 来实现。

if ('windowControlsOverlay' in navigator) {
  const { x } = navigator.windowControlsOverlay.getTitlebarAreaRect();
  // Window controls are on the right (like on Windows).
  // Chrome menu is left of the window controls.
  // [ windowControlsOverlay___________________ […] [_] [■] [X] ]
  if (x === 0) {
    div.classList.add('search-controls-right');
  }
  // Window controls are on the left (like on macOS).
  // Chrome menu is right of the window controls overlay.
  // [ [X] [_] [■] ___________________windowControlsOverlay [⋮] ]
  else {
    div.classList.add('search-controls-left');
  }
} else {
  // When running in a non-supporting browser tab.
  div.classList.add('search-controls-right');
}

经过修改的代码现在使用上述代码动态设置的两个类,而不是直接在 .search 类 CSS 规则中使用背景图片(像之前一样)。

/* For macOS: */
.search-controls-left {
  background-image: linear-gradient(90deg, #36c, 45%, #131313, 90%, #36c);
}

/* For Windows: */
.search-controls-right {
  background-image: linear-gradient(90deg, #36c, #131313, 33%, #36c);
}

确定窗口控件叠加层是否可见

在任何情况下,标题栏区域都不会显示窗口控件叠加层。虽然它在不支持窗口控件叠加层功能的浏览器上自然不会出现,但是当相关 PWA 在标签页中运行时,它也不会显示。如需检测这种情况,您可以查询 windowControlsOverlayvisible 属性:

if (navigator.windowControlsOverlay.visible) {
  // The window controls overlay is visible in the title bar area.
}

或者,您也可以在 JavaScript 和/或 CSS 中使用 display-mode 媒体查询:

// Create the query list.
const mediaQueryList = window.matchMedia('(display-mode: window-controls-overlay)');

// Define a callback function for the event listener.
function handleDisplayModeChange(mql) {
  // React on display mode changes.
}

// Run the display mode change handler once.
handleDisplayChange(mediaQueryList);

// Add the callback function as a listener to the query list.
mediaQueryList.addEventListener('change', handleDisplayModeChange);
@media (display-mode: window-controls-overlay) { 
  /* React on display mode changes. */ 
}

接收几何图形变更通知

使用 getTitlebarAreaRect() 查询窗口控件叠加层区域足以满足一次性的操作,例如根据窗口控件的位置设置正确的背景图片,但在其他情况下,需要进行更精细的控制。例如,可能的用例是根据可用空间调整窗口控件叠加层,并在有足够的空间时直接在窗口控件叠加层中添加笑话。

窄窗口上的窗口控件叠加区域,其中包含缩短的文本。
可适应窄窗口的标题栏控件。

您可以通过订阅 navigator.windowControlsOverlay.ongeometrychange 或为 geometrychange 事件设置事件监听器来接收几何图形更改通知。此事件仅在窗口控件叠加层可见时触发,即 navigator.windowControlsOverlay.visibletrue 时。

const debounce = (func, wait) => {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
};

if ('windowControlsOverlay' in navigator) {
  navigator.windowControlsOverlay.ongeometrychange = debounce((e) => {
    span.hidden = e.titlebarAreaRect.width < 800;
  }, 250);
}

您还可以向 windowControlsOverlay 添加事件监听器,而不是为 ongeometrychange 分配函数,如下所示。您可以在 MDN 上了解两者之间的区别。

navigator.windowControlsOverlay.addEventListener(
  'geometrychange',
  debounce((e) => {
    span.hidden = e.titlebarAreaRect.width < 800;
  }, 250),
);

在标签页中以及不受支持的浏览器中运行时的兼容性

可以考虑以下两种情况:

  • 应用正在支持窗口控制叠加层的浏览器中运行,但该应用在浏览器标签页中使用的情况。
  • 应用正在不支持窗口控制叠加层的浏览器中运行的情况。

在这两种情况下,默认情况下,为窗口控件叠加层构建的 HTML 会像常规 HTML 内容一样以内嵌方式显示,并且 env() 变量的回退值将会启动以进行定位。在支持的浏览器中,您还可以通过检查叠加层的 visible 属性,如果其报告 false,然后隐藏该 HTML 内容,决定不为窗口控件叠加层显示指定的 HTML。

一款在浏览器标签页中运行的 PWA,正文中显示窗口控件叠加层。
在旧版浏览器上,原本用于标题栏的控件可以轻松显示在正文中。

谨此提醒,不支持的浏览器要么根本不会考虑 "display_override" Web 应用清单属性,要么无法识别 "window-controls-overlay",进而根据回退链使用下一个可能的值,例如 "standalone"

在独立模式下运行的 PWA,正文中显示窗口控件叠加层。
在旧版浏览器上,原本用于标题栏的控件可以轻松显示在正文中。

界面注意事项

虽然您可能很想在“窗口控件叠加层”区域中创建经典的下拉菜单,但我们建议您不要这样做。这样做会违反 macOS 的设计准则。在该平台上,用户希望菜单栏(包括系统提供的菜单栏和自定义栏)位于屏幕顶部。

如果您的应用提供全屏体验,请仔细考虑窗口控件叠加层是否有必要成为全屏视图的一部分。您可能想要在 onfullscreenchange 事件触发时重新排列布局。

演示

我创建了一个演示版,您可以在不同支持和不支持的浏览器以及已安装和未安装状态下试玩该演示版。若要获得实际的窗口控制叠加层体验,您需要安装该应用。您可以查看下面两张屏幕截图,了解预期结果。Glitch 上提供了该应用的源代码

带有窗口控件叠加层的维基媒体精选内容演示版应用。
演示版应用可用于实验。

窗口控件叠加层中的搜索功能完全可以正常使用:

带有窗口控制叠加层的 Wikimedia 精选内容演示版应用,并且正在搜索字词“cleopa...”,其中突出显示了其中一篇文章匹配字词“Cleopatra”。
使用窗口控件叠加层的搜索功能。

安全注意事项

Chromium 团队按照控制对强大 Web 平台功能的访问权限中定义的核心原则(包括用户控制、透明度和工效学设计)设计和实现了 Window Controls Overlay API。

仿冒邮件

授予网站对标题栏的部分控制权,让开发者在以前是受信任的浏览器控制的区域中伪造内容。目前,在 Chromium 浏览器中,独立模式包含一个标题栏,首次启动时,标题栏会在左侧显示网页的标题,在右侧显示网页的来源(后跟“设置及更多”按钮和窗口控件)。几秒钟后,原始文字会消失。如果浏览器设置为从右到左 (RTL) 的语言,系统会翻转此布局,使源文本位于左侧。如果原点与叠加层右边缘之间的内边距不足,此操作会打开窗口控件叠加层,以伪造原点。例如,来源“evil.ltd”可以附加在可信网站“google.com”后面,让用户认为该来源是可信的。我们计划保留此源文本,以便用户知道应用的来源是什么,并确保其符合用户的预期。对于配置 RTL 的浏览器,源文本右侧必须有足够的内边距,以防止恶意网站将不安全的源与可信源相附加。

数字“指纹”收集

除了功能检测之外,启用窗口控件叠加层和可拖动区域不会带来明显的隐私问题。不过,由于窗口控制按钮在不同操作系统中的大小和位置不同,navigator.windowControlsOverlay.getTitlebarAreaRect() 方法会返回 DOMRect,其位置和尺寸会显示有关运行浏览器的操作系统的信息。目前,开发者已经可以通过用户代理字符串发现操作系统,但由于数字“指纹”收集方面的顾虑,目前正在讨论如何冻结 UA 字符串和统一操作系统版本。浏览器社区一直在努力了解窗口控件叠加层大小在不同平台上的变化频率,因为当前的假设是这些控件在不同操作系统版本之间相当稳定,对观察次要操作系统版本没有帮助。虽然这是一个潜在的指纹问题,但仅适用于已安装的使用自定义标题栏功能的 PWA,而不适用于常规浏览器使用情况。此外,navigator.windowControlsOverlay API 将不适用于嵌入 PWA 中的 iframe。

在 PWA 中导航到其他来源会导致该 PWA 回退到正常的独立标题栏,即使它符合上述条件并通过窗口控件叠加层启动也是如此。这是为了适应在导航到其他源站时出现的黑色长条。导航回原始来源后,系统将再次使用窗口控件叠加层。

针对源外导航的黑色网址栏。
当用户导航到其他出发地时,系统会显示一个黑色长条。

反馈

Chromium 团队希望了解您使用 Window Controls Overlay API 的体验。

向我们介绍 API 设计

是否存在 API 行为不符合您预期的情况?或者说,是否缺少某些方法或属性来实现您的想法?如果您对安全模型有疑问或意见,在相应的 GitHub 代码库中提交规范问题,或将您的想法添加到现有问题中。

报告实施方面的问题

您是否发现了 Chromium 实现中存在的错误?或者,实现方式是否不同于规范? 请在 new.crbug.com 提交 bug。请务必提供尽可能多的详细信息和简单的重现说明,并在组件框中输入 UI>Browser>WebAppInstallsGlitch 非常适合快速轻松地分享重现的视频。

显示对该 API 的支持

您打算使用 Window Controls Overlay API 吗?您的公开支持有助于 Chromium 团队确定各项功能的优先级,并向其他浏览器供应商显示支持这些功能的重要性。

您可以发送一条包含 #WindowControlsOverlay # 标签的 @ChromiumDev 微博,告诉我们您使用它的位置和方式。

实用链接

致谢

窗口控件叠加层由 Microsoft Edge 团队的 Amanda Baker 实现和指定。本文由 Joe MedleyKenneth Rohde Christiansen 审核。主打图片,作者:Sigmund,发布于 Unsplash 用户。