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

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

如果您还记得我的文章让您的 PWA 更像应用,您可能还记得我如何提到自定义应用标题栏是创建更像应用体验的策略。以下是 macOS Podcasts 应用的外观示例。

A macOS Podcasts app title bar showing media control buttons and metadata about the currently playing podcast.
自定义标题栏使您的 PWA 感觉更像特定于平台的应用。

现在您可能会想反驳说,Podcasts 是特定于 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. Origin Trial 完成
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 的来源匹配。

结果是一个空的标题栏区域,窗口控件位于左侧或右侧,具体取决于操作系统。

An app window with an empty titlebar with the window controls on the left.
一个空的标题栏,已准备好用于自定义内容。

将内容移入标题栏

现在标题栏中有空间了,您可以将一些内容移到那里。对于本文,我构建了一个 Wikimedia Featured Content 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 内容组成一样,而事实上,它确实如此。

An app window with a search bar in the title bar.
新的标题栏是活动且响应式的。

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

虽然上面的屏幕截图表明您已完成,但您还没有完全完成。PWA 窗口不再可拖动(除了一个非常小的区域),因为窗口控件按钮不是拖动区域,而标题栏的其余部分由搜索小部件组成。使用值为 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() 查询窗口控件叠加层区域就足够了,但在其他情况下,需要更精细的控制。例如,一个可能的用例可能是根据可用空间调整窗口控件叠加层,并在有足够空间时在窗口控件叠加层中添加一个笑话。

Window controls overlay area on a narrow window with shortened text.
标题栏控件已调整为适应窄窗口。

您可以通过订阅 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);
}

除了将函数分配给 ongeometrychange 之外,您还可以像下面这样向 windowControlsOverlay 添加事件侦听器。您可以在 MDN 上阅读有关两者之间差异的信息。

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

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

有两种可能的情况需要考虑

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

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

A PWA running in a browser tab with the window controls overlay displayed in the body.
用于标题栏的控件可以轻松地在旧版浏览器的主体中显示。

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

A PWA running in standalone mode with the window controls overlay displayed in the body.
用于标题栏的控件可以轻松地在旧版浏览器的主体中显示。

UI 考量因素

虽然可能很诱人,但不建议在窗口控件叠加层区域中创建经典的下拉菜单。这样做会违反 macOS 上的设计指南,在该平台上,用户期望在屏幕顶部看到菜单栏(系统提供的菜单栏和自定义菜单栏)。

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

演示

我创建了一个 演示,您可以在不同的支持和不支持的浏览器以及安装和未安装状态下进行体验。对于实际的窗口控件叠加层体验,您需要安装该应用。您可以在下面看到两个预期效果的屏幕截图。应用的源代码可在 Glitch 上找到。

The Wikimedia Featured Content demo app with Window Controls Overlay.
演示应用可用于实验。

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

The Wikimedia Featured Content demo app with Window Controls Overlay and active search for the term 'cleopa…' highlighting one of the articles with the matched term 'Cleopatra'.
使用窗口控件叠加层的搜索功能。

安全注意事项

Chromium 团队使用 控制对强大的 Web 平台功能的访问中定义的核心原则设计和实现了窗口控件叠加层 API,包括用户控制、透明度和人体工程学。

欺骗

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

指纹识别

启用窗口控件叠加层和可拖动区域不会造成重大的隐私问题,功能检测除外。但是,由于不同操作系统中窗口控件按钮的大小和位置不同,navigator.windowControlsOverlay.getTitlebarAreaRect() 方法返回的 DOMRect 的位置和尺寸会泄露有关浏览器运行所在的操作系统的信息。目前,开发者已经可以从用户代理字符串中发现操作系统,但由于指纹识别问题,正在讨论冻结 UA 字符串并统一操作系统版本。浏览器社区内部正在努力了解窗口控件叠加层的大小在不同平台上的变化频率,因为目前的假设是,这些在操作系统版本之间相当稳定,因此对于观察次要操作系统版本没有用处。虽然这是一个潜在的指纹识别问题,但它仅适用于使用自定义标题栏功能的已安装 PWA,而不适用于常规浏览器使用。此外,navigator.windowControlsOverlay API 将不适用于嵌入在 PWA 内部的 iframe。

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

A black URL bar for out-of-origin navigation.
当用户导航到不同的来源时,会显示黑色条。

反馈

Chromium 团队希望了解您在使用窗口控件叠加层 API 方面的体验。

告诉我们有关 API 设计的信息

API 是否有某些方面未按您的预期工作?或者是否有您需要用来实现您的想法的缺失方法或属性?对安全模型有疑问或意见?在相应的 GitHub 存储库上提交规范问题,或将您的想法添加到现有问题。

报告实现问题

您是否发现了 Chromium 实现中的错误?或者实现是否与规范不同?在 new.crbug.com 上提交错误。请务必尽可能详细地说明,提供用于重现的简单说明,并在 Components 框中输入 UI>Browser>WebAppInstallsGlitch 非常适合共享快速且简单的重现步骤。

表达对 API 的支持

您是否计划使用窗口控件叠加层 API?您的公开支持有助于 Chromium 团队确定功能的优先级,并向其他浏览器供应商展示支持这些功能的重要性。

发送推文至 @ChromiumDev,并在推文中添加 #WindowControlsOverlay 标签,让我们知道您在哪里以及如何使用它。

实用链接

致谢

窗口控件叠加层由 Microsoft Edge 团队的 Amanda Baker 实现和指定。本文由 Joe MedleyKenneth Rohde Christiansen 审阅。题图由 SigmundUnsplash 上提供。