Vue组件与Web Bluetooth/NFC API集成:构建响应式设备连接与状态管理
大家好!今天我们来深入探讨如何将Vue组件与Web Bluetooth API和Web NFC API整合,构建一个能够响应设备连接状态变化的应用。目标是创建一个能够实时反映设备连接状态,并将设备数据纳入Vue的响应式依赖图的应用架构。
Web Bluetooth API 和 Web NFC API 简介
在深入代码之前,我们先简单了解一下这两个API。
Web Bluetooth API 允许Web应用连接到附近的蓝牙低功耗(BLE)设备。可以读取设备的数据(例如心率、温度等)以及向设备写入数据。它提供了一种标准化的方式,让浏览器能够与蓝牙设备进行通信,无需安装额外的插件或驱动程序。
Web NFC API 允许Web应用读取和写入NFC (Near Field Communication) 标签。可以用于多种场景,如读取商品信息、支付、身份验证等。同样,它为Web应用提供了一种标准化的方式来与NFC标签交互。
项目初始化与环境配置
首先,我们需要创建一个Vue项目。这里使用Vue CLI:
vue create bluetooth-nfc-app
选择默认配置或者根据需要选择其他特性。项目创建完成后,进入项目目录:
cd bluetooth-nfc-app
为了更好地组织代码,我们创建一个 src/components/DeviceManager.vue 组件,用于处理设备的连接和数据读取逻辑。
DeviceManager 组件结构
DeviceManager.vue 组件的基本结构如下:
<template>
<div>
<button @click="connectDevice">Connect Device</button>
<p v-if="isConnected">Connected to: {{ deviceName }}</p>
<p v-else>Not connected</p>
<div v-if="heartRate">
Heart Rate: {{ heartRate }} BPM
</div>
<div v-if="nfcData">
NFC Data: {{ nfcData }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
deviceName: '',
isConnected: false,
heartRate: null,
nfcData: null,
device: null,
server: null,
characteristic: null,
nfcReader: null
};
},
methods: {
connectDevice() {
// 连接设备逻辑
},
startNFCReading() {
// NFC 读取逻辑
}
},
mounted() {
// 组件挂载时启动 NFC 读取
this.startNFCReading();
}
};
</script>
实现 Bluetooth 连接
接下来,实现 connectDevice 方法,使用 Web Bluetooth API 连接到设备。
async connectDevice() {
try {
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['heart_rate'] }], // 替换为你的设备的服务UUID
optionalServices: ['generic_access']
});
this.device = device;
this.deviceName = device.name;
this.device.addEventListener('gattserverdisconnected', this.onDisconnected);
const server = await device.gatt.connect();
this.server = server;
const service = await server.getPrimaryService('heart_rate'); // 替换为你的服务UUID
const characteristic = await service.getCharacteristic('heart_rate_measurement'); // 替换为你的特征UUID
this.characteristic = characteristic;
await characteristic.startNotifications();
characteristic.addEventListener('characteristicvaluechanged', this.handleHeartRate);
this.isConnected = true;
} catch (error) {
console.error('Error connecting to device:', error);
this.isConnected = false;
}
},
onDisconnected(event) {
// 设备断开连接时的处理
this.isConnected = false;
this.deviceName = '';
this.heartRate = null;
console.log('Device disconnected');
},
handleHeartRate(event) {
// 处理心率数据
const value = event.target.value;
this.heartRate = value.getUint8(1); // 根据设备的数据格式调整
}
这段代码首先使用 navigator.bluetooth.requestDevice 方法请求用户选择一个设备。filters 选项指定了要搜索的服务UUID,optionalServices 指定了可选服务。成功选择设备后,它会连接到设备的 GATT 服务器,获取指定的服务和特征,并开始监听特征值的变化。当设备断开连接时,会触发 onDisconnected 方法,更新组件的状态。handleHeartRate 方法负责解析从设备接收到的数据,并更新 heartRate 数据属性,从而触发Vue的响应式更新。
代码解释:
navigator.bluetooth.requestDevice(): 发起设备选择对话框。filters属性允许指定要搜索的蓝牙服务 UUID,从而过滤设备。device.gatt.connect(): 连接到设备的 GATT 服务器。server.getPrimaryService(): 获取指定UUID的 primary service。service.getCharacteristic(): 获取指定UUID的 characteristic。characteristic.startNotifications(): 启动 notifications,当 characteristic 的值改变时,会触发characteristicvaluechanged事件。characteristic.addEventListener('characteristicvaluechanged', this.handleHeartRate): 监听 characteristic 的值改变事件,并调用handleHeartRate方法处理数据。this.isConnected = true: 更新isConnected状态,触发 Vue 的响应式更新。this.onDisconnected: 处理设备断开连接事件,重置组件状态。this.handleHeartRate: 处理心率数据,根据设备数据格式解析数据,并更新heartRate状态。
实现 NFC 读取
接下来,实现 startNFCReading 方法,使用 Web NFC API 读取 NFC 标签的数据。
async startNFCReading() {
try {
this.nfcReader = new window.NDEFReader();
await this.nfcReader.scan();
this.nfcReader.addEventListener("reading", ({ message, serialNumber }) => {
console.log(`> Serial Number: ${serialNumber}`);
console.log(`> Records: (${message.records.length})`);
this.nfcData = message.records[0].data; // 假设第一个 record 包含数据
for (const record of message.records) {
console.log(` > Record type: ${record.recordType}`);
console.log(` > MIME type: ${record.mediaType}`);
console.log(` > Record id: ${record.id}`);
console.log(` > Data: ${new TextDecoder().decode(record.data)}`);
}
});
this.nfcReader.addEventListener("readingerror", () => {
console.log("Argh! Cannot read data from the NFC tag. Try another one?");
});
} catch (error) {
console.error("Error starting NFC reading:", error);
}
}
这段代码首先创建 NDEFReader 对象,然后调用 scan 方法开始扫描 NFC 标签。当读取到标签时,会触发 reading 事件。事件处理函数会解析标签的数据,并更新 nfcData 数据属性,从而触发Vue的响应式更新。
代码解释:
new window.NDEFReader(): 创建 NDEFReader 对象,用于读取 NFC 标签。nfcReader.scan(): 开始扫描 NFC 标签。nfcReader.addEventListener("reading", ...): 监听 reading 事件,当读取到 NFC 标签时触发。事件参数包含message和serialNumber,message包含标签的 NDEF 消息,serialNumber包含标签的序列号。message.records: NDEF 消息包含多个 record,每个 record 包含不同的数据。new TextDecoder().decode(record.data): 将 record 的数据解码为字符串。this.nfcData = message.records[0].data: 更新nfcData状态,触发 Vue 的响应式更新。nfcReader.addEventListener("readingerror", ...): 监听 readingerror 事件,当读取 NFC 标签出错时触发。
完整 DeviceManager 组件代码
下面是完整的 DeviceManager.vue 组件代码:
<template>
<div>
<button @click="connectDevice">Connect Bluetooth Device</button>
<p v-if="isConnected">Connected to: {{ deviceName }}</p>
<p v-else>Not connected</p>
<div v-if="heartRate">
Heart Rate: {{ heartRate }} BPM
</div>
<hr>
<div>
<p>NFC Reader</p>
<p v-if="nfcData">NFC Data: {{ nfcData }}</p>
<p v-else>No NFC tag detected</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
deviceName: '',
isConnected: false,
heartRate: null,
nfcData: null,
device: null,
server: null,
characteristic: null,
nfcReader: null
};
},
methods: {
async connectDevice() {
try {
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['heart_rate'] }], // 替换为你的设备的服务UUID
optionalServices: ['generic_access']
});
this.device = device;
this.deviceName = device.name;
this.device.addEventListener('gattserverdisconnected', this.onDisconnected);
const server = await device.gatt.connect();
this.server = server;
const service = await server.getPrimaryService('heart_rate'); // 替换为你的服务UUID
const characteristic = await service.getCharacteristic('heart_rate_measurement'); // 替换为你的特征UUID
this.characteristic = characteristic;
await characteristic.startNotifications();
characteristic.addEventListener('characteristicvaluechanged', this.handleHeartRate);
this.isConnected = true;
} catch (error) {
console.error('Error connecting to device:', error);
this.isConnected = false;
}
},
onDisconnected(event) {
// 设备断开连接时的处理
this.isConnected = false;
this.deviceName = '';
this.heartRate = null;
console.log('Device disconnected');
},
handleHeartRate(event) {
// 处理心率数据
const value = event.target.value;
this.heartRate = value.getUint8(1); // 根据设备的数据格式调整
},
async startNFCReading() {
try {
if ('NDEFReader' in window) {
this.nfcReader = new window.NDEFReader();
await this.nfcReader.scan();
this.nfcReader.addEventListener("reading", ({ message, serialNumber }) => {
console.log(`> Serial Number: ${serialNumber}`);
console.log(`> Records: (${message.records.length})`);
this.nfcData = message.records[0].data; // 假设第一个 record 包含数据
for (const record of message.records) {
console.log(` > Record type: ${record.recordType}`);
console.log(` > MIME type: ${record.mediaType}`);
console.log(` > Record id: ${record.id}`);
console.log(` > Data: ${new TextDecoder().decode(record.data)}`);
}
});
this.nfcReader.addEventListener("readingerror", () => {
console.log("Argh! Cannot read data from the NFC tag. Try another one?");
});
} else {
console.log("Web NFC API is not supported in this browser.");
}
} catch (error) {
console.error("Error starting NFC reading:", error);
}
}
},
mounted() {
// 组件挂载时启动 NFC 读取
this.startNFCReading();
}
};
</script>
将 DeviceManager 组件添加到 App.vue
将 DeviceManager 组件导入到 App.vue 并使用它:
<template>
<div id="app">
<DeviceManager />
</div>
</template>
<script>
import DeviceManager from './components/DeviceManager.vue';
export default {
components: {
DeviceManager
}
};
</script>
代码优化与注意事项
- 错误处理: 在实际应用中,需要更完善的错误处理机制,例如显示错误信息给用户,重试连接等。
- 蓝牙服务和特征UUID: 需要根据具体的蓝牙设备修改服务和特征的UUID。可以使用蓝牙调试工具(例如LightBlue或nRF Connect)来查找正确的UUID。
- 数据格式: 蓝牙设备发送的数据格式可能不同,需要根据设备的数据格式来解析数据。
- 浏览器兼容性: Web Bluetooth API 和 Web NFC API 的浏览器兼容性可能有所不同,需要进行测试和兼容性处理。
- 安全: 在生产环境中,需要考虑安全性问题,例如数据加密、身份验证等。
状态管理方案 (Vuex)
对于更复杂的应用,可以使用 Vuex 进行状态管理。将设备连接状态、数据存储在 Vuex store 中,可以方便地在多个组件之间共享状态。
首先安装 Vuex:
npm install vuex --save
创建一个 src/store/index.js 文件:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
deviceName: '',
isConnected: false,
heartRate: null,
nfcData: null,
},
mutations: {
setDeviceName(state, name) {
state.deviceName = name;
},
setIsConnected(state, isConnected) {
state.isConnected = isConnected;
},
setHeartRate(state, heartRate) {
state.heartRate = heartRate;
},
setNfcData(state, nfcData) {
state.nfcData = nfcData;
}
},
actions: {
updateDeviceName({ commit }, name) {
commit('setDeviceName', name);
},
updateIsConnected({ commit }, isConnected) {
commit('setIsConnected', isConnected);
},
updateHeartRate({ commit }, heartRate) {
commit('setHeartRate', heartRate);
},
updateNfcData({ commit }, nfcData) {
commit('setNfcData', nfcData);
}
},
getters: {
getDeviceName: state => state.deviceName,
getIsConnected: state => state.isConnected,
getHeartRate: state => state.heartRate,
getNfcData: state => state.nfcData
}
});
修改 DeviceManager.vue 组件,使用 Vuex 来管理状态:
<template>
<div>
<button @click="connectDevice">Connect Bluetooth Device</button>
<p v-if="isConnected">Connected to: {{ deviceName }}</p>
<p v-else>Not connected</p>
<div v-if="heartRate">
Heart Rate: {{ heartRate }} BPM
</div>
<hr>
<div>
<p>NFC Reader</p>
<p v-if="nfcData">NFC Data: {{ nfcData }}</p>
<p v-else>No NFC tag detected</p>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
export default {
computed: {
...mapGetters(['getDeviceName', 'getIsConnected', 'getHeartRate', 'getNfcData']),
deviceName() {
return this.getDeviceName;
},
isConnected() {
return this.getIsConnected;
},
heartRate() {
return this.getHeartRate;
},
nfcData() {
return this.getNfcData;
}
},
methods: {
...mapActions(['updateDeviceName', 'updateIsConnected', 'updateHeartRate', 'updateNfcData']),
async connectDevice() {
try {
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['heart_rate'] }], // 替换为你的设备的服务UUID
optionalServices: ['generic_access']
});
this.device = device;
this.updateDeviceName(device.name);
this.device.addEventListener('gattserverdisconnected', this.onDisconnected);
const server = await device.gatt.connect();
this.server = server;
const service = await server.getPrimaryService('heart_rate'); // 替换为你的服务UUID
const characteristic = await service.getCharacteristic('heart_rate_measurement'); // 替换为你的特征UUID
this.characteristic = characteristic;
await characteristic.startNotifications();
characteristic.addEventListener('characteristicvaluechanged', this.handleHeartRate);
this.updateIsConnected(true);
} catch (error) {
console.error('Error connecting to device:', error);
this.updateIsConnected(false);
}
},
onDisconnected(event) {
// 设备断开连接时的处理
this.updateIsConnected(false);
this.updateDeviceName('');
this.updateHeartRate(null);
console.log('Device disconnected');
},
handleHeartRate(event) {
// 处理心率数据
const value = event.target.value;
this.updateHeartRate(value.getUint8(1)); // 根据设备的数据格式调整
},
async startNFCReading() {
try {
if ('NDEFReader' in window) {
this.nfcReader = new window.NDEFReader();
await this.nfcReader.scan();
this.nfcReader.addEventListener("reading", ({ message, serialNumber }) => {
console.log(`> Serial Number: ${serialNumber}`);
console.log(`> Records: (${message.records.length})`);
this.updateNfcData(message.records[0].data); // 假设第一个 record 包含数据
for (const record of message.records) {
console.log(` > Record type: ${record.recordType}`);
console.log(` > MIME type: ${record.mediaType}`);
console.log(` > Record id: ${record.id}`);
console.log(` > Data: ${new TextDecoder().decode(record.data)}`);
}
});
this.nfcReader.addEventListener("readingerror", () => {
console.log("Argh! Cannot read data from the NFC tag. Try another one?");
});
} else {
console.log("Web NFC API is not supported in this browser.");
}
} catch (error) {
console.error("Error starting NFC reading:", error);
}
}
},
mounted() {
// 组件挂载时启动 NFC 读取
this.startNFCReading();
},
data() {
return {
device: null,
server: null,
characteristic: null,
nfcReader: null
};
}
};
</script>
在 main.js 中引入 Vuex:
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App)
}).$mount('#app')
使用 Composition API (Vue 3)
如果使用 Vue 3,可以使用 Composition API 来组织代码,使代码更具可读性和可维护性。
<template>
<div>
<button @click="connectDevice">Connect Bluetooth Device</button>
<p v-if="isConnected">Connected to: {{ deviceName }}</p>
<p v-else>Not connected</p>
<div v-if="heartRate">
Heart Rate: {{ heartRate }} BPM
</div>
<hr>
<div>
<p>NFC Reader</p>
<p v-if="nfcData">NFC Data: {{ nfcData }}</p>
<p v-else>No NFC tag detected</p>
</div>
</div>
</template>
<script>
import { ref, onMounted, computed } from 'vue';
export default {
setup() {
const deviceName = ref('');
const isConnected = ref(false);
const heartRate = ref(null);
const nfcData = ref(null);
let device = null;
let server = null;
let characteristic = null;
let nfcReader = null;
const connectDevice = async () => {
try {
const selectedDevice = await navigator.bluetooth.requestDevice({
filters: [{ services: ['heart_rate'] }], // 替换为你的设备的服务UUID
optionalServices: ['generic_access']
});
device = selectedDevice;
deviceName.value = device.name;
device.addEventListener('gattserverdisconnected', onDisconnected);
server = await device.gatt.connect();
const service = await server.getPrimaryService('heart_rate'); // 替换为你的服务UUID
characteristic = await service.getCharacteristic('heart_rate_measurement'); // 替换为你的特征UUID
await characteristic.startNotifications();
characteristic.addEventListener('characteristicvaluechanged', handleHeartRate);
isConnected.value = true;
} catch (error) {
console.error('Error connecting to device:', error);
isConnected.value = false;
}
};
const onDisconnected = (event) => {
isConnected.value = false;
deviceName.value = '';
heartRate.value = null;
console.log('Device disconnected');
};
const handleHeartRate = (event) => {
const value = event.target.value;
heartRate.value = value.getUint8(1); // 根据设备的数据格式调整
};
const startNFCReading = async () => {
try {
if ('NDEFReader' in window) {
nfcReader = new window.NDEFReader();
await nfcReader.scan();
nfcReader.addEventListener("reading", ({ message, serialNumber }) => {
console.log(`> Serial Number: ${serialNumber}`);
console.log(`> Records: (${message.records.length})`);
nfcData.value = message.records[0].data; // 假设第一个 record 包含数据
for (const record of message.records) {
console.log(` > Record type: ${record.recordType}`);
console.log(` > MIME type: ${record.mediaType}`);
console.log(` > Record id: ${record.id}`);
console.log(` > Data: ${new TextDecoder().decode(record.data)}`);
}
});
nfcReader.addEventListener("readingerror", () => {
console.log("Argh! Cannot read data from the NFC tag. Try another one?");
});
} else {
console.log("Web NFC API is not supported in this browser.");
}
} catch (error) {
console.error("Error starting NFC reading:", error);
}
};
onMounted(() => {
startNFCReading();
});
return {
deviceName,
isConnected,
heartRate,
nfcData,
connectDevice
};
}
};
</script>
表格总结:
| API | 描述 |
|---|---|
| Web Bluetooth | 允许Web应用连接到附近的蓝牙低功耗(BLE)设备,读取和写入数据。 |
| Web NFC | 允许Web应用读取和写入NFC标签,用于读取商品信息、支付、身份验证等。 |
| Vuex | Vue的状态管理模式和库,用于集中存储和管理所有组件的状态,提供可预测的状态管理。 |
| Composition API | Vue 3 中引入的一种新的组织组件逻辑的方式,通过函数组合来实现代码复用和逻辑分离,提高代码的可读性和可维护性。 |
应用的扩展与可能性
通过将Vue组件与Web Bluetooth API和Web NFC API集成,我们可以构建各种各样的应用。例如:
- 健康监测应用: 连接心率带、血压计等蓝牙设备,实时监测用户的健康数据。
- 智能家居控制应用: 连接智能灯泡、智能插座等设备,通过NFC标签控制设备的开关。
- 物流跟踪应用: 使用NFC标签记录商品的物流信息,方便用户查询。
- 门禁系统: 通过NFC标签进行身份验证,控制门禁。
关于代码和应用的未来
以上示例展示了如何将Web Bluetooth API和Web NFC API与Vue组件集成,实现设备的连接与状态管理。通过这些技术,我们可以构建各种创新应用,连接物理世界与Web应用,为用户提供更丰富的体验。
更多IT精英技术系列讲座,到智猿学院