在开发过程中,可能会遇到类似需求场景,即不断变化的数据在页面图表组件中如何保持热更新,本文以设备在线数量统计为例,实现环形图数据实时更新。

一、后端模拟设备心跳包数据

       使用WebSocket技术向前端应用不断推送设备心跳包数据,表示设备在线运行中。以Java语言主流开发框架SpringBoot为例,具体实现过程如下:   

1、SpringBoot集成WebSocket

// pom文件中引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

// WebSocket配置文件
@Configuration
public class WebSocketConfig {
    // 注入一个ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint    
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

// WebSocket工具类文件
@Component
// 用户名结合实际情况选择取舍
@ServerEndpoint("/websocket/{userName}")
public class WebSocket {
    private Session session;
    private static CopyOnWriteArraySet<WebSocket> webSockets =new CopyOnWriteArraySet<>();
    private static Map<String,Session> sessionPool = new HashMap<String,Session>();

    @OnOpen
    public void onOpen(Session session, @PathParam(value="userName")String userName) {
        this.session = session;
        webSockets.add(this);
        sessionPool.put(userName, session);
        System.out.println("[websocket消息]有新的连接,总数为:"+webSockets.size()+",新连接用户名为:"+userName);
    }

    @OnClose
    public void onClose() {
        webSockets.remove(this);
        System.out.println("[websocket消息]连接断开,总数为:"+webSockets.size());
    }

    @OnMessage
    public void onMessage(String message) {
        System.out.println("[websocket消息]收到客户端消息:"+message);
    }
    
