Vue.js 集成 Web Bluetooth/NFC API:响应式设备连接与状态管理
大家好!今天我们来深入探讨如何在 Vue.js 应用中集成 Web Bluetooth 和 Web NFC API,并巧妙地将设备连接状态和数据变化纳入 Vue 的响应式依赖图中,从而构建更流畅、更直观的用户体验。
Web Bluetooth API 概述
Web Bluetooth API 允许网页直接与用户附近的蓝牙设备进行通信。它提供了扫描设备、连接设备、读写 GATT 特性等功能,为 Web 应用带来了与物联网设备交互的可能性。
核心概念:
- BluetoothDevice: 代表一个蓝牙设备。
- BluetoothRemoteGATTServer: 代表设备上的 GATT 服务器。
- BluetoothService: GATT 服务器提供的服务,例如心率监测服务。
- BluetoothCharacteristic: 服务中的特性,代表数据的具体属性,例如心率值。
- GATT (Generic Attribute Profile): 定义了蓝牙设备之间如何交换数据的协议。
基本流程:
- 请求设备: 使用
navigator.bluetooth.requestDevice()提示用户选择要连接的蓝牙设备。 - 连接 GATT 服务器: 获取
BluetoothDevice后,调用device.gatt.connect()连接 GATT 服务器。 - 获取服务和特性: 从 GATT 服务器获取所需的服务和特性。
- 读写特性: 使用
characteristic.readValue()和characteristic.writeValue()读写特性值。 - 监听特性变化: 使用
characteristic.startNotifications()监听特性值的变化。
Web NFC API 概述
Web NFC API 允许网页读写 NFC (Near Field Communication) 标签。它可以用于读取标签中的数据,例如 URL、文本或自定义数据,也可以将数据写入标签。
核心概念:
- NDEF (NFC Data Exchange Format): 用于在 NFC 标签上存储数据的标准格式。
- NDEFMessage: 包含一个或多个 NDEF 记录的消息。
- NDEFRecord: 包含特定类型的数据的记录,例如 URL、文本或 MIME 类型的数据。
基本流程:
- 检查支持性: 使用
NDEFReader构造函数检查浏览器是否支持 Web NFC API。 - 监听扫描事件: 调用
reader.scan()启动扫描,并监听reading事件,该事件在检测到 NFC 标签时触发。 - 读取标签数据: 在
reading事件处理程序中,访问event.message获取NDEFMessage对象,并从中提取NDEFRecord。 - 写入标签数据: 创建
NDEFMessage和NDEFRecord对象,然后调用reader.write()将数据写入标签。
Vue.js 集成策略:响应式连接与状态管理
在 Vue.js 中集成这两个 API 的关键在于如何将异步操作和状态变化融入 Vue 的响应式系统中。我们可以采用以下策略:
- 使用
ref创建响应式状态: 使用ref创建响应式变量来存储设备连接状态、数据和错误信息。 - 使用
reactive创建响应式对象: 使用reactive创建响应式对象来管理更复杂的状态,例如设备信息和服务/特性列表。 - 使用
computed派生计算属性: 使用computed创建计算属性来根据响应式状态派生出新的状态,例如设备是否已连接。 - 在组合式函数中封装 API 调用: 创建组合式函数来封装 Web Bluetooth 和 Web NFC API 的调用,并将响应式状态作为返回值。
- 使用
watch监听状态变化: 使用watch监听状态变化,并在状态变化时执行副作用,例如更新 UI 或发送数据到服务器。 - 错误处理和用户反馈: 完善的错误处理机制是关键。使用
try...catch块捕获 API 调用中的错误,并使用响应式状态将错误信息显示给用户。
Web Bluetooth 集成示例
<template>
<div>
<button @click="connect" :disabled="isConnected">
{{ isConnected ? '已连接' : '连接蓝牙设备' }}
</button>
<p v-if="deviceName">设备名称: {{ deviceName }}</p>
<p v-if="heartRate">心率: {{ heartRate }} bpm</p>
<p v-if="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue';
const isConnected = ref(false);
const deviceName = ref('');
const heartRate = ref(null);
const error = ref('');
let bluetoothDevice = null;
let heartRateCharacteristic = null;
const HEART_RATE_SERVICE_UUID = 'heart_rate';
const HEART_RATE_MEASUREMENT_CHAR_UUID = 'heart_rate_measurement';
async function connect() {
try {
bluetoothDevice = await navigator.bluetooth.requestDevice({
filters: [{ services: [HEART_RATE_SERVICE_UUID] }],
optionalServices: [HEART_RATE_SERVICE_UUID]
});
deviceName.value = bluetoothDevice.name;
bluetoothDevice.addEventListener('gattserverdisconnected', onDisconnected);
const server = await bluetoothDevice.gatt.connect();
const service = await server.getPrimaryService(HEART_RATE_SERVICE_UUID);
heartRateCharacteristic = await service.getCharacteristic(HEART_RATE_MEASUREMENT_CHAR_UUID);
await heartRateCharacteristic.startNotifications();
heartRateCharacteristic.addEventListener('characteristicvaluechanged', handleHeartRateChanged);
isConnected.value = true;
error.value = '';
} catch (e) {
error.value = e.message;
console.error("连接失败:", e);
}
}
function handleHeartRateChanged(event) {
const value = event.target.value;
// 心率格式通常是 Uint8 或 Uint16
heartRate.value = value.getUint8(1); // 假设心率值在第二个字节
}
function onDisconnected() {
console.log('蓝牙设备已断开连接.');
isConnected.value = false;
deviceName.value = '';
heartRate.value = null;
error.value = '';
// 清理变量
bluetoothDevice = null;
heartRateCharacteristic = null;
}
onMounted(() => {
// 检查浏览器是否支持 Web Bluetooth
if (!navigator.bluetooth) {
error.value = '您的浏览器不支持 Web Bluetooth API。';
}
});
</script>
代码解释:
isConnected,deviceName,heartRate,error使用ref创建,用于存储连接状态、设备名称、心率值和错误信息。connect()函数处理设备连接逻辑。handleHeartRateChanged()函数处理心率值变化事件,并将心率值更新到heartRate响应式变量。onDisconnected()函数处理设备断开连接事件,重置状态。onMounted()钩子函数检查浏览器是否支持 Web Bluetooth API。HEART_RATE_SERVICE_UUID和HEART_RATE_MEASUREMENT_CHAR_UUID是心率服务的 UUID。
关键点:
- 连接状态 (
isConnected) 和心率值 (heartRate) 都是响应式变量,当它们的值发生变化时,Vue 会自动更新 UI。 - 错误信息 (
error) 也存储在响应式变量中,方便在 UI 中显示错误信息。 onDisconnected事件监听器确保在设备断开连接时,状态得到正确重置。
Web NFC 集成示例
<template>
<div>
<button @click="scanNFC" :disabled="isScanning">
{{ isScanning ? '扫描中...' : '扫描 NFC 标签' }}
</button>
<p v-if="nfcData">NFC 数据: {{ nfcData }}</p>
<p v-if="nfcError">{{ nfcError }}</p>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const isScanning = ref(false);
const nfcData = ref('');
const nfcError = ref('');
let ndefReader = null;
async function scanNFC() {
isScanning.value = true;
nfcData.value = '';
nfcError.value = '';
try {
ndefReader = new NDEFReader();
await ndefReader.scan();
ndefReader.addEventListener('reading', ({ message }) => {
for (const record of message.records) {
if (record.recordType === "text") {
const textDecoder = new TextDecoder('utf-8');
nfcData.value = textDecoder.decode(record.data);
break; // 找到第一个文本记录就停止
} else if (record.recordType === "url") {
nfcData.value = record.data; // URL可以直接显示
break; // 找到第一个URL记录就停止
} else {
nfcData.value = "未知数据类型: " + record.recordType;
}
}
});
ndefReader.addEventListener('readingerror', () => {
nfcError.value = '读取 NFC 标签时发生错误。';
isScanning.value = false;
});
} catch (error) {
nfcError.value = error.message || '发生未知错误。';
isScanning.value = false;
} finally {
// 确保在完成或出错后停止扫描
setTimeout(() => {
isScanning.value = false; // 模拟扫描超时
if (ndefReader) {
ndefReader.abort();
ndefReader = null; // 清理引用,防止内存泄漏
}
}, 10000); // 10秒超时
}
}
onMounted(() => {
if (!('NDEFReader' in window)) {
nfcError.value = '您的浏览器不支持 Web NFC API。';
}
});
</script>
代码解释:
isScanning,nfcData,nfcError使用ref创建,用于存储扫描状态、NFC 数据和错误信息。scanNFC()函数处理 NFC 标签扫描逻辑。- 在
reading事件处理程序中,提取 NFC 标签中的数据,并将其更新到nfcData响应式变量。 readingerror事件监听器处理读取错误。onMounted()钩子函数检查浏览器是否支持 Web NFC API。- 使用
setTimeout模拟扫描超时,避免长时间扫描。 - 使用
finally块确保扫描完成后停止扫描,即使发生错误。 - 调用
ndefReader.abort()停止扫描,并清理ndefReader引用,防止内存泄漏。
关键点:
- 扫描状态 (
isScanning) 和 NFC 数据 (nfcData) 都是响应式变量,当它们的值发生变化时,Vue 会自动更新 UI。 - 错误信息 (
nfcError) 也存储在响应式变量中,方便在 UI 中显示错误信息。 - 超时机制和
abort()方法确保扫描操作不会无限期运行。
组合式函数封装
为了提高代码的可重用性和可维护性,可以将 Web Bluetooth 和 Web NFC API 的调用封装到组合式函数中。
Web Bluetooth 组合式函数:
// useBluetooth.js
import { ref, onUnmounted } from 'vue';
export function useBluetooth(serviceUUID, characteristicUUID) {
const isConnected = ref(false);
const data = ref(null);
const error = ref('');
let bluetoothDevice = null;
let characteristic = null;
async function connect() {
try {
bluetoothDevice = await navigator.bluetooth.requestDevice({
filters: [{ services: [serviceUUID] }],
optionalServices: [serviceUUID]
});
bluetoothDevice.addEventListener('gattserverdisconnected', onDisconnected);
const server = await bluetoothDevice.gatt.connect();
const service = await server.getPrimaryService(serviceUUID);
characteristic = await service.getCharacteristic(characteristicUUID);
await characteristic.startNotifications();
characteristic.addEventListener('characteristicvaluechanged', handleValueChanged);
isConnected.value = true;
error.value = '';
} catch (e) {
error.value = e.message;
console.error(e);
}
}
function handleValueChanged(event) {
data.value = event.target.value.getUint8(0); // 假设数据是 Uint8
}
function onDisconnected() {
isConnected.value = false;
data.value = null;
error.value = '';
}
onUnmounted(() => {
if (bluetoothDevice) {
bluetoothDevice.removeEventListener('gattserverdisconnected', onDisconnected);
bluetoothDevice = null;
}
if (characteristic) {
characteristic.removeEventListener('characteristicvaluechanged', handleValueChanged);
characteristic = null;
}
});
return {
isConnected,
data,
error,
connect
};
}
在组件中使用:
<template>
<div>
<button @click="bluetooth.connect" :disabled="bluetooth.isConnected">
{{ bluetooth.isConnected ? '已连接' : '连接蓝牙设备' }}
</button>
<p v-if="bluetooth.data">数据: {{ bluetooth.data }}</p>
<p v-if="bluetooth.error">{{ bluetooth.error }}</p>
</div>
</template>
<script setup>
import { useBluetooth } from './useBluetooth';
const bluetooth = useBluetooth('heart_rate', 'heart_rate_measurement');
</script>
Web NFC 组合式函数:
// useNFC.js
import { ref, onUnmounted } from 'vue';
export function useNFC() {
const isScanning = ref(false);
const data = ref('');
const error = ref('');
let ndefReader = null;
async function scan() {
isScanning.value = true;
data.value = '';
error.value = '';
try {
ndefReader = new NDEFReader();
await ndefReader.scan();
ndefReader.addEventListener('reading', ({ message }) => {
for (const record of message.records) {
if (record.recordType === "text") {
const textDecoder = new TextDecoder('utf-8');
data.value = textDecoder.decode(record.data);
break;
} else if (record.recordType === "url") {
data.value = record.data;
break;
} else {
data.value = "未知数据类型: " + record.recordType;
}
}
});
ndefReader.addEventListener('readingerror', () => {
error.value = '读取 NFC 标签时发生错误。';
isScanning.value = false;
});
} catch (e) {
error.value = e.message;
} finally {
setTimeout(() => {
isScanning.value = false;
if (ndefReader) {
ndefReader.abort();
ndefReader = null;
}
}, 10000);
}
}
onUnmounted(() => {
if (ndefReader) {
ndefReader.abort();
ndefReader = null;
}
});
return {
isScanning,
data,
error,
scan
};
}
在组件中使用:
<template>
<div>
<button @click="nfc.scan" :disabled="nfc.isScanning">
{{ nfc.isScanning ? '扫描中...' : '扫描 NFC 标签' }}
</button>
<p v-if="nfc.data">NFC 数据: {{ nfc.data }}</p>
<p v-if="nfc.error">{{ nfc.error }}</p>
</div>
</template>
<script setup>
import { useNFC } from './useNFC';
const nfc = useNFC();
</script>
最佳实践总结
- 错误处理: 始终使用
try...catch块来处理 API 调用中的错误,并向用户显示友好的错误信息。 - 权限管理: 在调用
navigator.bluetooth.requestDevice()和NDEFReader.scan()之前,检查用户是否已授予相应的权限。 - UI 反馈: 在执行耗时操作(例如设备连接和数据读取)时,向用户提供 UI 反馈,例如显示加载指示器。
- 断开连接处理: 监听
gattserverdisconnected事件,并在设备断开连接时重置状态。 - 资源清理: 在组件卸载时,清理事件监听器和引用,以防止内存泄漏。
- 代码模块化: 使用组合式函数来封装 API 调用,提高代码的可重用性和可维护性。
- 数据验证: 对从蓝牙设备和 NFC 标签读取的数据进行验证,确保数据的有效性。
Web Bluetooth/NFC 与 Vue.js 响应式系统的结合
Web Bluetooth 和 Web NFC API 都是异步的,与 Vue.js 的响应式系统结合的关键在于:
- 响应式状态管理: 使用
ref和reactive创建响应式变量来存储 API 调用结果和设备状态。 - 异步操作处理: 使用
async/await来处理异步操作,并在操作完成后更新响应式状态。 - 计算属性: 使用
computed创建计算属性来根据响应式状态派生出新的状态,例如设备是否已连接。 - 副作用处理: 使用
watch监听状态变化,并在状态变化时执行副作用,例如更新 UI 或发送数据到服务器。
通过这些策略,我们可以将异步操作和状态变化无缝地融入 Vue 的响应式系统中,从而构建更流畅、更直观的用户体验。
案例分析:智能家居控制
我们可以使用 Web Bluetooth 和 Web NFC API 构建一个智能家居控制应用。
- Web Bluetooth: 用于连接智能灯泡、智能插座等蓝牙设备,并控制它们的开关和亮度。
- Web NFC: 用于读取 NFC 标签,例如放置在房间门口的 NFC 标签,用户扫描标签后,应用会自动打开该房间的灯。
在这个应用中,设备连接状态、设备数据和 NFC 标签数据都存储在响应式变量中,当这些值发生变化时,Vue 会自动更新 UI,从而实现实时控制和反馈。
展望:未来的可能性
Web Bluetooth 和 Web NFC API 为 Web 应用带来了与物理世界交互的可能性。未来,我们可以利用这些 API 构建更多创新应用,例如:
- 智能穿戴设备集成: 将智能手表、手环等穿戴设备的数据集成到 Web 应用中。
- 移动支付: 使用 Web NFC API 实现移动支付功能。
- 物联网数据可视化: 将物联网设备的数据可视化到 Web 应用中。
- 位置感知应用: 使用 Web Bluetooth Beacon 技术构建位置感知应用。
这些只是冰山一角,随着技术的不断发展,我们可以期待更多令人兴奋的应用场景。
响应式地管理设备状态和事件
通过使用 Vue.js 的响应式系统,我们可以轻松地管理设备连接状态、数据变化和错误信息,并将其反映在 UI 中。组合式函数可以提高代码的可重用性和可维护性。
更多IT精英技术系列讲座,到智猿学院