在网页里根据周围光线自动切换暗黑模式,核心是要拿到环境亮度信号并驱动样式变更。单纯依赖用户手动切换已无法满足体验需求,而不同浏览器与设备提供的光线获取能力差异明显,需要多套方案组合兜底。

一、Ambient Light Sensor API 直接读取 lux 值
环境光传感器(Ambient Light Sensor)是最直接的方案,它通过传感器事件返回以勒克斯(lux)为单位的光线强度。当光照低于某个阈值时,我们就可以切换到暗黑模式。该接口基于 Generic Sensor API,需要在安全上下文(HTTPS)中并获用户授权后才能使用。
下面代码展示了如何监听光线变化并切换主题类:
// 检测是否支持 Ambient Light Sensor
if ('AmbientLightSensor' in window) {
try {
const sensor = new AmbientLightSensor();
// 光线变化时触发
sensor.onreading = () => {
const lux = sensor.illuminance;
// 低于 50 lux 视为暗环境
if (lux < 50) {
document.body.classList.add('dark-mode');
} else {
document.body.classList.remove('dark-mode');
}
};
sensor.onerror = (event) => {
console.warn('光线传感器出错:', event.error.name);
};
sensor.start();
} catch (err) {
console.warn('无法启动光线传感器:', err);
}
} else {
console.log('当前浏览器不支持 Ambient Light Sensor');
}
这种方案的优点是数据精确、实时性强,但缺点同样突出:桌面 Chrome 虽支持却常需开启实验特性,iOS Safari 完全不支持,且用户首次访问会弹出权限申请,容易引发反感。因此在生产环境它通常只作为增强能力,而非唯一依赖。
另外需要注意,传感器返回的 illuminance 可能有较大抖动,实际项目中应做滑动平均或防抖处理,避免频繁切换主题造成闪烁。可维护一个最近十次读数的队列,取中位数作为判定依据。
二、matchMedia 监听系统暗黑模式偏好
多数操作系统已在设置中提供暗黑模式开关,浏览器通过 prefers-color-scheme 媒体特性暴露该状态。使用 matchMedia 可以同步系统主题,这是目前兼容性最好、最省心的方案。
代码示例展示了如何监听系统切换并响应:
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
function applySystemTheme(e) {
if (e.matches) {
document.body.classList.add('dark-mode');
} else {
document.body.classList.remove('dark-mode');
}
}
// 初始执行一次
applySystemTheme(darkModeQuery);
// 系统主题变更时监听
darkModeQuery.addEventListener('change', applySystemTheme);
该方式不依赖任何硬件传感器,只要用户系统开了暗色,网页就跟随变化,几乎零成本。但它只能感知系统级偏好,无法识别用户身处明亮房间却开了暗色,或反之的场景,因此谈不上真正的光线感应。
为了兼顾灵活度,很多站点会把 matchMedia 作为默认策略,同时提供手动切换按钮,并将用户选择存入 localStorage,优先级高于系统设定。这样既不违背用户意愿,也保留了自动能力。
三、基于页面颜色采样与摄像头亮度估计
当设备没有光线传感器,又希望粗略估算环境光时,可以借助 getUserMedia 调用前置摄像头,抓取一帧画面计算平均亮度。虽然涉及摄像头权限,但在某些 kiosk 或教育网页中可行。
以下代码演示了通过视频帧计算灰度均值:
async function estimateLightFromCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const video = document.createElement('video');
video.srcObject = stream;
await video.play();
const canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
const ctx = canvas.getContext('2d');
// 绘制当前帧
ctx.drawImage(video, 0, 0, 64, 64);
const data = ctx.getImageData(0, 0, 64, 64).data;
let sum = 0;
for (let i = 0; i < data.length; i += 4) {
// 灰度加权
const gray = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
sum += gray;
}
const avg = sum / (data.length / 4);
stream.getTracks().forEach(t => t.stop());
return avg; // 0-255 之间
} catch (e) {
console.warn('摄像头取光失败', e);
return null;
}
}
这种思路把摄像头当光线收集器,能反映设备朝向方向的真实光照。但持续开摄像头耗电且侵犯隐私,一般只适合一次性估算或明确告知用户用途。
若采用此方案,建议降低采样分辨率、拉长采样间隔,并在页面关闭时彻底释放轨道。同时应作为后备,在支持 Ambient Light Sensor 时优先用传感器。
四、计时器结合时间段的伪光线策略
如果以上硬件或系统能力都不可用,可用本地时间近似推测光线:白天亮、夜晚暗。虽不精准,但实现简单,可作为最后兜底。
示例代码按小时切换:
function applyTimeBasedTheme() {
const hour = new Date().getHours();
// 早 7 点到晚 7 点视为白天
if (hour >= 7 && hour < 19) {
document.body.classList.remove('dark-mode');
} else {
document.body.classList.add('dark-mode');
}
}
applyTimeBasedTheme();
// 每半小时检查一次
setInterval(applyTimeBasedTheme, 30 * 60 * 1000);
该方案无任何权限要求,但明显忽略了阴天、室内照明等变量。它适合内容型博客或工具页,在用户未做选择且系统不支持时给个基本体验。
为避免与用户手动选择冲突,时间策略应只在 localStorage 中无记录时生效,一旦用户显式切换就写入标记并停止定时器。
五、多方案融合与降级架构
真实项目中,我们通常不会只选一种方案,而是按能力探测顺序堆叠:优先 Ambient Light Sensor,其次 matchMedia,再次摄像头估算,最后时间兜底,并始终允许手动覆盖。
下表列出各方案特点:
| 方案 | 精度 | 权限 | 兼容性 |
|---|---|---|---|
| Ambient Light Sensor | 高 | 需授权 | 部分浏览器 |
| matchMedia | 中(仅系统级) | 无 | 广泛 |
| 摄像头估算 | 较高 | 摄像头权限 | 依赖 HTTPS |
| 时间兜底 | 低 | 无 | 全部 |
融合时代码可先定义一个 setDarkMode 统一入口,再根据探测结果绑定对应监听器。手动按钮点击后写入 localStorage 的 theme 字段,初始化时优先读取,为空才走自动逻辑。
最后提醒,暗黑模式不仅是反色,还要注意对比度符合 WCAG、图片与阴影在暗底下的表现。光线感应只是触发手段,完整的主题系统需配套 CSS 变量与组件适配,才能带来真正舒适的浏览体验。
Ambient_Light_SensormatchMediadark_mode修改时间:2026-07-31 20:15:39