Web Bluetooth API 实战:浏览器直连蓝牙设备的完整指南

深入解析 Web Bluetooth API 核心原理与实战用法,涵盖设备发现、GATT 服务读写、心率监测、IoT 数据采集等真实场景,附完整代码示例与安全最佳实践。

前端开发 2026-06-01 16 分钟

当你的智能手表、体脂秤或温度传感器都在用蓝牙传输数据时,一个自然的问题是:浏览器能直接读取这些数据吗? 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 中转)

⚠️ **警告:**在 requestDevicefilters 中使用的服务 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 设备配置——它已经是零成本、高效率的最佳方案。

核心要点:

  1. ✅ Web Bluetooth 基于 BLE/GATT 协议,只支持低功耗蓝牙
  2. ✅ 使用通知(Notifications)而非轮询来获取实时数据
  3. ✅ 必须处理断连重连——BLE 设备的连接不稳定是常态
  4. ⚠️ iOS/Safari 不支持是最大限制,需要降级方案
  5. ❌ 不要尝试自动连接或绕过浏览器安全限制

相关工具与资源:

📚 相关文章