Vue组件与Web Bluetooth/NFC API集成:将设备连接与状态变化纳入响应性依赖图

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 标签时触发。事件参数包含 messageserialNumbermessage 包含标签的 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精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注