目录
一、WebSocket【双向通信】的使用
1.1、前端
1.2、后端
二、前端组件的合并与优化
三、全屏切换
3.1、单页面切换
3.2、同页面多端联动
四、主题切换
4.1、单页面切换
4.2、同页面多端联动
一、WebSocket【双向通信】的使用
1.1、前端
在utils文件夹里创建socket_service.js,并在main.js里进行挂载
(1)、定义类SocketService,并定义成单例设计模式;
(2)、定义连接服务器的方法connect,(前端项目不用下载,直接window.socket即可);
(3)、监听事件:onopen、onclose、onmessage;
(4)、存储回调函数;
(5)、接收数据的处理;
(6)、定义发送数据的方法;
(7)、挂载SocketService对象到vue的原型对象上;
import SocketService from '@/utils/socket_service'
// 对服务端进行websocket的连接
SocketService.Instance.connect()
// 通过this.$socket.xxx获取所有方法
Vue.prototype.$socket = SocketService.Instance
export default class SocketService {// 单例static instance = nullstatic get Instance() {if (!this.instance) {this.instance = new SocketService()} return this.instance}// 和服务端连接的socket对象ws = null// 存储回调函数callBackMapping = {}// 标识是否连接成功connected = false// 记录重试次数sendRetryCount = 0// 重新连接尝试的次数connectRetryCount = 0// 定义连接服务器的方法connect() {if (!window.WebSocket) {return console.log('您的浏览器不支持WebSocket')}this.ws = new WebSocket('ws://localhost:9998')// 连接成功的事件this.ws.onopen = () => {console.log('连接服务端成功了');this.connected = truethis.connectRetryCount = 0}// 连接连接服务端失败this.ws.onclose = () => {console.log('连接服务端失败');this.connected = falsethis.connectRetryCount++setTimeout(() => {this.connect()}, this.connectRetryCount * 500)}// 得到服务端发送过来的数据this.ws.onmessage = msg => {console.log('从服务端获取到了数据', msg.data)const recvData = JSON.parse(msg.data)const socketType = recvData.socketType// 判断回调函数是否存在if (this.callBackMapping[socketType]) {const action = recvData.actionif (action === 'getData') {const realData = JSON.parse(recvData.data)this.callBackMapping[socketType].call(this, realData);//获取数据} else if (action === 'fullScreen') {this.callBackMapping[socketType].call(this, recvData);//全屏切换} else if (action === 'themeChange') {this.callBackMapping[socketType].call(this, recvData);//主题切换}}}}// 回调函数的注册registerCallBack(socketType, callBack) {this.callBackMapping[socketType] = callBack}// 取消某一个回调函数unRegisterCallBack(socketType) {this.callBackMapping[socketType] = null}// 发送数据send(data) {if (this.connected) {//判断此时有没有连接成功this.sendRetryCount = 0this.ws.send(JSON.stringify(data))} else {this.sendRetryCount++setTimeout(() => {this.send(data)}, this.sendRetryCount * 500)}}
}
1.2、后端
在service文件夹里创建web_socket_service.js,在app.js里引入
const WebSocketService = require('./service/web_socket_service')
// 开启服务端的监听,监听客户端的连接
// 当某一个客户端连接成功后,就会对这个客户端进行message事件的监听
WebSocketService.listen()
(1)、安装包 npm i ws -S;
(2)、创建对象;
(3)、监听事件;
(4)、发送数据;
const path = require('path')
const fileUtils = require('../utils/file_utils')
const WebSocket = require('ws')
const { log } = require('console')
// 创建WebSocket服务端的对象, 绑定的端口号是9998
const wss = new WebSocket.Server({port: 9998
})
// 服务端开启了监听
module.exports.listen = () => {// 对客户端的连接事件进行监听// client:代表的是客户端的连接socket对象wss.on('connection', client => {// console.log('有客户端连接成功了...')// 对客户端的连接对象进行message事件的监听// msg: 由客户端发给服务端的数据client.on('message', async msg => {let payload = JSON.parse(msg)console.log(action, '客户端发送数据给服务端了: ' + msg)const action = payload.actionif (action === 'getData') {let filePath = '../data/' + payload.chartName + '.json'filePath = path.join(__dirname, filePath)const ret = await fileUtils.getFileJsonData(filePath)// 需要在服务端获取到数据的基础之上, 增加一个data的字段// data所对应的值,就是某个json文件的内容payload.data = retclient.send(JSON.stringify(payload))} else {// 原封不动的将所接收到的数据转发给每一个处于连接状态的客户端// wss.clients // 所有客户端的连接wss.clients.forEach(client => {
//如果数据被转成了Buffer对象,故使用msg.toString()将Buffer转换为字符串,前端用JSON.parse(xxx)解析字符串为JSON对象即可client.send(msg.toString())})}})})
}
// 备注:前后端约定的字段
// let msg = {
// "action": 'getData',//获取数据方法名
// "socketType": "trendData",//前端响应函数的标识
// "chartName": 'trend',
// "value": '',
// "data":'.json里的数据'
// }
二、前端组件的合并与优化
(1)、在组件创建完成后,进行回调函数的注册;在组件销毁时,进行回调函数的取消;
(2)、发送数据给服务器;
(3)、直接 getData(ret) 得到数据,不用再发请求了;
(4)、重发数据机制
让属性connected在onopen时设置为true,在onclose时设置为false,计算重发次数
(5)、断开重连机制
如果失败,则延时的时长随着尝试的次数而增加,如果成功,则将次数归0
以Trend.vue组件为例:
created() {this.$socket.registerCallBack('trendData', this.getData);},destroyed() {this.$socket.unRegisterCallBack('trendData');},mounted() {......// this.getData();this.$socket.send({action: "getData",socketType: "trendData",chartName: "trend",value: "",});//发送数据给服务器,告诉它,该组件需要数据},methods:{// 旧版本// async getData() {// const { data: ret } = await this.$http.get("trend");// this.allData = ret;// this.updateChart();// },// websocket版本getData(ret) {this.allData = ret;this.updateChart();},
}
三、全屏切换
以"热销商品占比图表"为例:
(1)、定义全屏状态的数据、样式;
(2)、在点击指定组件的放大图标时传递自己的属性名,在方法里根据对应的属性值切换对应的样式和图标,并调用组件自己的screenAdapter();
(3)、联动效果:
发送全屏数据给服务器,服务器在收到这个数据时,会转发给每一个处于连接状态的客户端,前端在created()时注册回调、在destroyed()时取消回调、在recvData()里接收到数据,进行属性值处理。
3.1、单页面切换
3.2、同页面多端联动
四、主题切换
4.1、单页面切换
(1)、自己在echarts的"主题编辑器"里选择要切换的主题.json文件;
(2)、点击切换按钮,修改vuex中的theme数据(通过mutations修改state里的数据);
(3)、在每个组件里监听theme的变化,用xx.dispose()销毁当前图表,再重新渲染(在registerTheme()和init()函数里改为变化的theme)
(4)、HTML样式随之改变:
utils文件夹的theme_utils.js内容
const theme = {chalk: {backgroundColor: '#161522',titleColor: '#fff',logoSrc: 'logo_dark.png',//左上角logo图标路径themeSrc: 'qiehuan_dark.png',//切换主题按钮的图片路径headerBorderSrc: 'header_border_dark.png'//页面顶部的边框图片},vintage: {backgroundColor: '#fff',titleColor: '#000',logoSrc: 'logo_light2.png',themeSrc: 'qiehuan_light.png',headerBorderSrc: 'header_border_light.png'}
}
export function getThemeValue(themeName) {return theme[themeName]
}
store文件夹里的index内容
state: {theme: 'chalk',},
mutations: {changeTheme(state) {if (state.theme == 'chalk') {state.theme = 'vintage'} else {state.theme = 'chalk'}},},
以Hot.vue组件为例:
<template><div class="com-container"><div class="com-chart" ref="hot_ref"></div><!-- 左右箭头 --><span class="iconfont arr-left" @click="toLeft" :style="comStyle"></span><span class="iconfont arr-right" @click="toRight" :style="comStyle"></span><!-- 一级标题 --><span class="cat-name" :style="comStyle">{{ catName }}系列</span></div>
</template>
<script>
import * as ets from "echarts";
import { mapState } from "vuex";
import chalk from "../../../../public/static/theme/chalk.json"; //自己下载
import vintage from "../../../../public/static/theme/vintage.json"; //自己下载
import { getThemeValue } from "@/utils/theme_utils.js";
export default {data() {return {isStyle: chalk,};},computed: {...mapState(["theme"]),comStyle() {return {fontSize: `${this.titleFontSize}px`,color: getThemeValue(this.theme).titleColor,//HTML字体颜色改变};},},watch: {theme() {if (this.theme == "chalk") {this.isStyle = chalk;} else {this.isStyle = vintage;}this.chartInstance.dispose(); //销毁当前图表// 重新初始化图表、完成屏幕适配、更新图表展示this.initChart();this.screenAdapter();this.updateChart();},},methods: {// 初始化echartsInstance对象initChart() {// ets.registerTheme("chalk", chalk);// this.chartInstance = ets.init(this.$refs.hot_ref, "chalk");ets.registerTheme("isStyle", this.isStyle);this.chartInstance = ets.init(this.$refs.hot_ref, "isStyle");},},
};
</script>
4.2、同页面多端联动
可参考全屏切换模块
echartsPage.vue页面(整合所有组件)代码:
<template><div class="screen-container" :style="containStyle"><header class="screen-header"><div><img :src="headerSrc" alt="" /></div><span class="logo"><img :src="logoSrc" alt="" /></span><span class="title">电商平台实时监控系统</span><div class="title-right"><img :src="themeSrc" class="qiehuan" @click="handleChangeTheme" /><span class="datetime">2049-01-01 00:00:00</span></div></header><div class="screen-body"><section class="screen-left"><divid="left-top":class="[fullScreenStatus.trend ? 'fullscreen' : '']"><!-- 销量趋势图表 --><Trend ref="trend"></Trend><div class="resize"><span@click="changeSize('trend')":class="['iconfont',fullScreenStatus.trend? 'icon-compress-alt': 'icon-expand-alt',]"></span></div></div><divid="left-bottom":class="[fullScreenStatus.seller ? 'fullscreen' : '']"><!-- 商家销售金额图表 --><Seller ref="seller"></Seller><div class="resize"><span@click="changeSize('seller')":class="['iconfont',fullScreenStatus.seller? 'icon-compress-alt': 'icon-expand-alt',]"></span></div></div></section><section class="screen-middle"><divid="middle-top":class="[fullScreenStatus.map ? 'fullscreen' : '']"><!-- 商家分布图表 --><Map ref="map"></Map><div class="resize"><span@click="changeSize('map')":class="['iconfont',fullScreenStatus.map ? 'icon-compress-alt' : 'icon-expand-alt',]"></span></div></div><divid="middle-bottom":class="[fullScreenStatus.rank ? 'fullscreen' : '']"><!-- 地区销量排行图表 --><Rank ref="rank"></Rank><div class="resize"><span@click="changeSize('rank')":class="['iconfont',fullScreenStatus.rank ? 'icon-compress-alt' : 'icon-expand-alt',]"></span></div></div></section><section class="screen-right"><div id="right-top" :class="[fullScreenStatus.hot ? 'fullscreen' : '']"><!-- 热销商品占比图表 --><Hot ref="hot"></Hot><div class="resize"><span@click="changeSize('hot')":class="['iconfont',fullScreenStatus.hot ? 'icon-compress-alt' : 'icon-expand-alt',]"></span></div></div><divid="right-bottom":class="[fullScreenStatus.stock ? 'fullscreen' : '']"><!-- 库存销量分析图表 --><Stock ref="stock"></Stock><div class="resize"><span@click="changeSize('stock')":class="['iconfont',fullScreenStatus.stock? 'icon-compress-alt': 'icon-expand-alt',]"></span></div></div></section></div></div>
</template><script>
import Hot from "./components/Hot.vue";
import Map from "./components/Map.vue";
import Rank from "./components/Rank.vue";
import Seller from "./components/Seller.vue";
import Stock from "./components/Stock.vue";
import Trend from "./components/Trend.vue";
import { mapState } from "vuex";
import { getThemeValue } from "@/utils/theme_utils.js";
export default {data() {return {// 定义每一个图表的全屏状态fullScreenStatus: {trend: false,seller: false,map: false,rank: false,hot: false,stock: false,},};},created() {this.$socket.registerCallBack("fullScreen", this.recvData);this.$socket.registerCallBack("themeChange", this.recvThemeChange);},destroyed() {this.$socket.unRegisterCallBack("fullScreen");this.$socket.unRegisterCallBack("themeChange");},components: {Hot,Map,Rank,Seller,Stock,Trend,},computed: {...mapState(["theme"]),headerSrc() {return "/static/img/" + getThemeValue(this.theme).headerBorderSrc;},logoSrc() {return "/static/img/" + getThemeValue(this.theme).logoSrc;},themeSrc() {return "/static/img/" + getThemeValue(this.theme).themeSrc;},containStyle() {return {backgroundColor: getThemeValue(this.theme).backgroundColor,color: getThemeValue(this.theme).titleColor,};},},methods: {changeSize(chartName) {// 全屏切换单页面写法// 改变fullScreenStatus中组件对应的属性值// this.fullScreenStatus[chartName] = !this.fullScreenStatus[chartName];// 调用每个组件里的屏幕适配方法// this.$nextTick(() => {// this.$refs[chartName].screenAdapter();// });// 同页面多端联动切换写法const targetValue = !this.fullScreenStatus[chartName];this.$socket.send({action: "fullScreen",socketType: "fullScreen",chartName: chartName,value: targetValue,});},// 处理接收到的全屏数据recvData(data) {const chartName = data.chartName;const targetValue = data.value;this.fullScreenStatus[chartName] = targetValue;this.$nextTick(() => {this.$refs[chartName].screenAdapter();});},handleChangeTheme() {// this.$store.commit("changeTheme"); //修改VueX里的数据this.$socket.send({action: "themeChange",socketType: "themeChange",chartName: "",value: "",});},recvThemeChange() {this.$store.commit("changeTheme");},},
};
</script>
<style lang="less" scoped>
// 全屏样式的定义
.fullscreen {position: fixed !important;top: 0 !important;left: 0 !important;width: 100% !important;height: 100% !important;margin: 0 !important;z-index: 100;
}
.screen-container {width: 100%;height: 100%;padding: 0 20px;background-color: #161522;color: #fff;box-sizing: border-box;
}
.screen-header {width: 100%;height: 64px;font-size: 20px;position: relative;> div {img {width: 100%;}}.title {position: absolute;left: 50%;top: 50%;font-size: 20px;transform: translate(-50%, -50%);}.title-right {display: flex;align-items: center;position: absolute;right: 0px;top: 50%;transform: translateY(-80%);}.qiehuan {width: 28px;height: 21px;cursor: pointer;}.datetime {font-size: 15px;margin-left: 10px;}.logo {position: absolute;left: 0px;top: 50%;transform: translateY(-80%);img {height: 35px;width: 128px;}}
}
.screen-body {width: 100%;height: 100%;display: flex;margin-top: 10px;.screen-left {height: 100%;width: 27.6%;#left-top {height: 53%;position: relative;}#left-bottom {height: 31%;margin-top: 25px;position: relative;}}.screen-middle {height: 100%;width: 41.5%;margin-left: 1.6%;margin-right: 1.6%;#middle-top {width: 100%;height: 56%;position: relative;}#middle-bottom {margin-top: 25px;width: 100%;height: 28%;position: relative;}}.screen-right {height: 100%;width: 27.6%;#right-top {height: 46%;position: relative;}#right-bottom {height: 38%;margin-top: 25px;position: relative;}}
}
.resize {position: absolute;right: 20px;top: 20px;cursor: pointer;
}
</style>