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

使用窗口控件旁边的标题栏区域,让您的 PWA 看起来更像应用。

如果您还记得我之前的文章《让您的 PWA 看起来更像应用》,可能还记得我提到过自定义应用的标题栏是一种打造更像应用的体验的策略。以下示例展示了 macOS 版“播客”应用的显示效果。

macOS 版播客应用标题栏,其中显示了媒体控件按钮和有关当前播放的播客的元数据。
自定义标题栏可让您的 PWA 看起来更像特定平台的应用。

现在,您可能会想反驳,说播客是一款平台专用 macOS 应用,不会在浏览器中运行,因此可以随心所欲,而无需遵守浏览器的规则。没错,但好消息是,您很快就可以通过窗口控件叠加层功能(本文的主题)为 PWA 创建类似的界面。

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

  1. 网络应用清单中 "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 的来源匹配。

这样一来,标题栏区域就会是空白的,常规窗口控件位于左侧或右侧(具体取决于操作系统)。

标题栏为空的应用窗口,窗口控件位于左侧。
一个空白标题栏,可放置自定义内容。

将内容移至标题栏

现在,标题栏中有了空白,您可以将一些内容移到那里。在本文中,我构建了一个 Wikimedia 精选内容 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),
);

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

您需要考虑以下两种可能的情况:

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

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

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

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

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

界面注意事项

虽然很诱人,但我们不建议在窗口控件叠加层区域创建传统下拉菜单。这样做会违反 macOS 的设计准则。在 macOS 平台上,用户希望菜单栏(系统提供的和自定义的)位于屏幕顶部。

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

演示

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

包含窗口控件叠加层的 Wikimedia 精选内容演示版应用。
演示版应用可用于实验。

窗口控件叠加层中的搜索功能完全正常运行:

显示窗口控件叠加层的 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 中导航到其他源会导致其回退到常规的独立标题栏,即使它符合上述条件并使用窗口控件叠加层启动也是如此。这是为了容纳在导航到其他来源时显示的黑条。导航回原始来源后,系统会再次使用窗口控件叠加层。

用于导航到非源网址的黑色网址栏。
当用户导航到其他来源时,系统会显示一个黑条。

反馈

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

请向我们说明 API 设计

API 是否存在某些方面未按预期运行?或者,您是否缺少实现想法所需的方法或属性?对安全模型有疑问或意见?在相应的 GitHub 代码库中提交规范问题,或在现有问题中添加您的想法。

报告实现方面的问题

您是否发现了 Chromium 实现中的 bug?或者实现方式是否与规范不同? 请访问 new.crbug.com 提交 bug。请务必提供尽可能详细的信息、简单的重现说明,并在 Components 框中输入 UI>Browser>WebAppInstalls故障非常适合分享快速简便的重现步骤。

显示对该 API 的支持

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

在 Twitter 上向 @ChromiumDev 发送包含 #WindowControlsOverlay 标签的推文,告诉我们您在哪里以及如何使用该工具。

实用链接

致谢

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