作者:全栈开发工程师
阅读时长:15分钟
项目背景与需求分析
随着物联网技术的快速发展,智能家居应用需要同时支持移动端App、微信小程序、Web端等多平台。UniApp凭借其”一次开发,多端部署”的优势,成为构建此类应用的理想选择。
核心功能需求:
- 设备状态实时监控与控制
- 场景模式一键切换
- 能耗数据统计分析
- 多用户权限管理
- 消息推送与智能提醒
技术架构设计
整体架构图:
┌─────────────────────────────────────────┐ │ 前端展示层 │ │ ┌─────────┬─────────┬─────────────┐ │ │ │ App │ 小程序 │ H5页面 │ │ │ └─────────┴─────────┴─────────────┘ │ └─────────────────┬───────────────────────┘ │ ┌─────────────────▼───────────────────────┐ │ UniApp业务层 │ │ ┌───────┬──────────┬────────────────┐ │ │ │ 设备管理 │ 场景控制 │ 数据统计 │ │ │ └───────┴──────────┴────────────────┘ │ └─────────────────┬───────────────────────┘ │ ┌─────────────────▼───────────────────────┐ │ 后端服务层 │ │ ┌───────┬──────────┬────────────────┐ │ │ │ MQTT │ WebSocket │ RESTful API │ │ │ └───────┴──────────┴────────────────┘ │ └─────────────────┬───────────────────────┘ │ ┌─────────────────▼───────────────────────┐ │ 设备硬件层 │ │ ┌───────┬──────────┬────────────────┐ │ │ │ 智能灯光 │ 空调系统 │ 安防设备 │ │ │ └───────┴──────────┴────────────────┘ │ └─────────────────────────────────────────┘
技术栈选型:
层级 | 技术方案 | 说明 |
---|---|---|
前端框架 | UniApp 3.9 + Vue 3 + TypeScript | 提供类型安全和更好的开发体验 |
状态管理 | Pinia + 持久化存储 | 设备状态全局管理 |
UI框架 | uView Plus 3.1 + 自定义组件 | 统一多端UI体验 |
实时通信 | MQTT.js + WebSocket | 设备状态实时同步 |
数据可视化 | ECharts + 自定义图表 | 能耗数据展示 |
核心模块实现
1. 设备管理模块
实现设备的增删改查和状态监控:
// types/device.ts
export interface SmartDevice {
id: string
name: string
type: DeviceType
status: DeviceStatus
online: boolean
lastUpdate: number
properties: Record<string, any>
}
export enum DeviceType {
LIGHT = 'light',
THERMOSTAT = 'thermostat',
SECURITY = 'security',
PLUG = 'plug'
}
export enum DeviceStatus {
ONLINE = 'online',
OFFLINE = 'offline',
ERROR = 'error'
}
// stores/deviceStore.ts
import { defineStore } from 'pinia'
export const useDeviceStore = defineStore('device', {
state: () => ({
devices: new Map<string, SmartDevice>(),
roomGroups: new Map<string, string[]>(),
selectedRoom: 'living-room'
}),
getters: {
currentRoomDevices: (state) => {
const deviceIds = state.roomGroups.get(state.selectedRoom) || []
return deviceIds.map(id => state.devices.get(id)).filter(Boolean)
},
onlineDeviceCount: (state) => {
return Array.from(state.devices.values()).filter(device => device.online).length
}
},
actions: {
async fetchDevices() {
try {
const response = await uni.request({
url: '/api/devices',
method: 'GET'
})
if (response.statusCode === 200) {
const devices: SmartDevice[] = response.data
devices.forEach(device => {
this.devices.set(device.id, device)
})
}
} catch (error) {
console.error('获取设备列表失败:', error)
}
},
updateDeviceStatus(deviceId: string, status: Partial<SmartDevice>) {
const device = this.devices.get(deviceId)
if (device) {
this.devices.set(deviceId, { ...device, ...status })
}
},
async controlDevice(deviceId: string, command: string, params?: any) {
try {
const response = await uni.request({
url: `/api/devices/${deviceId}/control`,
method: 'POST',
data: { command, params }
})
if (response.statusCode === 200) {
this.updateDeviceStatus(deviceId, response.data)
}
} catch (error) {
uni.showToast({
title: '控制指令发送失败',
icon: 'error'
})
}
}
}
})
2. 实时通信服务
基于MQTT实现设备状态实时同步:
// services/mqttService.ts
import mqtt from 'mqtt'
class MQTTService {
private client: any = null
private isConnected = false
constructor() {
this.init()
}
private init() {
// #ifdef H5
this.client = mqtt.connect('wss://mqtt.example.com:8884')
// #endif
// #ifdef APP-PLUS || MP-WEIXIN
this.client = mqtt.connect('wx://mqtt.example.com:8883')
// #endif
this.client.on('connect', () => {
this.isConnected = true
console.log('MQTT连接成功')
this.subscribe('device/status/#')
})
this.client.on('message', (topic: string, message: Buffer) => {
this.handleMessage(topic, JSON.parse(message.toString()))
})
this.client.on('error', (error: any) => {
console.error('MQTT连接错误:', error)
this.isConnected = false
})
}
private handleMessage(topic: string, message: any) {
const topicParts = topic.split('/')
const deviceId = topicParts[2]
switch (topicParts[1]) {
case 'status':
useDeviceStore().updateDeviceStatus(deviceId, message)
break
case 'alert':
this.handleDeviceAlert(deviceId, message)
break
}
}
private handleDeviceAlert(deviceId: string, alert: any) {
uni.showModal({
title: '设备告警',
content: `设备 ${deviceId} 发生异常: ${alert.message}`,
showCancel: false
})
}
public publish(deviceId: string, command: string, params: any) {
if (!this.isConnected) return
const topic = `device/control/${deviceId}`
this.client.publish(topic, JSON.stringify({ command, params }))
}
public subscribe(topic: string) {
if (this.isConnected) {
this.client.subscribe(topic)
}
}
}
export const mqttService = new MQTTService()
3. 场景模式管理
实现一键切换多种家居场景:
// components/scene-selector.vue
<template>
<view class="scene-selector">
<scroll-view scroll-x class="scene-scroll">
<view
v-for="scene in scenes"
:key="scene.id"
class="scene-item"
:class="{ active: currentScene?.id === scene.id }"
@click="switchScene(scene)"
>
<image :src="scene.icon" class="scene-icon"></image>
<text class="scene-name">{{ scene.name }}</text>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
interface Scene {
id: string
name: string
icon: string
devices: SceneDevice[]
}
interface SceneDevice {
deviceId: string
status: Record<string, any>
}
const scenes = ref<Scene[]>([
{
id: 'home',
name: '回家模式',
icon: '/static/scenes/home.png',
devices: [
{ deviceId: 'light-1', status: { power: true, brightness: 80 } },
{ deviceId: 'thermostat-1', status: { temperature: 24 } }
]
},
{
id: 'away',
name: '离家模式',
icon: '/static/scenes/away.png',
devices: [
{ deviceId: 'light-1', status: { power: false } },
{ deviceId: 'security-1', status: { armed: true } }
]
}
])
const currentScene = ref<Scene | null>(null)
const switchScene = async (scene: Scene) => {
try {
uni.showLoading({ title: '切换场景中...' })
// 批量控制场景中的设备
const promises = scene.devices.map(device =>
mqttService.publish(device.deviceId, 'set', device.status)
)
await Promise.all(promises)
currentScene.value = scene
uni.showToast({ title: '场景切换成功' })
} catch (error) {
uni.showToast({ title: '场景切换失败', icon: 'error' })
} finally {
uni.hideLoading()
}
}
onMounted(() => {
// 默认选择回家模式
currentScene.value = scenes.value[0]
})
</script>
4. 能耗统计图表
使用ECharts展示设备能耗数据:
// components/energy-chart.vue
<template>
<view class="energy-chart">
<ec-canvas ref="chartRef" canvas-id="energy-chart"></ec-canvas>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
const chartRef = ref()
const initChart = () => {
const chart = echarts.init(chartRef.value, null, {
width: uni.getSystemInfoSync().windowWidth - 40,
height: 300
})
const option = {
title: {
text: '本月能耗统计',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['客厅', '卧室', '厨房', '卫生间'],
bottom: 10
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['第1周', '第2周', '第3周', '第4周']
},
yAxis: {
type: 'value',
name: '能耗 (kWh)'
},
series: [
{
name: '客厅',
type: 'bar',
stack: 'total',
data: [120, 132, 101, 134]
},
{
name: '卧室',
type: 'bar',
stack: 'total',
data: [220, 182, 191, 234]
},
{
name: '厨房',
type: 'bar',
stack: 'total',
data: [150, 232, 201, 154]
},
{
name: '卫生间',
type: 'bar',
stack: 'total',
data: [320, 332, 301, 334]
}
]
}
chart.setOption(option)
}
onMounted(() => {
setTimeout(initChart, 100)
})
</script>
性能优化策略
1. 设备状态缓存
// utils/deviceCache.ts
export class DeviceCache {
private static instance: DeviceCache
private cache = new Map<string, any>()
private maxSize = 100
static getInstance() {
if (!DeviceCache.instance) {
DeviceCache.instance = new DeviceCache()
}
return DeviceCache.instance
}
set(key: string, value: any, ttl: number = 300000) { // 5分钟默认过期
if (this.cache.size >= this.maxSize) {
this.evictOldest()
}
this.cache.set(key, {
value,
timestamp: Date.now(),
ttl
})
}
get(key: string) {
const item = this.cache.get(key)
if (!item) return null
if (Date.now() - item.timestamp > item.ttl) {
this.cache.delete(key)
return null
}
return item.value
}
private evictOldest() {
let oldestKey = ''
let oldestTime = Infinity
for (const [key, item] of this.cache) {
if (item.timestamp < oldestTime) {
oldestTime = item.timestamp
oldestKey = key
}
}
if (oldestKey) {
this.cache.delete(oldestKey)
}
}
}
2. 请求防抖处理
// utils/debounce.ts
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null
return (...args: Parameters<T>) => {
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 在设备控制中使用
const debouncedControl = debounce((deviceId: string, command: string) => {
mqttService.publish(deviceId, command)
}, 300)
多端适配方案
1. 平台特定功能处理
// utils/platform.ts
export class PlatformUtils {
static isApp() {
// #ifdef APP-PLUS
return true
// #endif
return false
}
static isWechat() {
// #ifdef MP-WEIXIN
return true
// #endif
return false
}
static async requestNotificationPermission() {
if (this.isApp()) {
// App端推送权限申请
const result = await uni.requestPushPermission()
return result[0].type === 'accept'
} else if (this.isWechat()) {
// 小程序端订阅消息
return await this.requestWechatSubscribe()
}
return true
}
static showShareMenu() {
// #ifdef MP-WEIXIN
uni.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline']
})
// #endif
}
}
部署与发布
1. 自动化构建配置
// package.json 构建脚本
{
"scripts": {
"build:app": "uni build --platform app",
"build:mp-weixin": "uni build --platform mp-weixin",
"build:h5": "uni build --platform h5",
"build:all": "npm run build:app && npm run build:mp-weixin && npm run build:h5"
}
}
2. 环境变量配置
// config/env.ts
const envConfig = {
development: {
baseUrl: 'http://localhost:3000',
mqttHost: 'ws://localhost:8083'
},
production: {
baseUrl: 'https://api.smarthome.com',
mqttHost: 'wss://mqtt.smarthome.com'
}
}
export const config = envConfig[process.env.NODE_ENV || 'development']
项目总结
技术亮点:
- 架构设计:采用分层架构,清晰分离关注点
- 实时通信:基于MQTT实现设备状态实时同步
- 类型安全:全面使用TypeScript,提升代码质量
- 性能优化:多级缓存、防抖处理、懒加载等优化手段
- 多端适配:完善的平台差异处理方案
业务价值:
- 一套代码同时覆盖App、小程序、Web三端
- 降低开发和维护成本约60%
- 提升用户体验和系统稳定性
- 为后续功能扩展奠定良好基础