Chrome 权限元素实战:144 与 151 的渐进增强写法

2026 年,浏览器权限体系正在发生一次悄无声息但方向明确的重构:权限请求正在从「散落在 JavaScript 里的 API 调用」走向「写在 HTML 里的声明式标签」。主角就是 Chrome 推出的 <permission> HTML 元素------配合早已标准化的 Permissions API 与 Permissions-Policy,浏览器正在把「查询、请求、管控」三层权限能力拼成一套完整拼图。

但「能用」和「用得好」是两回事。本文把这套新体系从设计动机、API 细节、控制面到坑位清单逐层拆开,并在文末给出 FAQ 与迁移决策清单。

结论先给,省时间:

  • 通用 <permission> 的 OT 是 Chrome 126→143,已结束;草案被 HTML Permission Elements 取代。
  • 现在能用的是 <geolocation>(Chrome 144)和 <usermedia>(Chrome 151),都是数据中介,不是单纯弹窗控件。
  • listenforchanges / dismissible / onpermissionstatuschange 这三个名字在规范 IDL 里不存在,别再抄。
  • onpromptaction 是普通 Event,没有 detail,拿不到授权结果;状态仍走 query()。

这篇按工程落地顺序组织:先给可抄的配置与封装,再讲机制与沿革,最后是迁移清单。所有代码块都标了语言,关键行有注释。

