当你的智能手表、体脂秤或温度传感器都在用蓝牙传输数据时,一个自然的问题是:浏览器能直接读取这些数据吗? Web Bluetooth API 给出了肯定的答案——它让 Web 应用可以通过 Bluetooth Low Energy(BLE)协议直接与附近的蓝牙设备通信,无需安装任何原生 App。根据 Chrome Platform Status 的数据,Web Bluetooth API 的使用量在 2025-2026 年间增长了 180%,被广泛应用于医疗健康、工业 IoT 和智能硬件配置等场景。如果你正在构建需要与硬件交互的 Web 应用,这篇文章会帮你从零掌握 Web Bluetooth 的核心能力。
🔗 一、Web Bluetooth 核心原理与浏览器支持
1.1 什么是 BLE 与 GATT
Web Bluetooth API 基于 Bluetooth Low Energy(BLE,蓝牙低功耗)协议,这是蓝牙 4.0 引入的低功耗通信标准,专为传感器和可穿戴设备设计。与经典蓝牙(如音频传输)不同,BLE 的核心数据模型是 GATT(Generic Attribute Profile)——一个基于「服务(Service)→ 特征(Characteristic)→ 值(Value)」三层结构的标准化数据协议。
📌 **记住:**Web Bluetooth 只支持 BLE(低功耗蓝牙),不支持经典蓝牙(Classic Bluetooth)。这意味着它不能用于音频流传输或文件传输,只能用于小数据量的传感器通信。
GATT 的层级结构可以用一个类比理解:
| 层级 | 类比 | 说明 |
|---|---|---|
| Service(服务) | 文件夹 | 一组相关功能的集合,如「心率服务」 |
| Characteristic(特征) | 文件 | 一个具体的数据点,如「心率测量值」 |
| Descriptor(描述符) | 文件元数据 | 描述特征的额外信息,如单位、格式 |
| Value(值) | 文件内容 | 实际的数据值 |
每个 Service 和 Characteristic 都有一个标准化的 UUID(128 位)来标识。例如:
- 心率服务:
0000180d-0000-1000-8000-00805f9b34fb - 电池服务:
0000180f-0000-1000-8000-00805f9b34fb
1.2 浏览器支持现状
截至 2026 年 6 月,Web Bluetooth API 的浏览器支持情况:
| 浏览器 | 支持版本 | 状态 |
|---|---|---|
| Chrome | 56+ | ✅ 完整支持 |
| Edge | 79+ | ✅ 完整支持(基于 Chromium) |
| Opera | 43+ | ✅ 完整支持 |
| Firefox | ❌ | ⚠️ 实验性标志,未正式发布 |
| Safari | ❌ | ❌ 未实现,无明确计划 |
| Android Chrome | 56+ | ✅ 完整支持 |
| iOS Safari | ❌ | ❌ 不支持 |
⚠️ **警告:**Safari 和 iOS 不支持 Web Bluetooth 是最大的部署障碍。如果你的应用面向 C 端用户,需要提供降级方案(如二维码配对 + WebSocket 中转)。对于 B 端场景(如工厂巡检用 Chromebook),覆盖率通常不是问题。
1.3 安全要求
Web Bluetooth API 有严格的安全限制:
- ✅ 必须在 HTTPS 环境下使用(
localhost除外) - ✅ 必须由 用户手势 触发(如点击按钮),不能自动扫描
- ✅ 设备选择对话框由浏览器控制,JavaScript 无法绕过
- ✅ 每次连接都需要用户明确授权
// ✅ 检测 Web Bluetooth 是否可用
function isWebBluetoothSupported() {
return 'bluetooth' in navigator;
}
if (!isWebBluetoothSupported()) {
console.error('此浏览器不支持 Web Bluetooth API');
// 提供降级方案:引导用户使用支持的浏览器
}
🚀 二、实战:设备发现与 GATT 通信
2.1 设备发现与连接
Web Bluetooth 的核心入口是 navigator.bluetooth.requestDevice(),它会弹出浏览器原生的设备选择对话框:
// 连接心率监测设备
async function connectHeartRateMonitor() {
try {
// requestDevice 接受一个过滤器,指定要搜索的服务 UUID
const device = await navigator.bluetooth.requestDevice({
filters: [
{ services: ['heart_rate'] } // 标准心率服务 UUID
],
// 或者用 acceptAllDevices 扫描所有设备(需要指定 optionalServices)
// acceptAllDevices: true,
// optionalServices: ['heart_rate', 'battery_service']
});
console.log(`已选择设备: ${device.name}`);
console.log(`设备 ID: ${device.id}`);
// 监听设备断开事件
device.addEventListener('gattserverdisconnected', onDisconnected);
// 连接 GATT 服务器
const server = await device.gatt.connect();
console.log('GATT 服务器已连接');
return server;
} catch (error) {
if (error.name === 'NotFoundError') {
console.log('用户取消了设备选择');
} else if (error.name === 'SecurityError') {
console.log('安全错误:请确保在 HTTPS 环境下使用');
} else {
console.error('连接失败:', error);
}
throw error;
}
}
function onDisconnected(event) {
const device = event.target;
console.log(`设备 ${device.name} 已断开`);
// 实现自动重连逻辑
setTimeout(() => reconnect(device), 3000);
}
2.2 读取特征值
连接成功后,可以通过 GATT 层级读取设备数据:
// 读取心率测量值
async function readHeartRate(gattServer) {
// 第一步:获取心率服务
const service = await gattServer.getPrimaryService('heart_rate');
console.log('已获取心率服务');
// 第二步:获取心率测量特征
const characteristic = await service.getCharacteristic('heart_rate_measurement');
// 第三步:读取特征值(一次性读取)
const value = await characteristic.readValue();
// 解析心率数据(BLE 协议规定的格式)
const flags = value.getUint8(0);
const is16Bit = (flags & 0x01) !== 0; // 第 0 位:心率值格式
const heartRate = is16Bit
? value.getUint16(1, true) // 小端序
: value.getUint8(1);
console.log(`当前心率: ${heartRate} BPM`);
return heartRate;
}
2.3 实时通知(Notifications)
对于持续变化的数据(如心率),使用 通知(Notifications) 比轮询更高效:
// 订阅心率实时通知
async function subscribeHeartRate(gattServer) {
const service = await gattServer.getPrimaryService('heart_rate');
const characteristic = await service.getCharacteristic('heart_rate_measurement');
// 检查是否支持通知
if (!characteristic.properties.notify) {
throw new Error('此特征不支持通知');
}
// 启动通知
await characteristic.startNotifications();
// 监听值变化事件
characteristic.addEventListener('characteristicvaluechanged', (event) => {
const value = event.target.value;
const flags = value.getUint8(0);
const is16Bit = (flags & 0x01) !== 0;
const heartRate = is16Bit
? value.getUint16(1, true)
: value.getUint8(1);
console.log(`心率更新: ${heartRate} BPM`);
// 更新 UI
updateHeartRateDisplay(heartRate);
});
console.log('心率通知已启动');
}
// 停止通知(清理资源)
async function stopHeartRateNotifications(gattServer) {
const service = await gattServer.getPrimaryService('heart_rate');
const characteristic = await service.getCharacteristic('heart_rate_measurement');
await characteristic.stopNotifications();
characteristic.removeEventListener('characteristicvaluechanged', handleHeartRate);
console.log('心率通知已停止');
}
💡 **提示:**BLE 通知的更新频率取决于设备硬件。大多数心率监测器的更新频率为 1Hz(每秒一次),而某些工业传感器可能为 10-100Hz。在 UI 层做节流(throttle)可以避免过度渲染。
2.4 写入特征值
某些设备支持通过写入特征值来控制设备行为:
// 向 BLE 设备发送控制命令
async function sendCommand(gattServer, serviceUuid, characteristicUuid, data) {
const service = await gattServer.getPrimaryService(serviceUuid);
const characteristic = await service.getCharacteristic(characteristicUuid);
// 检查是否支持写入
if (!characteristic.properties.write && !characteristic.properties.writeWithoutResponse) {
throw new Error('此特征不支持写入');
}
// 创建写入数据(Uint8Array)
const encoder = new TextEncoder();
const command = encoder.encode(data);
// writeValue:需要设备确认
// writeValueWithoutResponse:不需要确认,更快但不可靠
if (characteristic.properties.write) {
await characteristic.writeValue(command);
console.log('命令已发送并确认');
} else {
await characteristic.writeValueWithoutResponse(command);
console.log('命令已发送(无确认)');
}
}
// 示例:控制 BLE LED 灯的颜色
async function setLEDColor(gattServer, r, g, b) {
const service = await gattServer.getPrimaryService('0000ffe0-0000-1000-8000-00805f9b34fb');
const characteristic = await service.getCharacteristic('0000ffe1-0000-1000-8000-00805f9b34fb');
// 构造 RGB 颜色命令
const colorData = new Uint8Array([0xAA, r, g, b, 0xFF]);
await characteristic.writeValue(colorData);
}
💡 三、真实场景:构建智能设备数据采集系统
3.1 温湿度传感器数据采集
以下是一个完整的温湿度传感器数据采集示例,展示如何将 Web Bluetooth 集成到实际应用中:
// 温湿度传感器数据采集器
class BLETempHumidityCollector {
constructor() {
this.device = null;
this.server = null;
this.dataBuffer = [];
this.isConnected = false;
}
async connect() {
this.device = await navigator.bluetooth.requestDevice({
filters: [
// 自定义服务 UUID(根据实际设备修改)
{ services: ['environmental_sensing'] }
]
});
this.device.addEventListener('gattserverdisconnected', () => {
this.isConnected = false;
console.log('传感器断开,尝试重连...');
this.autoReconnect();
});
this.server = await this.device.gatt.connect();
this.isConnected = true;
console.log(`已连接传感器: ${this.device.name}`);
}
async startCollecting(intervalMs = 5000) {
if (!this.isConnected) throw new Error('设备未连接');
const service = await this.server.getPrimaryService('environmental_sensing');
// 温度特征
const tempChar = await service.getCharacteristic('temperature');
// 湿度特征
const humidityChar = await service.getCharacteristic('humidity');
// 定时读取(适用于不支持通知的设备)
this.collectionInterval = setInterval(async () => {
try {
const tempValue = await tempChar.readValue();
const humidityValue = await humidityChar.readValue();
// 解析温度(IEEE 11073 16-bit SFLOAT 格式)
const temperature = tempValue.getInt16(0, true) / 100;
// 解析湿度(百分比)
const humidity = humidityValue.getUint16(0, true) / 100;
const dataPoint = {
timestamp: Date.now(),
temperature: temperature.toFixed(1),
humidity: humidity.toFixed(1),
deviceId: this.device.id
};
this.dataBuffer.push(dataPoint);
this.onDataPoint(dataPoint);
} catch (err) {
console.error('数据读取失败:', err);
}
}, intervalMs);
}
onDataPoint(data) {
// 可被覆盖的回调函数
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 温度: ${data.temperature}°C, 湿度: ${data.humidity}%`);
}
stopCollecting() {
if (this.collectionInterval) {
clearInterval(this.collectionInterval);
this.collectionInterval = null;
}
}
async exportData(format = 'json') {
if (format === 'json') {
return JSON.stringify(this.dataBuffer, null, 2);
}
if (format === 'csv') {
const header = 'timestamp,temperature,humidity,deviceId';
const rows = this.dataBuffer.map(d =>
`${d.timestamp},${d.temperature},${d.humidity},${d.deviceId}`
);
return [header, ...rows].join('\n');
}
}
async disconnect() {
this.stopCollecting();
if (this.server?.connected) {
this.server.disconnect();
}
this.isConnected = false;
}
async autoReconnect() {
if (!this.device) return;
let retries = 5;
while (retries > 0 && !this.isConnected) {
try {
await new Promise(r => setTimeout(r, 2000));
this.server = await this.device.gatt.connect();
this.isConnected = true;
console.log('重连成功');
return;
} catch {
retries--;
console.log(`重连失败,剩余尝试: ${retries}`);
}
}
}
}
// 使用示例
const collector = new BLETempHumidityCollector();
collector.onDataPoint = (data) => {
document.getElementById('temp').textContent = `${data.temperature}°C`;
document.getElementById('humidity').textContent = `${data.humidity}%`;
};
document.getElementById('connectBtn').addEventListener('click', async () => {
await collector.connect();
await collector.startCollecting(3000);
});
3.2 电池电量监控
// 读取 BLE 设备电池电量
async function monitorBatteryLevel(gattServer) {
const service = await gattServer.getPrimaryService('battery_service');
const characteristic = await service.getCharacteristic('battery_level');
// 初始读取
const initialValue = await characteristic.readValue();
console.log(`电池电量: ${initialValue.getUint8(0)}%`);
// 订阅电量变化通知
await characteristic.startNotifications();
characteristic.addEventListener('characteristicvaluechanged', (event) => {
const level = event.target.value.getUint8(0);
console.log(`电量更新: ${level}%`);
if (level < 10) {
showLowBatteryWarning(level);
}
});
}
⚠️ 四、避坑指南与最佳实践
4.1 常见问题排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
requestDevice 无反应 |
蓝牙未开启或设备不在范围内 | 检查系统蓝牙状态,提示用户靠近设备 |
| 连接后无法发现服务 | optionalServices 未配置 |
在 requestDevice 中声明所有需要的服务 UUID |
| 读取数据全是 0 | 设备未就绪或特征不支持读取 | 先订阅通知,等待设备主动推送数据 |
| 频繁断连 | 信号弱或设备进入省电模式 | 实现自动重连 + 指数退避策略 |
| iOS 无法使用 | Safari 不支持 Web Bluetooth | 提供替代方案(原生 App 或 WebSocket 中转) |
⚠️ **警告:**在
requestDevice的filters中使用的服务 UUID 必须与设备实际广播的服务 UUID 完全匹配。如果你不确定设备的 UUID,可以先用acceptAllDevices: true扫描所有设备,然后通过optionalServices声明你要访问的服务。
4.2 性能优化建议
// ❌ 避免:频繁读取导致设备响应超时
setInterval(async () => {
const value = await characteristic.readValue(); // 每 100ms 读一次,设备可能来不及响应
}, 100);
// ✅ 推荐:使用通知模式,由设备主动推送
await characteristic.startNotifications();
characteristic.addEventListener('characteristicvaluechanged', handler);
// ✅ 如果必须轮询,设置合理间隔(>= 1 秒)
setInterval(async () => {
try {
const value = await characteristic.readValue();
handleValue(value);
} catch (err) {
// BLE 读取可能超时,需要容错
console.warn('读取超时,跳过本次');
}
}, 1000);
4.3 安全最佳实践
- ✅ 输入验证:BLE 设备返回的数据可能被篡改,永远不要信任未验证的设备数据
- ✅ 最小权限:只请求必需的服务 UUID,不要用
acceptAllDevices扫描所有设备 - ✅ 加密通信:对于敏感数据,确保使用 BLE 的加密连接(LE Secure Connections)
- ✅ 设备白名单:在生产环境中,根据设备 ID 或名称做白名单校验
- ❌ 避免自动连接:始终需要用户手势触发,不要尝试绕过浏览器的安全限制
📊 五、Web Bluetooth vs 其他方案对比
| 方案 | 覆盖平台 | 开发成本 | 离线能力 | 硬件访问 | 分发难度 |
|---|---|---|---|---|---|
| Web Bluetooth | Chrome/Edge | 低 | 有限 | BLE | 极低(URL) |
| 原生 App (Swift/Kotlin) | 全平台 | 高 | 完整 | 完整 | 中(应用商店) |
| PWA + BLE | Chrome/Edge | 中 | 完整 | BLE | 低 |
| Electron + BLE | 全桌面 | 中 | 完整 | 完整 | 中 |
| Capacitor BLE | 全平台 | 中 | 完整 | 完整 | 中 |
⚡ 关键结论:Web Bluetooth 最大的优势是零安装、零分发——用户打开浏览器就能用。对于 B 端场景(如设备配置工具、工厂数据采集),这是巨大的体验优势。但如果你需要支持 iOS 用户或需要经典蓝牙功能,原生 App 仍然是唯一选择。
🔧 六、总结与相关工具
Web Bluetooth API 为前端开发者打开了一扇通往硬件世界的大门。虽然浏览器支持仍有局限(尤其是 Safari 的缺席),但在 Chrome 生态覆盖的场景中——工厂巡检、医疗数据采集、IoT 设备配置——它已经是零成本、高效率的最佳方案。
核心要点:
- ✅ Web Bluetooth 基于 BLE/GATT 协议,只支持低功耗蓝牙
- ✅ 使用通知(Notifications)而非轮询来获取实时数据
- ✅ 必须处理断连重连——BLE 设备的连接不稳定是常态
- ⚠️ iOS/Safari 不支持是最大限制,需要降级方案
- ❌ 不要尝试自动连接或绕过浏览器安全限制
相关工具与资源:
- 🔧 Web Bluetooth API 文档 — Google 官方指南
- 🔧 noble — Node.js BLE 库,用于后端蓝牙通信
- 🔧 Web Bluetooth Developer Tools — Chrome 扩展,调试 BLE 通信
- 🔧 Nordic nRF Connect — 移动端 BLE 调试工具,用于确认设备 UUID
- 🔧 jsjson.com JSON 格式化工具 — 格式化 BLE 设备返回的 JSON 数据