浏览器 Web Bluetooth API 使用方法
浏览器 Web Bluetooth API 的使用方法,涵盖核心概念如设备、服务、特征及 UUID,以及完整的通信流程。内容包括如何扫描并选择设备、连接 GATT 服务器、获取服务与特征、读写数据(包括通知模式)、数据转换方法(DataView 处理)、标准 UUID 参考、错误处理机制及重连策略。文章还提供了 JavaScript 类封装示例和 Vue 组件集成方案,帮助开发者实现网页与蓝牙设备的交互。

浏览器 Web Bluetooth API 的使用方法,涵盖核心概念如设备、服务、特征及 UUID,以及完整的通信流程。内容包括如何扫描并选择设备、连接 GATT 服务器、获取服务与特征、读写数据(包括通知模式)、数据转换方法(DataView 处理)、标准 UUID 参考、错误处理机制及重连策略。文章还提供了 JavaScript 类封装示例和 Vue 组件集成方案,帮助开发者实现网页与蓝牙设备的交互。

Web Bluetooth API 让网页应用可以与蓝牙设备通信。通过这个 API,你可以:
医疗设备、手环、手表、传感器、遥控器、音箱、灯等所有支持蓝牙的设备都可以通过这个 API 与网页应用通信。
| 浏览器 | 支持 | 最低版本 |
|---|---|---|
| Chrome/Edge | ✅ | 56+ / 79+ |
| Firefox | ⚠️ 需启用 | 98+ |
| Safari | ❌ | - |
物理设备(血压计、手环等)
↓ GATT 服务器(设备内的数据服务)
├─ Service(服务,定义功能)
│ ├─ Characteristic(特征,具体的数据)
│ └─ Characteristic
└─ Service
└─ Characteristic
| 名称 | 说明 | 类比 |
|---|---|---|
| Device | 蓝牙设备 | 一台手机 |
| Service | 功能模块 | 手机的相机模块 |
| Characteristic | 具体数据 | 相机拍摄的照片 |
| UUID | 全局唯一标识 | 身份证号 |
发送数据:Web App → 设备
↓ writeValue() 向设备发送命令
接收数据:设备 → Web App
↓ 两种方式:
1. startNotifications() 设备主动推送(推荐)
2. readValue() 手动读取(备用)
// 让用户选择要连接的设备
const device = await navigator.bluetooth.requestDevice({
// 按服务过滤(设备必须提供这些服务才会出现)
filters: [{ services: ['battery_service'] }], // 电池服务
// 可选:列出可能需要的其他服务
optionalServices: ['device_information']
});
console.log('选中设备:', device.name);
重点:
requestDevice() 会弹出浏览器对话框,让用户选择// 连接到 GATT 服务器
const gattServer = await device.gatt.connect();
console.log('✅ 已连接');
// 监听断开连接事件
device.addEventListener('gattserverdisconnected', () => {
console.log('❌ 设备已断开');
});
重点:
gatt.connect() 是异步的,需要等待// 从设备获取指定的服务
const service = await gattServer.getPrimaryService('battery_service');
console.log('✅ 获取服务成功');
重点:
// 从服务中获取特征
const characteristic = await service.getCharacteristic('battery_level');
console.log('✅ 获取特征成功');
重点:
// 方式 1:手动读取一次
const value = await characteristic.readValue();
// 转换为可读的格式
const battery = value.getUint8(0);
console.log('电池电量:', battery, '%');
重点:
readValue() 返回 DataView 对象getUint8()、getInt16() 等方法提取数据// 启用通知,让设备主动推送数据
await characteristic.startNotifications();
console.log('✅ 通知已启用,等待数据...');
// 监听数据变化
characteristic.addEventListener('characteristicvaluechanged', event => {
const value = event.target.value;
// 提取数据
const battery = value.getUint8(0);
console.log('电池电量变化:', battery, '%');
});
重点:
startNotifications() 让设备主动推送数据characteristicvaluechanged 事件// 构造要发送的数据
const command = new Uint8Array([
0x01, // 命令代码
0x02, // 参数 1
0x03 // 参数 2
]);
// 发送数据到设备
await characteristic.writeValue(command);
console.log('✅ 命令已发送');
重点:
Uint8Array 格式// 停止通知
await characteristic.stopNotifications();
// 断开连接
device.gatt.disconnect();
console.log('✅ 已清理资源');
class SimpleBluetoothClient {
constructor() {
this.device = null;
this.gattServer = null;
this.service = null;
this.characteristic = null;
}
/**
* 连接设备
* @param {string} serviceName - 服务名称(如 'battery_service')
* @param {string} characteristicName - 特征名称(如 'battery_level')
*/
async connect(serviceName, characteristicName) {
try {
// 1. 让用户选择设备
this.device = await navigator.bluetooth.requestDevice({
filters: [{ services: [serviceName] }],
optionalServices: []
});
console.log('✅ 已选择设备:', this.device.name);
// 2. 连接到设备
this.gattServer = await this.device.gatt.connect();
console.log('✅ GATT 连接成功');
// 3. 获取服务
this.service = await this.gattServer.getPrimaryService(serviceName);
console.log('✅ 获取服务成功');
// 4. 获取特征
this.characteristic = await this.service.getCharacteristic(characteristicName);
console.log('✅ 获取特征成功');
// 5. 监听断开事件
this.device.addEventListener('gattserverdisconnected', () => {
console.log('⚠️ 设备已断开');
this.device = null;
});
} catch (error) {
console.error('❌ 连接失败:', error);
throw error;
}
}
/**
* 启用通知
* @param {Function} callback - 收到数据时的回调
*/
async startListening(callback) {
try {
await this.characteristic.startNotifications();
console.log('✅ 已启用通知');
this.characteristic.addEventListener('characteristicvaluechanged', event => {
const value = event.target.value;
callback(value);
});
} catch (error) {
console.error('❌ 启用通知失败:', error);
throw error;
}
}
/**
* 发送命令
* @param {Uint8Array} data - 要发送的数据
*/
async sendCommand(data) {
try {
await this.characteristic.writeValue(data);
console.log('✅ 命令已发送');
} catch (error) {
console.error('❌ 发送失败:', error);
throw error;
}
}
/**
* 读取一次数据
*/
async readOnce() {
try {
const value = await this.characteristic.readValue();
console.log('✅ 数据已读取');
return value;
} catch (error) {
console.error('❌ 读取失败:', error);
throw error;
}
}
/**
* 断开连接
*/
disconnect() {
if (this.device && this.device.gatt.connected) {
this.device.gatt.disconnect();
console.log('✅ 已断开连接');
}
}
}
<template>
<div>
<!-- 状态 -->
<div :class="{ connected: isConnected }">
{{ isConnected ? '已连接' : '未连接' }}
</div>
<!-- 按钮 -->
<button @click="connectDevice" :disabled="isConnected"> 连接设备 </button>
<button @click="readData" :disabled="!isConnected"> 读取数据 </button>
<button @click="disconnect" :disabled="!isConnected"> 断开连接 </button>
<!-- 显示数据 -->
<div v-if="lastData">
<p>最后收到的数据:</p>
<code>{{ lastData }}</code>
</div>
</div>
</template>
<script>
import SimpleBluetoothClient from '@/utils/BluetoothClient';
export default {
data() {
return {
bluetooth: null,
isConnected: false,
lastData: null
};
},
methods: {
async connectDevice() {
try {
this.bluetooth = new SimpleBluetoothClient();
// 连接到设备
// 注:这里用的是标准的蓝牙服务 UUID
await this.bluetooth.connect(
'180a', // 设备信息服务
'2a29' // 制造商名称
);
this.isConnected = true;
// 启用通知
await this.bluetooth.startListening(value => {
this.lastData = this.formatData(value);
});
} catch (error) {
alert('连接失败:' + error.message);
}
},
async readData() {
try {
const value = await this.bluetooth.readOnce();
this.lastData = this.formatData(value);
} catch (error) {
alert('读取失败:' + error.message);
}
},
disconnect() {
this.bluetooth.disconnect();
this.isConnected = false;
this.lastData = null;
},
formatData(dataView) {
// 将 DataView 转换为十六进制字符串
const bytes = new Uint8Array(dataView.buffer);
return Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join(' ');
}
}
};
</script>
<style scoped>
.bluetooth-demo {
padding: 20px;
}
.status {
padding: 10px;
margin-bottom: 20px;
border-radius: 4px;
font-weight: bold;
color: white;
background: red;
}
.status.connected {
background: green;
}
button {
padding: 10px 20px;
margin-right: 10px;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.data {
margin-top: 20px;
padding: 10px;
background: #f0f0f0;
border-radius: 4px;
}
code {
font-family: monospace;
font-size: 12px;
}
</style>
// 假设接收到的数据
const value = dataView; // 类型为 DataView
// 提取单个字节(无符号)
const byte0 = value.getUint8(0); // 0-255
const byte1 = value.getUint8(1);
// 提取有符号字节
const signedByte = value.getInt8(0); // -128 到 127
// 提取 16 位整数
const int16 = value.getUint16(0, true); // true = 小端序
// 提取 32 位整数
const int32 = value.getUint32(0, true);
// 提取浮点数
const float = value.getFloat32(0, true);
const double = value.getFloat64(0, true);
// 创建一个 4 字节的命令
const command = new Uint8Array([
0x01, // 第 1 字节:命令 ID
0x02, // 第 2 字节:参数 1
0x03, // 第 3 字节:参数 2
0x04 // 第 4 字节:参数 3
]);
// 发送
await characteristic.writeValue(command);
// 数组转十六进制字符串
function arrayToHex(dataView) {
const bytes = new Uint8Array(dataView.buffer || dataView);
return Array.from(bytes).map(b => '0x' + b.toString(16).padStart(2, '0')).join(', ');
}
console.log(arrayToHex(value)); // 0x01, 0x02, 0x03, ...
// 十六进制字符串转数组
function hexToArray(hexString) {
const hex = hexString.replace(/\s+|0x/g, '');
const array = [];
for (let i = 0; i < hex.length; i += 2) {
array.push(parseInt(hex.substr(i, 2), 16));
}
return new Uint8Array(array);
}
const arr = hexToArray('01 02 03 04');
await characteristic.writeValue(arr);
这些是 Bluetooth SIG 定义的标准服务:
const STANDARD_SERVICES = {
'180a': '设备信息服务',
'180d': '心率服务',
'180b': '血压服务',
'180f': '电池服务',
'1800': '通用访问',
'1801': '通用属性'
};
const STANDARD_CHARACTERISTICS = {
'2a19': '电池电量',
'2a29': '制造商名称',
'2a24': '型号号码',
'2a25': '序列号码',
'2a37': '心率测量',
'2a35': '血压测量'
};
try {
await navigator.bluetooth.requestDevice(...);
} catch (error) {
if (error.name === 'NotFoundError') {
// 用户取消了选择
console.log('用户取消了设备选择');
} else if (error.name === 'NotSupportedError') {
// 浏览器不支持 Web Bluetooth
console.log('浏览器不支持 Web Bluetooth API');
} else if (error.name === 'SecurityError') {
// 需要 HTTPS 或 localhost
console.log('需要在 HTTPS 或 localhost 上运行');
} else if (error.name === 'NetworkError') {
// 通信失败
console.log('蓝牙通信失败,设备可能已离线');
} else {
console.error('未知错误:', error.name);
}
}
async function connectWithRetry(maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
console.log(`第 ${i + 1}/${maxRetries} 次连接尝试...`);
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['battery_service'] }]
});
const gattServer = await device.gatt.connect();
console.log('✅ 连接成功');
return gattServer;
} catch (error) {
console.warn(`第 ${i + 1} 次尝试失败:`, error.message);
if (i < maxRetries - 1) {
// 等待 1 秒后重试
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
throw new Error('连接失败,已达到最大重试次数');
}
// 将 DataView 转为可读的格式
function debugData(dataView) {
const bytes = new Uint8Array(dataView.buffer || dataView);
const hexArray = Array.from(bytes).map(b => b.toString(16).padStart(2, '0').toUpperCase());
console.log('Raw hex:', hexArray.join(' '));
console.log('Decimal:', Array.from(bytes).join(', '));
console.log('Binary:', Array.from(bytes).map(b => b.toString(2).padStart(8, '0')).join(' '));
}
// 使用
characteristic.addEventListener('characteristicvaluechanged', event => {
debugData(event.target.value);
});
// 在 Chrome 控制台运行:
navigator.bluetooth.getDevices().then(devices => {
console.table(devices.map(d => ({ name: d.name, id: d.id, connected: d.gatt.connected })));
});
// 设备事件
device.addEventListener('gattserverdisconnected', () => {
console.log('❌ 设备已断开');
});
// 特征值变化
characteristic.addEventListener('characteristicvaluechanged', event => {
console.log('📥 数据变化:', event.target.value);
});
requestDevice() 让用户选择gatt.connect()getPrimaryService()getCharacteristic()startNotifications() 或 readValue()getUint8()、getInt16() 等正确提取数据Uint8Array 构造发送的数据stopNotifications()disconnect()// 1. 扫描选择
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['SERVICE_UUID'] }]
});
// 2. 连接
const gattServer = await device.gatt.connect();
// 3. 获取服务
const service = await gattServer.getPrimaryService('SERVICE_UUID');
// 4. 获取特征
const char = await service.getCharacteristic('CHARACTERISTIC_UUID');
// 5. 读取或监听
// 方式 A:启用通知
await char.startNotifications();
char.addEventListener('characteristicvaluechanged', event => {
const data = event.target.value;
});
// 方式 B:手动读取
const data = await char.readValue();
// 6. 写入
await char.writeValue(new Uint8Array([0x01, 0x02]));
// 7. 清理
await char.stopNotifications();
device.gatt.disconnect();
// DataView 方法
value.getUint8(index); // 无符号字节
value.getInt8(index); // 有符号字节
value.getUint16(index, true); // 无符号 16 位
value.getInt16(index, true); // 有符号 16 位
value.getUint32(index, true); // 无符号 32 位
value.getInt32(index, true); // 有符号 32 位
value.getFloat32(index, true); // 浮点数 32 位
value.getFloat64(index, true); // 浮点数 64 位
Web Bluetooth API 的核心就是这 7 个步骤:
requestDevice()gatt.connect()getPrimaryService()getCharacteristic()startNotifications() 或 readValue()writeValue() 或监听事件disconnect()掌握这 7 个 API,你就可以与任何蓝牙设备通信!

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online