屏幕唤醒锁(Screen Wake Lock)是浏览器提供的一个实用能力,典型场景是视频播放、地图导航、在线演示等不希望屏幕自动熄灭的页面。不过很多开发者在TypeScript项目中接入时发现,navigator.wakeLock在默认的lib.dom.d.ts中要么没有声明,要么声明过于简略,尤其是在与Media Session配合设置唤醒锁激活状态(setWakeLockActive这类语义)时,状态同步的类型定义经常报错。这篇文章就来系统地讲清楚这套API的类型该怎么写。

Wake Lock API的核心概念与类型结构
Wake Lock API的整体结构并不复杂:通过navigator.wakeLock.request()请求一把锁,返回一个WakeLockSentinel对象,这个对象既是锁的句柄,也是事件源。锁有两种类型:屏幕锁(screen)用于阻止屏幕熄灭,系统锁(system)用于阻止设备休眠,其中系统锁目前浏览器支持非常有限,实际项目中基本只用screen。
类型定义的关键在于对生命周期的准确建模。Sentinel对象有一个released布尔属性表示锁是否已释放,一个release()方法用于主动释放,以及一个release事件,当锁因页面失焦、标签页隐藏、系统策略等原因被浏览器强制释放时触发。很多类型写法的坑就出在把released当成永远可靠的状态,实际上它只是一个快照,真正的状态同步必须依赖事件。
基础类型可以这样声明:
type WakeLockType = 'screen' | 'system';
interface WakeLockSentinelEvent extends Event {
target: WakeLockSentinel;
}
interface WakeLockSentinel extends EventTarget {
readonly type: WakeLockType;
readonly released: boolean;
onrelease: ((this: WakeLockSentinel, ev: WakeLockSentinelEvent) => void) | null;
release(): Promise<void>;
}
interface WakeLock {
request(type: WakeLockType): Promise<WakeLockSentinel>;
}扩展navigator类型与Media Session状态同步的类型设计
有了基础接口之后,还需要把它挂到navigator上。由于TypeScript的接口声明合并特性,最干净的方式是通过模块声明补充Navigator接口,而不是用as any硬绕过去。这样既保留了类型检查,又不会污染全局的其他逻辑。
declare global {
interface Navigator {
wakeLock?: WakeLock; // 可选,因为不是所有浏览器都支持
}
// 使用时的能力检测
async function acquireScreenLock(): Promise<WakeLockSentinel | null> {
if (!navigator.wakeLock) {
console.warn('当前浏览器不支持 Wake Lock API');
return null;
}
try {
return await navigator.wakeLock.request('screen');
} catch (err) {
console.warn('请求唤醒锁被拒绝:', err);
return null;
}
}注意这里把wakeLock声明为可选属性。这个细节很重要:Safari和部分移动端浏览器对Wake Lock的支持并不一致,如果把类型写成必有属性,能力检测的分支就会被类型系统误判为多余代码,反而隐藏了运行时风险。
在Media Session场景下做状态同步时,推荐用一个可辨识联合来建模锁的三种状态:未请求、持有中、已释放。这样做的好处是,消费状态的代码必须通过类型收窄才能拿到Sentinel引用,编译期就能挡住“锁已释放还在调用release”这类低级错误:
type LockState =
| { status: 'idle' }
| { status: 'locked'; sentinel: WakeLockSentinel }
| { status: 'released' };
class ScreenLockManager {
private state: LockState = { status: 'idle' };
async lock(): Promise<boolean> {
const sentinel = await acquireScreenLock();
if (!sentinel) return false;
this.state = { status: 'locked', sentinel };
sentinel.addEventListener('release', () => {
this.state = { status: 'released' };
});
return true;
}
async unlock(): Promise<void> {
if (this.state.status === 'locked') {
await this.state.sentinel.release();
this.state = { status: 'released' };
}
}
get isActive(): boolean {
return this.state.status === 'locked';
}
}这个例子里的setWakeLockActive语义就体现在isActive这个状态查询上,它始终反映真实的锁状态,而不是简单地读取sentinel.released,因为后者在异常释放路径上可能更新滞后。
事件监听的类型收窄与竞态处理
Wake Lock最容易被忽视的一点是:页面切到后台时锁会被浏览器自动释放,回到前台不会自动恢复。所以完整的实现必须监听visibilitychange事件并重新请求锁。这时就会出现竞态问题——如果用户在极短时间内反复切换标签页,可能同时存在多个request()的Promise在飞行中。
处理思路是引入一个请求序号(或称epoch),只有最新一次请求的结果才允许写入状态,旧的结果直接丢弃。类型上可以给内部状态加一个token字段来配合判断:
type ManagedState =
| { status: 'idle'; token: 0 }
| { status: 'locked'; token: number; sentinel: WakeLockSentinel }
| { status: 'released'; token: number };
class ReentrantLockManager {
private state: ManagedState = { status: 'idle', token: 0 };
private nextToken = 1;
async relock(): Promise<void> {
const token = this.nextToken++;
const sentinel = await acquireScreenLock();
if (!sentinel || token !== this.nextToken - 1) {
// 已有更新的请求,本次结果作废
sentinel?.release();
return;
}
this.state = { status: 'locked', token, sentinel };
sentinel.addEventListener('release', () => {
if (this.state.status === 'locked' && this.state.token === token) {
this.state = { status: 'released', token };
}
});
}
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
new ReentrantLockManager().relock();
}
});另外要注意addEventListener('release', ...)的回调类型。如果你的tsconfig里的lib版本较老,WakeLockSentinel继承自EventTarget时事件回调参数会推断为宽泛的Event,此时可以自定义一个类型守卫函数,把事件对象的target收窄回WakeLockSentinel:
function isWakeLockSentinelEvent(e: Event): e is WakeLockSentinelEvent {
return e.target !== null && 'released' in (e.target as object);
}兼容性与降级方案的类型处理
最后一件事是降级。对不支持Wake Lock API的浏览器,常见的替代方案是播放一段无声视频来阻止息屏。为了不让两套逻辑的类型混在一起,可以抽象一个统一的接口:
interface ScreenKeepAwake {
activate(): Promise<boolean>;
deactivate(): Promise<void>;
readonly active: boolean;
readonly strategy: 'wake-lock' | 'fallback-video';
}
function createKeepAwake(): ScreenKeepAwake {
if (navigator.wakeLock) {
return new WakeLockAdapter();
}
return new SilentVideoAdapter();
}把策略名strategy写进类型里还有一个好处:日志和埋点代码可以直接依据这个字段区分上报来源,排查线上问题时能快速判断用户走的是哪条路径。
总结一下,Wake Lock的类型定义核心是三点:把navigator.wakeLock声明为可选属性以强制能力检测、用可辨识联合建模锁的状态机、在重入场景下用token机制防竞态。把这三点落实到位,屏幕唤醒锁的接入就能在TypeScript项目中既严谨又稳妥地跑起来。
TypeScriptWake Lock API类型定义修改时间:2026-09-16 20:04:46