一、首先介绍一下数据可视化Echarts
数据可视化, 说白了, 就是把数据以更加直观的方式进行呈现. 那什么方式是更加直观的方式呢? 就是图表.
ECharts是百度公司开源的一个使用 JavaScript 实现的开源可视化库,兼容性强,底层依赖矢量图形库 ZRender ,提供直观,交互丰富,可高度个性化定制的数据可视化图表。它的优点是:开源免费,功能丰富,移动端的优化,跨平台……
下面展示一下基本的echarts的图表,这里是 html+js 实现的效果,这里是本人初学echarts的练手项目:
可见echarts的强大,它可以使苍白无力的文字变得可视化。
二、关于后端 Koa2的使用
关于本项目,是前后端分离项目,前端使用vue+echarts,后端使用koa2,获取数据使用 axios(阿贾克斯), 项目起初是前端利用axios向后端发送数据请求,后来加以完善,转变成后端向前端推送数据。
关于koa2,是基于 Node.js 平台的Web服务器框架,利用洋葱模型的中间件,进行处理请求。
本项目利用koa2实现以下功能:
1.计算服务器处理请求的总耗时
计算出服务器对于这个请求它的所有中间件总耗时时长究竟是,我们需要计算一下
2.在响应头上加上响应内容的 mime 类型
加入mime类型, 可以让浏览器更好的来处理由服务器返回的数据.
如果响应给前端浏览器是 json 格式的数据,这时候就需要在咱们的响应头当中增加 Content-
Type 它的值就是 application/json , application/json 就是 json 数据类型的 mime 类型
3.根据URL读取指定目录下的文件内容
为了简化后台服务器的代码,前端图表所要的数据, 并没有存在数据库当中,而是将存在文件当中
的,这种操作只是为了简化咱们后台的代码. 所以咱们是需要去读取某一个目录下面的文件内容
的。
每一个目标就是一个中间件需要实现的功能, 所以后台项目中需要有三个中间件
下面是三个中间件的代码逻辑:
// 处理业务逻辑的中间件,读取某一个json文件数据
const path = require('path')
const fileUtils = require('../utils/file_utils')
module.exports = async (ctx,next) => {
// 获取请求路径,拼接路径 , 读取文件内容
//console.log(ctx.request.url); // 属于端口路径 ../data/seller.json
const url = ctx.request.url
let filePath = url.replace('/api','') // /seller
filePath = '../data' + filePath + '.json' // 可以读取吗?
filePath = path.join(__dirname,filePath)
try{
const ret = await fileUtils.getFileJsonData(filePath) // 语法糖操作
ctx.response.body = ret
} catch(error) {
const errorMsg = {
message:'读取文件失败,文件资源不存在',
status:404
}
ctx.response.body = JSON.stringify(errorMsg) // Json数据的转化
}
//console.log(filePath);
await next()
// 设置响应体 通过 ctx
}
// 计算服务器消耗时长的中间件
module.exports = async (ctx, next) => {
// 记录开始时间
const start = Date.now()
// 让内层中间件得到执行
await next()
// 记录结束的时间
const end = Date.now() // 打错字
// 设置响应头 X-Response-Time
const duration = end - start
// ctx.set 设置响应头
ctx.set('X-Response-Time', `${duration} ms`) // es6的模板字符串
}
// 设置响应头的中间件
module.exports = async (ctx,next) =>{
const contentType = 'application/json; charset=utf-8'
ctx.set('Content-Type', contentType)
// 解决跨域的问题 设置请求头
ctx.set("Access-Control-Allow-Origin", "*")
ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE") // 配置请求头的操作
await next()
}
其中包括设置响应头处理跨域的方法。
三、关于 前端 vue
整个项目的架构是基于 Vue 的, 所以我们需要创建 Vue 项目, 然后在 Vue 项目中开发各个图表组件.涉及技术栈 :vue-cli、vue-router、vuex、node.js 、echarts、axios
目录结构:
前端项目准备:
1.vue-cli脚手架创建项目
2.全局echarts对象
3.axios的处理(原型链挂载)
4.单独图表组件的开发
将从这四个方面进行前端项目的编写:
图表的设计流程:
1.基本图表
2.分页动画
3.UI调整
4.分辨率适配
因为实现起来比较容易,代码就不发出来了。
下面分享关键步骤的代码:
1.main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// 引入全局的样式文件
import './assets/css/global.less'
import './assets/font/iconfont.css' // 引入字体文件
import axios from 'axios'
import SocketService from './utils/socket_service'
// 请求基准路径的配置
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/' // 这里是进行了基础配置 并不是我们所需要的跨域解决
// 将axios挂载到Vue的原型对象上
// 在别的组件中 this.$http
Vue.prototype.$http = axios
// 将全局的echarts对象挂载到Vue的原型对象上
// 别的组件中 this.$echarts
Vue.prototype.$echarts = window.echarts
Vue.config.productionTip = false
// 连接到后台WebSocket服务器
SocketService.Instacne.connect()
// 把SocketService实例挂载到Vue原型上
Vue.prototype.$socket = SocketService.Instacne
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
2.map.vue(图表)
<template>
<div class="com-container" @dblclick="resetMap">
<div class="com-chart" ref="mapRef"></div>
</div>
</template>
<script>
// import { getChinaMapData, getMapData, getProvinceMapData } from '@/api/map'
import { getChinaMapData, getProvinceMapData } from '@/api/map'
import { getProvinceMapInfo } from '@/utils/map_utils'
import { mapGetters } from 'vuex'
export default {
data () {
return {
echartInstance: null,
allData: null,
mapData: {} // 所获取的省份的地图矢量数据
}
},
computed: {
...mapGetters(['getTheme']) // 扩展运算符
},
watch: {
getTheme () {
this.echartInstance.dispose() // 销毁之前的echarts实例
this.initChart() // 重新创建echarts实例
this.screenAdapter() // 重新进行屏幕适配
this.updateChart() // 重新绘制图表
}
},
created () {
this.$socket.registerCallBack('mapData', this.getData) // 后端向前端推送数据
},
mounted () {
this.initChart()
// this.getData()
this.$socket.send({ // 实现多端联动的效果
action: 'getData',
chartName: 'map',
socketType: 'mapData',
value: ''
})
window.addEventListener('resize', this.screenAdapter) // 屏幕适配
this.screenAdapter()
},
destroyed () {
window.removeEventListener('resize', this.screenAdapter)
this.$socket.unRegisterCallBack('mapData')
},
methods: {
async initChart () {
this.echartInstance = this.$echarts.init(this.$refs.mapRef, this.getTheme)
this.echartInstance.on('click', async arg => {
console.log(arg.name)
const provinceMapInfo = getProvinceMapInfo(arg.name) // 导出拼音
if (provinceMapInfo.key === undefined) {
return 0 // bug问题 如果不存在这个省份 就直接退出 不进行数据的请求
}
try {
if (!this.mapData[provinceMapInfo.key]) {
const res = await getProvinceMapData(provinceMapInfo.path)
this.$echarts.registerMap(provinceMapInfo.key, res.data)
this.mapData[provinceMapInfo.key] = res.data
}
const changeOption = {
geo: {
map: provinceMapInfo.key
}
}
this.echartInstance.setOption(changeOption)
} catch (err) {
console.log(err)
}
})
const res = await getChinaMapData()
this.$echarts.registerMap('china', res.data)
const initOption = {
title: {
text: '▎商家分布',
left: 20,
top: 20
},
legend: {
left: '5%',
bottom: '5%',
orient: 'vertical'
},
geo: {
type: 'map',
map: 'china',
top: '5%',
bottom: '5%',
itemStyle: {
areaColor: '#2E72BF',
borderColor: '#333'
}
}
}
this.echartInstance.setOption(initOption)
},
// async getData () {
getData (res) {
// const res = await getMapData()
// const legendData = res.data.map(item => item.name)
// const seriesArr = res.data.map(item => {
this.allData = res
this.updateChart()
},
updateChart () {
const legendData = this.allData.map(item => item.name)
const seriesArr = this.allData.map(item => {
return {
type: 'effectScatter',
name: item.name,
data: item.children,
coordinateSystem: 'geo',
rippleEffect: {
scale: 5,
brushType: 'stroke'
}
}
})
const dataOption = {
legend: {
data: legendData
},
series: seriesArr
}
this.echartInstance.setOption(dataOption)
},
screenAdapter () {
const titleFontSize = (this.$refs.mapRef.offsetWidth / 100) * 3.6
const adapterOption = {
title: {
textStyle: {
fontSize: titleFontSize
}
},
legend: {
itemWidth: titleFontSize / 2,
itemHeight: titleFontSize / 2,
itemGap: titleFontSize / 2,
textStyle: {
fontSize: titleFontSize / 2
}
}
}
this.echartInstance.setOption(adapterOption)
this.echartInstance.resize()
},
resetMap () { // 双击返回
const changeOption = {
geo: {
map: 'china'
}
}
this.echartInstance.setOption(changeOption)
}
}
}
</script>
3.socket约束
// 处理与websocket进行的数据
export default class SocketService {
// 单利设计模式
static instance = null
static get Instacne () {
if (!this.instance) {
this.instance = new SocketService()
}
return this.instance
}
ws = null
connected = false
// 发送失败之后尝试的次数
sendTryCount = 0
// 连接失败之后尝试的次数
connectTryCount = 0
// 存储回调函数
callBackMapping = {}
registerCallBack = (type, callBackFunc) => {
this.callBackMapping[type] = callBackFunc
}
unRegisterCallBack = type => {
delete this.callBackMapping[type]
}
connect () {
// 连接服务器
if (!window.WebSocket) {
alert('您的浏览器不支持WebSocket')
return
}
this.ws = new WebSocket('ws://127.0.0.1:9998')
// this.ws = new WebSocket(process.env.VUE_APP_SOCKETURL)
// 连接成功的事件
this.ws.onopen = () => {
this.connected = true
this.connectTryCount = 0
}
// 连接失败的事件
this.ws.onclose = () => {
console.log('连接失败,请重试...')
this.connectTryCount++
this.connected = false
// 失败之后尝试连接
setTimeout(() => {
this.connect()
}, this.connectTryCount * 500)
}
this.ws.onmessage = msg => {
const msgObj = JSON.parse(msg.data)
if (msgObj.action === 'getData') {
if (msgObj.socketType) {
this.callBackMapping[msgObj.socketType].call(
this,
JSON.parse(msgObj.data)
)
}
} else if (msgObj.action === 'fullScreen') {
this.callBackMapping[msgObj.socketType].call(this, msgObj)
} else if (msgObj.action === 'changeTheme') {
this.callBackMapping[msgObj.socketType].call(this, msgObj)
}
}
}
// 发送数据给服务器
send = data => {
if (this.connected) {
this.ws.send(JSON.stringify(data))
} else {
this.sendTryCount++
// 尝试再次发送数据
setTimeout(() => {
this.send(data)
}, this.sendTryCount * 500)
}
}
}
四、项目运行结果
本项目可进行多端数据联动、支持主题切换效果
CODE FOR BETTER 唤醒代码的力量!!!