页面如何分辨自己是已安装的 PWA 还是普通标签页:display-mode 媒体查询、iOS 的 navigator.standalone 与各上下文的差异。
同一个网页应用,从主屏幕图标打开和从浏览器标签页打开,表现往往不一样:没有地址栏、拥有独立窗口,有时布局也不同。页面并不是猜出来的,而是向浏览器问了一个问题:我是作为已安装的应用在运行,还是只是一个普通标签页? 本文讲这个检测如何工作、哪里会出问题,以及它为什么也与指纹识别有关。
核心要点
- 基于标准的检测方式是
display-mode媒体特性:当页面以没有浏览器界面的已安装应用窗口运行时,window.matchMedia('(display-mode: standalone)').matches为true。 - 模式取决于 Web 应用清单中的
display成员,但页面看到的是它实际启动时的模式,而不是清单所请求的模式。 - iOS Safari 另有一个非标准的布尔值
navigator.standalone。这是 Apple WebKit 的属性:在 iOS 上从主屏幕启动为true,在标签页中为false,其他几乎所有环境下为undefined。 - 可靠的代码会同时使用两个信号,并把结果当作"呈现方式"的提示,而不是身份或安全的证明。
- 已安装的上下文比标签页稀有得多;而声称是 iOS 却没有
navigator.standalone的 User-Agent 在内部自相矛盾,所以检测工具也会读取这些信号。
标准方式:display-mode 媒体查询
应用清单通过 display 成员声明安装后的外观。MDN 的 display-mode 参考列出了页面可以匹配的取值,下面是最核心的四个(另有 window-controls-overlay、picture-in-picture 等较新的模式):
| 取值 | 用户看到的效果 |
|---|---|
fullscreen | 占满整个屏幕,没有系统界面 |
standalone | 独立的应用窗口,没有浏览器地址栏和标签页 |
minimal-ui | 类应用窗口,只保留少量浏览器控件 |
browser | 普通浏览器标签页或窗口 |
同一个媒体特性在 CSS 和 JavaScript 中都能使用:
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
// react if the user installs or switches while the page is open
window.matchMedia('(display-mode: standalone)')
.addEventListener('change', (e) => console.log('standalone:', e.matches));
@media (display-mode: standalone) {
.install-banner { display: none; }
}
有两个细节容易踩坑。第一,查询报告的是当前模式:清单写着 standalone 的应用,如果通过链接在标签页中打开,依然会报告 browser。第二,浏览器会按规范定义的回退链处理,所以在不允许全屏的平台上请求 fullscreen,最终可能得到 standalone 或 minimal-ui。要检测你关心的那个模式,不要直接假定清单里的值。
iOS 的额外信号:navigator.standalone
iOS 上的 Safari 早于这个媒体特性,自带一个标志:
// true: launched from the Home Screen; false: in a Safari tab;
// undefined: not iOS WebKit
const iosStandalone = navigator.standalone === true;
有用的地方在于区分 false 与 undefined。实现了该属性、但不是从主屏幕启动的浏览器返回 false,而 Windows 或 Android 上的 Chrome 返回 undefined。由于所有 iOS 浏览器都基于 WebKit,这个属性来自引擎本身,与浏览器 App 的品牌无关。
实用的检测器会把两者结合:
const installed =
window.matchMedia('(display-mode: standalone)').matches ||
navigator.standalone === true;
网站为什么要检查
- 数据分析。 按"已安装 / 标签页"拆分会话,可以看出安装后是否真被使用,以及安装用户的行为是否不同。
- 安装提示。 对已经添加到主屏幕的人,再显示"添加到主屏幕"毫无意义。
- 界面差异。 没有返回按钮和地址栏时,独立应用需要自己的导航,页面可能还要额外的安全区域内边距。
- 排查问题。 当页面记录了自己运行在哪种上下文,"标签页里正常、变成应用就坏"这类反馈会容易定位得多。
独立应用、WebView 与标签页
读者常把三种都没有常规浏览器界面的上下文混为一谈:
| 上下文 | display-mode: standalone | 典型特征 |
|---|---|---|
| 普通标签页 | 否(browser) | 完整的浏览器界面 |
| 已安装的 PWA | 是 | 独立窗口,从图标启动 |
| 应用内浏览器(WebView) | 通常否 | wv 标记或特定于应用的 User-Agent 特征 |
应用内浏览器是渲染在另一个应用(如社交软件)内部的页面,是宿主应用而非用户决定把它显示在那里的。已安装的 PWA 则是用户明确添加的。只检查"有没有地址栏"的页面会把两者混淆,所以要把媒体查询和 User-Agent 信号配合使用。
指纹识别的角度
这个信号本身能识别身份吗?单独来看几乎不能:它只有一个比特。但它会从两个方面补充画像。已安装的上下文比标签页稀有,因此会稍微缩小人群范围,与其他媒体特性信号的作用类似。同时它是一项一致性检查:User-Agent 说是 iPhone,而 navigator.standalone 却是 undefined,或者在桌面引擎上出现"iOS"配置,正是指纹一致性清单所描述的那类矛盾。它是移动端指纹版图中又一块小拼图。
关于应用模型本身,MDN 的 PWA 指南介绍了清单、可安装性,以及浏览器如何呈现已安装的应用。
亲自验证
在普通标签页中打开 BrowserInsight 的 WebView 检测,记下结果。然后把站点添加到主屏幕(或从浏览器菜单安装),从图标打开再运行一次。"独立(PWA)模式"信号读取的正是上面两项检查,你可以看着它翻转。我们的内核检测也会把 navigator.standalone 作为 WebKit 信号之一读取。
常见问题
display-mode 在桌面端有效吗?
有效。桌面版 Chromium 系浏览器可以把网站安装成应用窗口,该窗口会匹配 (display-mode: standalone)。各浏览器的支持和安装流程不同,请用特性检测而不要想当然。
页面能知道应用"能否被安装"吗?
通过这些信号不能。它们只描述页面当前的运行方式。安装提示依赖独立的、因浏览器而异的事件,而从未安装的标签页用户看起来与其他标签页无异。
还需要 navigator.standalone 吗?
新版 iOS 也支持该媒体查询,所以新代码可以主要依赖 display-mode。许多网站仍然两者都检查,因为只需一行代码,还能覆盖旧版行为。
用户能伪造吗?
User-Agent 切换器或自动化工具可以伪造字符串,但要伪造得一致,还必须与引擎的真实属性吻合,而一致性检查正是针对这一点。请把这个信号当作呈现方式的提示,而不是安全边界。
总结
检测 PWA 是否以独立模式运行只需两行:display-mode 媒体查询,再加上用于 iOS 主屏幕应用的 navigator.standalone。页面由此得知的是自己的启动方式,这对数据分析、安装提示和布局有用,对指纹识别则略有参考价值,主要用作一致性检查。


