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()。
这篇按工程落地顺序组织:先给可抄的配置与封装,再讲机制与沿革,最后是迁移清单。所有代码块都标了语言,关键行有注释。
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和 iframeallow双侧对齐。头做纵深,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