本项目为电商平台数据可视化简易学习项目,教程详见电商平台数据可视化实时监控系统学习项目。本项目分为前端部分和后端部分,其中前端技术栈为VUE2.0 + Echarts,后端技术栈为Koa。
TIP:更多内容详见数据可视化专题!!!
cd koa_serve
npm install
node app.js
cd vision_project
npm install
npm run serve
KOA2是基于 Node.js 平台的Web服务器框架,通过
npm i koa
安装。
特点:
- 支持async/await
- 采用洋葱模型的中间件
洋葱模型中间件的调用顺序为第一层->第二层->第三层->第三层->第二层->第一层,非常类似于冒泡的行为,请求最先到达第一层中 间件,而最后也是第一层中间件对请求再次处理了一下
- Koa2 的实例对象通过 use 方法加入一个中间件
- 一个中间件就是一个函数,这个函数具备两个参数,分别是 ctx 和 next
- 内层中间件能否执行取决于外层中间件的 next 函数是否调用
- 调用 next 函数得到的是 Promise 对象, 如果想得到 Promise 所包装的数据, 可以结合 await 和 async
app.use(async (ctx, next) => {
// 刚进入中间件想做的事情
await next()
// 内层所有中间件结束之后想做的事情
})
举例:
app.use(async (ctx, next) => {
console.log('第一层中间件-1')
await next()
console.log('第一层中间件-2')
})
app.use(async(ctx, next) => {
console.log('第二层中间件-1')
await next()
console.log('第二层中间件-2')
})
app.use(async (ctx, next) => {
console.log('第三层中间件-1')
await next()
console.log('第三层中间件-2')
})
// 结果:
// 第一层中间件-1
// 第二层中间件-1
// 第三层中间件-1
// 第三层中间件-2
// 第二层中间件-2
// 第一层中间件-2
// 第一层中间件-1
// 注意上述结果会出现两次,这是因为浏览器会自动请求一次图标啥的,此处无需关注。
步骤:
- 安装 koa2 ——
npm i koa
- 编写app.js文件
// // 1.创建koa对象
const Koa = require('koa') // 导入构造方法
const app = new Koa() // 通过构造方法, 创建实例对象
// 2.编写响应函数(中间件)
app.use((ctx, next) => {
console.log(ctx.request.url)
ctx.response.body = 'hello world'
})
// 3.绑定端口号 3000
app.listen(3000)
ctx:上下文, 指的是请求所处于的Web容器,可以通过 ctx.request 拿到请求对象, 也可以通过 ctx.response 拿到响应对象
next:内层中间件执行的入口
- 启动服务器 ——
node app.js
- 在指定端口查看即可 ——
127.0.0.1:3000
开发目标:三个中间件
- 计算服务器处理请求的总耗时
- 在响应头上加上响应内容的 mime 类型(浏览器更好的来处理由服务器返回的数据)
- 根据URL读取指定目录下的文件内容
响应中间件开发
module.exports = async (ctx, next) => {
// 记录开始的时间
const start = Date.now()
// 让内层中间件执行
await next()
// 记录结束的时间
const end = Date.now()
// 计算响应时间
const duration = end - start
// 设置响应头
ctx.set("X-Response-Time", duration + "ms")
}
响应头中间件
module.exports = async (ctx, next) => {
ctx.set("Content-Type", "application/json; charset=utf-8")
ctx.set("Access-Control-Allow-Origin", "*")
ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE")
ctx.response.body = "{'success': true}"
await next()
}
业务逻辑中间件
const path = require("path")
const fileUtils = require("../utils/file_utils")
module.exports = async (ctx, next) => {
// 获取请求路径
const url = ctx.request.url;
// 转化为文件路径
const filePath = "../data" + url.replace("/api", "") + ".json"
// 转化为绝对路径
const finalPath = path.join(__dirname, filePath)
// 获取数据并设置响应体
try {
ctx.response.body = await fileUtils.getFileJsonData(finalPath)
} catch (error) {
ctx.response.body = JSON.stringify({
message: "读取文件失败!",
status: 404
})
}
await next()
}
文件读取
// 读取文件的工具方法
const fs = require('fs')
module.exports.getFileJsonData = (filePath) => {
// 根据文件的路径, 读取文件的内容
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf-8', (error, data) => {
if(error) {
// 读取文件失败
reject(error)
} else {
// 读取文件成功
resolve(data)
}
})
})
}
跨域设置
// 设置响应头
app.use(async (ctx, next) => {
ctx.set("Access-Control-Allow-Origin", "*")
ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST,
DELETE")
await next();
})
- 安装echarts
npm i echarts -S
- 在
src/main.js
文件中引入并将其挂载到 Vue 的原型对象上
import * as echarts from 'echarts';
// 将全局echarts对象挂载到Vue的原型对象上
Vue.prototype.$echarts = echarts
- 在别的组件中直接通过
this.$echarts
即可使用
- 组件结构的设计
- A组件为针对路由路径而显示的文件,其中引入了B组件
- B组件是真正显示图表的
- 布局结构的设计
- CSS样式设置
- 基本图表的实现
- initChart(初始化实例对象)
- getData(获取数据)
- updateData(即设置option)
<template>
<div class="seller-wrap com-container">
seller
<div ref="seller" class="seller-chart com-chart"></div>
</div>
</template>
<script>
export default {
name: 'Seller',
data () {
return {
chartInstance: null
}
},
mounted () {
this.initChart()
this.getData()
},
methods: {
// 初始化Echarts实例对象
initChart () {
this.chartInstance = this.$echarts.init(this.$refs.seller)
},
// 获取数据
async getData () {
// 前面axios已经挂载在main.js上,即全局,可直接使用this.$http
const res = await this.$http.get('seller')
if (res.status === 200) {
this.updateData(res.data)
}
},
// 更新数据
updateData (data) {
const option = {
// ....
}
this.chartInstance.setOption(option)
}
}
}
</script>
-
动态刷新的实现
- 根据实际需求采用不同的方法(一般采用定时器)
-
界面样式的调整
-
拆分图表option
- 初始化配置option
- 获取数据之后的dataOption
- 分辨率适配的updatOption
-
分辨率适配
- 监听窗口大小变化事件
export default {
mounted () {
window.addEventListener('resize', this.handlerResize)
}
}
综上,公共流程如下:
<template>
<div class="com-container">
<div ref="dom" class="com-chart"></div>
</div>
</template>
<script>
export default {
name: 'Demo',
data () {
return {
chartInstance: null,
data: null
}
},
mounted () {
this.initChart()
this.getData()
window.addEventListener('resize', this.handlerResize)
this.handlerResize()
},
destroyed () {
window.removeEventListener('resize', this.handlerResize)
},
methods: {
// 初始化
initChart () {
this.chartInstance = this.$echarts.init(this.$refs.dom)
const initOption = {}
this.chartInstance.setOption(initOption)
},
// 获取数据
async getData () {
const res = await this.$http.get(...)
if (res && res.status === 200) {
this.data = res.data
this.updateData()
}
},
// 更新数据
updateData () {
const dataOption = {}
this.chartInstance.setOption(dataOption)
},
// 分辨率适配
handlerResize () {
const adapterOption = {}
this.chartInstance.setOption(adapterOption)
this.chartInstance.resize()
}
}
}
</script>
<style lang='less' scoped></style>
可以保持着浏览器和客户端之间的长连接, 通过 可以实现数据由后端推送到前 端,保证了数据传输的实时性. 非常重要!!!
- 安装 WebSocket 包 ——
npm i ws -S
- 创建 WebSocket 实例对象
- 监听事件
// 后端
const WebSocket = require("ws") // 创建出WebSocket实例对象
const wss = new WebSocket.Server({
port: 9998
})
wss.on("connection", client => {
console.log("有客户端连接...")
client.on("message", msg => {
console.log("客户端发送数据过来了")
client.send('hello socket') // 发送数据给客户端
})
})
// 前端(WebSocket是window对象提供,不需要额外包)
const ws = new WebSocket('ws://localhost:9998') // 服务器地址
ws.onopen = () => {
console.log('连接服务器成功')
}
ws.onmessage = msg => {
console.log('从服务器接收到了数据')
// msg.data
}
ws.onclose = e => {
console.log('服务器关闭了连接')
}
略,详见代码。
基础流程:
- 定义类 SocketService,并设计单例模式
- 创建websocket对象
- 定义连接服务器方法,并在main.js中调用
- 监听事件 - onopen、onmessage、onclose
- 定义存储回调函数对象、注册回调函数方法、删除回调函数方法
- 接受数据处理:onmessage中调用之前注册的回调函数
- 定义发送数据的方法
- 将SocketService对象挂在到Vue原型对象上,方便在组建中直接使用相关方法
优化机制:
- 发送数据需要判断是否连接,如果未连接则要进行轮询
// 单例模式
export default class SocketService {
static instance = null
static 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:9999')
// 连接成功的事件
this.ws.onopen = () => {
console.log('连接服务端成功了')
this.connected = true
// 重置重新连接的次数
this.connectRetryCount = 0
}
// 连接服务端失败 或 当连接成功之后服务器关闭的情况
this.ws.onclose = () => {
console.log('连接服务端失败')
this.connected = false
this.connectRetryCount++
setTimeout(() => {
this.connect()
}, 500 * this.connectRetryCount)
}
// 得到服务端发送过来的数据
this.ws.onmessage = msg => {
console.log('从服务端获取到了数据')
const recvData = JSON.parse(msg.data)
const socketType = recvData.socketType
// 判断回调函数是否注册
if (this.callBackMapping[socketType]) {
const action = recvData.action
if (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 = 0
this.ws.send(JSON.stringify(data))
} else {
this.sendRetryCount++
setTimeout(() => {
this.send(data)
}, this.sendRetryCount * 500)
}
}
}
- created - 注册回调函数
- destoryed - 取消回调函数
- 原本获取数据地方改为websocket发送数据
如果发现本项目有错误,欢迎提交 issues 指正,也可联系邮箱EnvisionShen@gmail.com。