    public static void sendAllMessage(String message) {
        for(WebSocket webSocket : webSockets) {
            System.out.println("[websocket消息]广播消息:"+message);
            try {
                webSocket.session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void sendOneMessage(String userName, String message) {
        System.out.println("[websocket消息]单点消息:"+message);
        Session session = sessionPool.get(userName);
        if (session != null) {
            try {
                session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("[websocket消息]单点消息:"+message+"未发送");
            }
        }
    }
}

2、使用WebSocket推送设备心跳包数据 

@RequestMapping("/server")
@RestController
public class ServerController {
    private static WebSocket webSocket;

    @GetMapping("/sendHeartBeat")
    public void sendHeartBeat() throws JSONException {
        try{
            JSONObject heartBeat = new JSONObject();
            // 简单模拟一台设备不停的向已连接客户端发送心跳包数据,可结合项目实际情况采用多线程并发执行
            while (true) {           
                heartBeat.put("devId", nowTimeStr("100100100100100"));
                heartBeat.put("date", nowTimeStr("yyyy-MM-dd HH:mm:ss"));
                Thread.currentThread().sleep(5000);
                webSocket.sendAllMessage(heartBeat.toString());                         
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

二、前端图表数据热更新

       通常中小型Web图表展示应用使用Vue+ECharts框架进行搭建,本文为能在Web、H5、小程序和APP等多端应用中实现图表数据的实时更新,采用uni-app+uCharts跨平台框架进行开发。具体实现过程如下:

1、使用Vuex管理心跳数据

// /store/modules/webSocket.js
export default {
  namespaced: true,
  state: {
    webSocketInfo: []
  },
  getters: {},
  mutations: {
    SET_WS_INFO: (state, webSocketInfo) => {
      state.webSocketInfo = webSocketInfo
    },
    REMOVE_WS_INFO: (state) => {
      state.webSocketInfo = []
    }
  },
  actions: {}
}

// /store/index.js
import webSocket from './modules/webSocket.js'

// #ifndef VUE3
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
  modules: {
    webSocket
  }
})
// #endif

// #ifdef VUE3
import {
  createStore
} from 'vuex'
const store = createStore({
  modules: {
    webSocket
  }
})
// #endif

export default store

2、uni-app集成WebSocket,监听心跳数据

// /app.config.js
export default {
  // 可结合项目情况自行修改端口和地址
  webSocketUrl: 'ws://localhost:8888/websocket'
}

// /plugin/modules/webSocket.js
import appConfig from '@/app.config.js'
import store from '@/store'
const WebSocket = {
  url: appConfig.webSocketUrl,
  isOpen: false,
  socketTask: false,
  // 连接
  open() {
    if (this.isOpen) return
    this.socketTask = uni.connectSocket({
      // 用户名结合实际情况选择取舍和更改
      url: this.url + '/admin',
      complete: (e) => {}
    })
    if (!this.socketTask) return
    this.socketTask.onOpen(() => {
      this.isOpen = true
    })
    // 监听信息
    this.message()
    // 监听关闭
    this.socketTask.onClose(() => {
      this.isOpen = false
      this.socketTask = false
      uni.showToast({
        icon: "none",
        title: '服务器通信连接断开',
      });
    })
    // 监听错误
    this.socketTask.onError((e) => {
      this.isOpen = false
      this.socketTask = false
      uni.showToast({
        icon: "none",
        title: '服务器通信连接错误',
      });
    })
  },
  // 关闭连接
  close() {
    if (this.isOpen) {
      this.socketTask.close()
      return
    }
  },
  // 心跳数据处理逻辑,可结合项目情况自行修改
  message() {
    this.socketTask.onMessage((e) => {
      // 字符串转json
      let data = JSON.parse(e.data)
      // 读取Vuex中管理的心跳数据
      const webSocketInfo = store.state.webSocket.webSocketInfo
      const index = webSocketInfo.findIndex((v) =>
        v.devId == data.devId
      )
      if (index == -1) {
        webSocketInfo.push({
          'devId': data.devId,
          'date': data.date
        })      
      }else{
        webSocketInfo[index].date = data.date
      }
      // 向Vuex中重新写入心跳数据
      store.commit('webSocket/SET_WS_INFO', webSocketInfo)
    })
  }
}
// #ifndef VUE3
export function initWebSocket(Vue) {
  Vue.prototype.webSocket = WebSocket
}
// #endif

// #ifdef VUE3
export function initWebSocket(app) {
  app.config.globalProperties.webSocket = WebSocket
}
// #endif

3、将Vuex和WebSocket挂载全局,开启WebSocket服务

// /main.js
import App from './App'
import config from "./app.config.js"
import store from './store'
import plugin from './plugin/index'
// #ifndef VUE3
import Vue from 'vue'
Vue.config.productionTip = false
// 挂载全局方法
Vue.prototype.config = config
Vue.use(plugin)
App.mpType = 'app'
const app = new Vue({
  config,
  store,
    ...App
})
app.$mount()
// #endif

// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
  const app = createSSRApp(App)
  app.use(config)
  app.use(store)
  app.use(plugin)
  return {
    app
  }
}
// #endif

// /App.vue
<script>
  export default {
    onLaunch: function() {
      // 开启WebSocket服务
      this.webSocket.open()
      console.log('App Launch')
    }
  }
</script>

4、uni-app引入uCharts

       参考官方使用文档https://ext.dcloud.net.cn/plugin?id=271,无需配置,直接导入即可。

5、页面渲染心跳数据并实时更新

// /pages/test/test.vue
<template>
  <view>
    <qiun-data-charts type="ring" :opts="chartOpts" :chartData="chartData" background="none" />
  </view>
</template>
<script>
  import {
    mapState
  } from 'vuex'
  export default {
    data() {
      return {
        // 直接初始化设备数量,可结合项目情况从后端请求数据
        total: 5,
        onLine: 0,
        offLine: 0,
        chartData: {
          "series": [{
            "data": [{
                "name": "在线",
                "value": 0
              },
              {
                "name": "离线",
                "value": 0
              }
            ]
          }]
        },
        chartOpts: {
          "color": [
            "#23eb6a",
            "#ff0000"
          ],
          "legend": {
            "position": "bottom",
            "fontSize": 20
          },
          "title": {
            "name": "在线率",
            "fontSize": 22,
            "color": "#00aaff"
          },
          "subtitle": {
            "name": "0.00%",
            "fontSize": 24,
            "color": "#00aaff"
          }
        }
      }
    },
    onLoad() {
      this.__init()
    },
    computed: {
      ...mapState('webSocket', ['webSocketInfo'])
    },
    // 监听心跳数据变化
    watch: {
      webSocketInfo: {
        handler(newVal, oldVal) {
          this.onLine = newVal.length
          this.offLine = this.total - this.onLine
          this.updateDevice()
        }
      }
    },
    methods: {
      __init() {
        // 首先默认设备全部处于离线状态
        this.offLine = this.total
        this.onLine = this.webSocketInfo.length
        this.updateDevice()
      },
      //更新设备状态
      updateDevice() {
        this.chartData.series[0].data[0].value = this.onLine
        this.chartData.series[0].data[1].value = this.offLine
        this.chartOpts.subtitle.name = (this.onLine / this.total).toFixed(4) * 100 + '%'
      }
    }
  }
</script>

三、效果展示

ECharts类图表数据实时更新以设备在线数量统计为例,实现环形图数据实时更新https://mp.weixin.qq.com/s?__biz=MzU5MjUyNzE4Mw==&mid=2247483885&idx=1&sn=d3ea4d2ceace6ab4dd6fbca03bb34a90&chksm=fe1f2bcdc968a2dba2988f66fbfe6943da9b2ae8d9ea3e5d3fd04d066c193c8bf7012f34c7ee&token=1116670217&lang=zh_CN#rd

四、总结

       此方式仅适用于小流量业务场景,对于高并发或海量数据集热更新等情况需后端统计计算直接推送结果或另辟蹊径。因作者技术能力有限,文中不足之处还望各位大佬直抒己见、批评指正,反馈邮箱:yourshare@foxmail.com。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