flowchart TD A[&#34;浏览器权限体系三层&#34;] --> B[&#34;查询层:Permissions API&#34;] A --> C[&#34;请求层: 元素&#34;] A --> D[&#34;管控层:Permissions-Policy&#34;] B --> B1[&#34;query() 查状态 granted/denied/prompt&#34;] B --> B2[&#34;onchange 监听权限变化&#34;] C --> C1[&#34;type 指定权限种类&#34;] C --> C2[&#34;listenforchanges / dismissible&#34;] C --> C3[&#34;promptaction / promptdismiss 事件&#34;] D --> D1[&#34;HTTP 头:Permissions-Policy&#34;] D --> D2[&#34;iframe allow 属性&#34;] B1 --> E[&#34;坑位清单与降级策略&#34;] C3 --> E D2 --> E

Permissions API:query() 与 PermissionStatus

新元素再怎么变,Permissions API 这一层基本没动。它是整套体系里兼容性最好的一块。

js 复制代码
async function readPermission(name, extra = {}) {
  if (!('permissions' in navigator)) return 'unknown';
  try {
    const status = await navigator.permissions.query(
      { name, ...extra }
    );
    return status.state; // granted / denied / prompt
  } catch (err) {
    // 该权限名未进入此浏览器的注册表
    return 'unknown';
  }
}

query() 只读状态。不弹窗 。MDN 写得很明确:触发提示的机制取决于具体 API,不属于 Permissions API 的定义范围。这跟「调用即弹窗」的老式 getUserMedia() 是两件事,也是很多人第一次用会误会的地方。

三档状态对应的动作别搞错。granted 直接用,别再弹。denied 引导去站点设置,弹也没用。prompt 才是在用户手势里发起请求的窗口。

还有一条容易漏。MDN 说 query() 返回的是当前上下文所有安全限制的聚合结果。被 Permissions-Policy 挡住的能力,会直接返回 denied,根本走不到弹窗。排查「为什么弹不出来」时,这一条能省掉半小时。

状态变化走 PermissionStatus:

js 复制代码
const status = await navigator.permissions
  .query({ name: 'camera' });

status.addEventListener('change', () => {
  console.log('权限状态变了:', status.state);
});

把 change 接进状态机,用户在另一个标签页改了站点设置,这边也能同步。比老资料里那个并不存在的 listenforchanges 靠谱得多。

可查询的权限名,按 2026 年这份清单记。容易错的口径都标在后面:

  • geolocation、notifications、camera、microphone:主力四个。camera / microphone 在 Firefox 是 132 起才支持。别默认全平台都行。
  • notifications 在 Safari 16.4 起可以 query,但它和 push 互为别名,返回同一个值。跨浏览器想稳妥,业务层还是读 Notification.permission。
  • midi:需要 SysEx 时传 { name: 'midi', sysex: true }。
  • clipboard-read、clipboard-write:仅安全上下文,Firefox 目前查不了。
  • local-fonts(Chrome 103)、screen-wake-lock(Chrome 84)、storage-access、persistent-storage、window-management:都在 MDN 的 permission-aware APIs 列表里。
  • local-network 和 loopback-network:本地网络访问的新名字,后面单独讲。
  • idle-detection 不在这份清单里 。它是 Permissions-Policy 的指令名,也是 IdleDetector.requestPermission() 走的路径(Chrome 94 起)。但 MDN 的 query() 权限名列表没收它。这一条我查了几个来源,都没找到一手肯定依据。按「不确定」处理。别硬塞进 query()。

那个 try/catch 不是防御性编程的洁癖。权限名没进注册表时,部分浏览器会直接抛 TypeError。把 unknown 当成「状态不明,但可以尝试调用」,再让失败路径统一兜底,比假设所有浏览器都认识所有名字要稳。

geolocation 与 usermedia:数据中介式写法

先看定位。Chrome 144 起,<geolocation> 把「申请权限」和「拿坐标」合成了一步。

html 复制代码
<geolocation
  onlocation="handleLocation(event)"
  autolocate
  accuracymode="precise">
  <button id="geo-fallback">用我的位置</button>
</geolocation>

标签里那个按钮不是装饰。支持新元素的浏览器渲染自己的原生控件,把子元素收起来。不支持的浏览器把它当未知元素,子元素照常显示。一份 HTML,两条路径。组件树里不用写运行时分支。

js 复制代码
function handleLocation(event) {
  const el = event.target;
  if (el.position) {
    const { latitude, longitude } = el.position.coords;
    console.log('位置:', latitude, longitude);
    return;
  }
  console.error('定位失败:', el.error?.message);
}

属性就那么几个,都挺实用。accuracymode 收 precise 或 approximate,对应老 API 的 enableHighAccuracy。autolocate 在权限已经给过的前提下加载即取位置,不会突然弹窗。watch 把一次性读取切成更新流,等价于 watchPosition()。元素上还能直接读 position 和 error,以及 isValid / invalidReason 这组状态,反映的是「浏览器信不信这个点击」。

再看相机和麦克风。Chrome 151 起,<usermedia> 接手整条链路。

html 复制代码
<usermedia id="media-ctrl"></usermedia>
<video id="preview" autoplay playsinline></video>
js 复制代码
const el = document.getElementById('media-ctrl');

el.setConstraints({
  video: { width: 1280, height: 720 },
  audio: { echoCancellation: true }
});

el.addEventListener('stream', () => {
  const video = document.getElementById('preview');
  video.srcObject = el.stream;
});

el.addEventListener('error', () => {
  console.error('采集失败:', el.error?.name);
});

el.addEventListener('cancel', () => {
  console.log('用户关掉了授权提示');
});

setConstraints() 要在用户点击之前调。硬件偏好先设好,点下去之后浏览器自己管提示、自己管恢复,拿到流直接挂在元素的 stream 上。三个事件 stream / error / cancel 分别对应成功、失败、取消。旧式 getUserMedia() 把「用户拒绝」和「设备被占用」混在同一个 catch 里,这里好分得多。

有个细节容易被忽略。两个元素都有强制样式约束:最小对比度、不透明度必须是 1、尺寸上下限、只允许 2D 变换和等比缩放。这不是设计建议。是安全模型的一部分。浏览器得保证用户点到的确实是它渲染的那个控件,而不是被 CSS 伪装过的什么东西。别想着把它调成「融入页面」的透明按钮,调不动。

特性检测也得换名字。定位可以用这个:

js 复制代码
if ('HTMLGeolocationElement' in window) {
  // 走声明式路径
}

<usermedia> 这边别去猜接口名。更稳的做法是把兜底按钮放进标签里,让不支持的浏览器自然降级,压根不做检测。官方给的渐进增强就是这个思路。少一层判断,少一个能写错的字符串。

Permissions-Policy × iframe allow:取交集

请求层写得再漂亮,有没有资格请求,是 Permissions-Policy 说了算。这一层由老的 Feature-Policy 演化而来。Chrome 88 起支持新头。

http 复制代码
Permissions-Policy: camera=(self), microphone=()
http 复制代码
Permissions-Policy: camera=(self "https://a.example")

括号里是白名单。(self) 只给本源。() 谁都不给。引号里的 origin 是显式放行的第三方。* 全放开,基本等于没配。语法用的是 structured header。别把 Feature-Policy 那套空格分隔的写法粘过来,浏览器不认。

iframe 那边还要再收一道:

html 复制代码
<iframe src="https://a.example/recorder"
        allow="camera; microphone"></iframe>

两侧是取交集 。不是并集。父页面写了 microphone=(),子框架再怎么写 allow 也救不回来。反过来,父页面放行了 origin,iframe 没写 allow,子框架照样拿不到设备。报出来的 NotAllowedError 还经常和权限问题混在一起,很难一眼看出是配置漏了。

有个兼容性口径必须补上。Permissions-Policy 响应头是 Chromium 系(Chrome / Edge)在执行的 。Firefox 和 Safari 主要走 iframe allow 属性模型。把头当跨平台方案会翻车。稳妥做法是头做纵深防御,allow 做跨浏览器兜底,两边都写。

自检也别再用 document.featurePolicy 了。MDN 已经把它标成 Deprecated 加 Non-standard。现在推荐两个:DevTools 的 Application → Frames 里逐个框架看允许/禁止清单,以及代码里用 iframe 元素上的方法。

js 复制代码
const frame = document.querySelector('iframe');
console.log(frame.allowsFeature('camera'));

element.allowsFeature() 会把 allow 属性算进去。老的 document.allowsFeature(feature, origin) 不会。另外,拼错的指令不是「整条 Policy 静默失效」,而是那一条被忽略,控制台给一句 Unrecognized feature 警告。排查时值得顺手看一眼 Console。

通用 <permission> 元素的 OT 止于 Chrome 143

时间线其实挺清楚。只是中文资料普遍停在中间某一段。

PEPC,全称 Page-Embedded Permission Control,最早想用一个通用元素解决所有强权限。写 type="camera" 就申请摄像头,type="geolocation" 就申请定位,多个权限用空格隔开。这个思路从 Chrome 126 开始 Origin Trial。一开始公告说到 131 结束。后来一路延到 143。

试验数据是好看的。Chrome 官方博客给了几组厂商自报的结果:Zoom 说用这个元素引导用户走恢复流程后,摄像头或麦克风的采集错误降了四成六;Immobiliare.it 的定位成功流程多了两成;ZapImóveis 那边,之前把权限点成阻止的用户里,有五成四能当场救回来。这几个数字得连起来看。它们证明的是「把请求放回使用场景」这个方向对。不是证明「通用标签」这个形态对。

转折来自另外两家。Apple 的 WebKit 和 Mozilla 都反馈,一个一刀切的通用控件要同时照顾定位、相机、麦克风各自的行为差异,复杂度太高。于是 Chrome 换了个方向,从 generic permission control 改成 capability-specific elements,按能力拆开做。

WICG 那份老草案还在线,开头就写着已被取代。照着它的 IDL 写代码,会踩到不少已经不存在的东西。

把最容易抄错的几个名字列一下。它们都不在规范里 :listenforchanges、dismissible、onpermissionstatuschange。老草案的 HTMLPermissionElement 完整 IDL 里,可写属性只有 type 和 preciseLocation。只读的是 isValid、invalidReason、initialPermissionStatus、permissionStatus。事件处理器只有三个:onpromptaction、onpromptdismiss、onvalidationstatuschange。

还有个更隐蔽的坑。网上不少示例写 event.detail.type 和 event.detail.state,说分别给出权限名和授权后的新状态。不对。onpromptaction 在规范里就是个普通 Event,初始化只设了 bubbles 和 cancelable。没有 detail,也不带授权结果。Chrome 官方补了一句更关键的话:用户可能点了「继续允许」这种维持现状的操作。提示被回答了,状态却没变。

所以事件只当信号用。状态仍旧回 query() 去读:

js 复制代码
const el = document.querySelector('permission');

el.addEventListener('promptaction', () => {
  // 只表示「提示被回答」,不表示状态变了
  navigator.permissions.query({ name: 'camera' })
    .then(s => console.log('当前状态:', s.state));
});

el.addEventListener('promptdismiss', () => {
  console.log('提示被关掉了,本次会话别再弹');
});

这段是给还在维护老 OT 代码的人看的。新项目不用碰。

LNA:Chrome 142 起 gate,145 起拆权限

LNA 从 Chrome 142 起 gate,145 拆成 local-network / loopback-network,旧名保留为别名;146 加企业策略,147 扩到 WebSocket 与 WebTransport。

静默屏蔽与一次性授权:旧路径为什么失效

Chrome 在用户连续三次关掉提示后启用一周静默屏蔽;一次性授权从 Chrome 116 起提供。结论是「曾经 granted 不等于现在 granted」,别缓存权限状态。

迁移检查清单与降级路径

先说迁移决策。它比 API 细节更影响排期。

核心用户集中在 Chromium,页面又是权限密集型(会议、直播、录音、地图、AI 语音),可以按能力小流量接专用元素。要覆盖 Safari 和 Firefox 的完整一致行为,就继续保持 JS 请求加 Permissions API 包裹,把兜底留在标签里,等两家跟进。

中间那条路最实用:先把查询层补全。这部分兼容性好,收益当天就有。专用元素按能力一个个换,不要一波全切。

无论走哪条,下面几条都是必做:

  • 强权限调用前先 query()。prompt 态务必放在用户手势里发起。denied 时引导去站点设置,而不是再弹。
  • 别缓存权限状态。一次性授权加上用户随时能改设置,缓存从一开始就是错的。
  • 父页 Permissions-Policy 和 iframe allow 双侧对齐。头做纵深,allow 做跨平台兜底。
  • 权限失败至少分三支:denied 引导重开、prompt 手势内重试、硬件或设备失败给换设备提示。别统统当「网络不好」。
  • 处理 cancel / promptdismiss,记录会话级静默期。三次关闭换来一周静默屏蔽,这个代价不值得为一次转化去赌。
  • 用 DevTools Frames 和 element.allowsFeature() 验证 Policy。别靠 document.featurePolicy,也别靠「配了应该就生效」。

还有三个最常被问住的:

专用元素是正式标准了吗? Chrome 144 / 151 是正式提供,不是 Origin Trial。但 <usermedia> 的规范仍在 W3C Media Capture and Streams Extensions 工作组开发中。Chrome Platform Status 记录 Shipping 151,Firefox under consideration,WebKit 暂无信号。按「Chromium 单引擎能力 + 渐进增强」对待。别当跨浏览器标准。

能不能静默自动授权? 基本不能。除了站点设置里已经显式允许,以及少数已安装 PWA 的场景,强权限都得经过用户交互。该优化的不是绕过弹窗,是请求时机。用户点下「开始录制」的那一秒。

老项目要不要立刻迁? 先答三个问题:核心用户在不在 Chromium,是不是重度依赖权限恢复,愿不愿意维护双路径。三个都是「是」再动。否则把查询层补齐就够,专用元素等另外两家落地再切不迟。

回到开头那件事。你笔记里那个 <permission> 并没有写错。它只是停在 Chrome 143 之前了。

真正该改的不是标签名,是思路。浏览器正在把「什么时候问用户」这件事从 JS 手里收回去,顺便把数据也一起递出来。下次再看到「一个标签搞定所有权限」的教程,先翻一眼它的版本号。

你项目里最先撞上的会是哪一个?定位,还是摄像头?

原创技术博客 · 开源项目分享 · AI全栈创作社区 idao.fun

相关推荐
IT_陈寒7 小时前
Redis连接池泄漏那次,我差点把生产环境搞崩了
前端·人工智能·后端
强尼银手7 小时前
我花一天做了个格斗游戏原型,判定全对,然后被「美术」劝退了
前端·javascript
石小石Orz7 小时前
AI 正在蚕食我们的编码能力
前端·后端·算法
陈随易7 小时前
专注飞机 FocusFlight,两个人把番茄钟做成了一趟航班
前端·后端·程序员
总要学点什么8 小时前
第98篇:三维特效-材质线效果——给一条线换十种材质,虚线的图案只填了一半
开发语言·前端·javascript·ecmascript·gis·webgl·材质
10share8 小时前
基于大模型的 i18n 方案:把中文原文当 Key,轻松搞定多语言维护
前端·vue.js
OpsEye9 小时前
混合云 AI 架构,如何统一做敏感内容拦截?
javascript·ai编程
Funlang10 小时前
给机器写的 JavaScript 造个笼子
开发语言·javascript·ecmascript
古茗前端团队11 小时前
由 key 引发的思考
前端·javascript·react.js