Node.js + express + react + echarts 从零搭建数据可视化平台
最近负责搭建公司大屏可视化平台,前端用到 vue + echarts ,后端 java 以及 大数据 提供数据支持。过程中踩过许多坑,于是准备在项目上线后,自己搭建响应式数据可视化平台。
- react
- node.js
第三方插件
- express 中间件
- echarts 图表
- Socket.io 服务
- WOW.js 动画
资源连接:https://github.com/zhangyongwnag/screen-visual
Demo:https://zhangyongwnag.github.io/screen-visual/build/index.html
客户端
一、初始化
create-react-app screen-visual
二、安装插件
npm i echarts react-countup wowjs react-transition-group -S
yarn add echarts react-countup wowjs react-transition-group
三、静态页面
因为我们最后要做成响应式布局,所以编写css要规范化,考虑自适应
我们决定网页的整体布局采用传统的圣杯布局,俩边固定,中间自适应
基础默认样式:这里由于用到很多transform动画,动画触发间接浏览器回流重绘,考虑到性能问题,这里开启硬件加速
* {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif !important;
padding: 0;
margin: 0;
user-select: none; /*禁止选中*/
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
cursor: pointer; /*鼠标指针*/
box-sizing: border-box; /*怪异盒模型*/
font-size: 18px;
transform: translateZ(0); /*硬件加速*/
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-o-transform: translateZ(0);
-ms-transform: translateZ(0);
}
考虑到自适应,我们把俩边也设为百分比
整体的布局:左边24%,中间40%,右边36%
/*最外层包裹器*/
.wrap {
width: 100vw;
min-width: 360px;
height: 100vh;
overflow: hidden;
/*filter: blur(5px);*/
background-color: #000;
display: flex;
justify-content: space-evenly;
padding: 0.5%;
transition: all 0.5s;
}
/*俩边*/
.section_side {
width: 24%;
height: 100%;
padding-bottom: 0.5%;
transition: all 0.5s;
background-color: rgba(32, 43, 71, 0.6);
-webkit-transition: border linear .2s, -webkit-box-shadow linear .5s;
border-color: rgba(141, 39, 142, .75);
-webkit-box-shadow: 0 0 7px rgba(87, 147, 243, 1);
}
/*中间*/
.section_middle {
width: 40%;
height: 100%;
margin: 0 0.5%;
padding-bottom: 0.5%;
transition: all 0.5s;
background-color: rgba(32, 43, 71, 0.6);
-webkit-transition: border linear .2s, -webkit-box-shadow linear .5s;
border-color: rgba(141, 39, 142, .75);
-webkit-box-shadow: 0 0 7px rgba(87, 147, 243, 1);
}
布局
return (
<div className='wrap'>
<div className='section_side'></div>
<div className='section_middle'></div>
<div className='section_side' style={{width: '36%'}}></div>
</div>
)
我们看到基本的布局效果:
接下来,我们把不同的echarts图表插入到页面
插入不用的图表,经过这一步,我想你对echarts的图表会有一个更深的体会,更熟练的应用(数据都是模拟)
走到这里,会发现一个问题,切换视口大小时,echarts图表不会自适应切换,我们查阅文档,发现官方提供了对应的方法:resize()
于是我们在每个echarts绘制完后,主动调用echarts.resize()方法,发现他只会在绘制的时候生效,切换视口还是不会自适应
于是找度娘,度娘给出的意见:
在每个echarts图表绘制完后,监听窗口resize,随之调用resize()方法
方法的确有效果,但是我们每绘制一个echarts图表,就要监听窗口resize,代码重复
我们尝试在页面初始化监听窗口resize,并且把绘制的echarts维护到全局的变量,当窗口大小发生变化,我们就遍历调用resize()
import React, {Component} from 'react';
import echarts from 'echarts'
export default class App extends Component {
constructor() {
super()
this.state = {
echartsList: [], // echarts绘制的结果
}
}
componentDidMount() {
// 监听窗口变化
window.addEventListener('resize', this.resizeEcharts)
}
// 绘制某个echarts图表
renderUserInfoEcharts = () => {
let dom = document.getElementById('user_info')
let eCharts = echarts.init(dom)
let option = [
...
]
eCharts.setOption(option, true)
let echartsList = [...this.state.echartsList, eCharts]
this.setState({
echartsList
})
}
// 重置echarts的大小
resizeEcharts = () => {
this.state.echartsList.map(item => {
setTimeout(() => {
item.resize()
}, 200)
})
}
根据上面的方式,我们在绘制图表时配置一遍,配置完后。我们可以看到效果图,完美自适应 ( gif制作工具没充钱,看起来比较卡,实际不卡 )
响应式
PC布局我们已经写好了,接下来,我们加几个断点制作一个简单地响应式布局
- 基准断点:
- < 1499px
- < 1199px
- < 967px
- < 748px
- < 479px
< 1499px
@media screen and (max-width: 1499px) {
...
}
< 1199px
@media screen and (max-width: 1199px) {
...
}
< 967px
@media screen and (max-width: 967px) {
...
}
< 748px
@media screen and (max-width: 748px) {
...
}
< 479px
@media screen and (max-width: 479px) {
...
}
四、动画效果
静态页面我们已经完成了,接下来,我们给网页加些动画效果
①:首次Loading加载效果
首先,我们在初始化加载页面时,加载一个loading动画,这里我们写一个Loading加载的组件
import React, {Component} from 'react'
import '../asset/css/Loading.css'
export default class Loading extends Component {
constructor(props){
super(props)
}
render () {
return (
<div style={{width:'100vw',height:'100vh',backgroundColor:'#2a2a2a'}} className='common_flex'>
<div className='loader'>Loading...</div>
</div>
)
}
}
接着在App.js引入使用,
import React, {Component} from 'react';
import Loading from ' ./components/Loading.'
export default class App extends Component {
constructor() {
super()
this.state = {
loading: true, // loading加载标识,默认加载中
}
}
componentDidMount() {
// 这里模拟加载一秒,后面我们写了服务端后,根据数据返回情况按需加载
setTimeout(() => {
this.setState({
loading: false
}, () => {
// 当loading状态发生变化立即执行
})
}, 1000)
}
render() {
let { loading } = this.state
if (loading) {
return (
<Loading/>
)
}else {
return (
<div>主内容</div>
)
}
}
}
Loading加载动画基本实现
②:元素滚动动画效果
接下来,我们给页面的每个元素加滚动动画,这里我们用到 wow.js 动画库
wowjs 动画库依赖于 animate.css 所以大部分的css都可以直接拿来使用
前面已经下载过了,直接在我们需要用到的组件引入 wowjs
import React, {Component} from 'react';
import {WOW} from ' wowjs'
export default class App extends Component {
constructor() {
super()
}
componentDidMount() {
let wow = new WOW({
boxClass: 'wow',
animateClass: 'animated',
offset: 0,
mobile: true,
live: true
});
wow.init();
}
render() {
return (
<div className=‘wow slideInLeft’ data-wow-duration="2s" data-wow-delay="5s" data-wow-offset="10" data-wow-iteration="10">动画效果</div>
)
}
自定义配置
属性/方法 | 类型 | 默认值 | 说明 |
---|---|---|---|
boxClass | String | wow | 需要执行动画的元素的 class |
animateClass | String | animated | animation.css 动画的 class |
offset | Number | 0 | 距离可视区域多少开始执行动画 |
mobile | Boolean | true | 是否在移动端执行动画 |
live | Boolean | true | 异步加载的内容是否有效 |
标签属性配置
属性 | 说明 |
---|---|
data-wow-duration=“2s” | 执行动画所需要的时间 |
data-wow-delay=“2s” | 执行动画延迟执行的时间 |
data-wow-offset=“0” | 距顶部多少开始执行动画 |
data-wow-iteration=“infinity” | 执行动画的次数 infinity无限 |
③:数字滚动效果
我们利用 react-countup 插件实现数字滚动效果,他是依赖于 countup 轻量级插件
前面已经下载,直接在所需要的组件引入使用
import React, {Component} from 'react';
import countUp from 'react-countup'
export default class App extends Component {
constructor() {
super()
}
render() {
return (
<CountUp start={0} end={parseInt(Math.random() * 10000)} suffix=' 个' duration={4} separator=','/>
)
}
属性配置
属性 | 类型 | 说明 |
---|---|---|
start | Number | 开始的值 |
end | Number | 结束的值 |
suffix | String | 单位 |
duration | Number | 动画执行时间 |
separator | String | 分隔符 |
… | … | … |
④:列表插入效果
之前的文章我们有介绍 react-transition-group 官方提供的动画库,这里就不过多介绍了
我们直接介绍用法
import React, {Component} from 'react'
import {TransitionGroup, CSSTransition} from 'react-transition-group'
export default class Table extends Component {
constructor(props) {
super(props)
this.state = {
tableList:['1','2','3','4']
}
}
render() {
let { tableList } = this.state
return (
<TransitionGroup>
{
tableList.map((item, index) => (
<CSSTransition
key={index}
timeout={1000} //动画执行1秒
classNames='fade' //自定义的class名
>
<div>{item}</div>
</CSSTransition>
))
}
</TransitionGroup>
)
}
}
这里的滚动效果,利用 transform: translateY(value)
客户端OK,接下来我们写一个简单的服务端
服务端
我们利用express中间件搭配socket.io建立通信
一、安装
npm i express socket.io -S
二、搭建
socket服务配合http服务一起使用,所以必须要http监听
let express = require('express')
let app = express()
let http = require('http').Server(app)
let io = require('socket.io')(http)
//设置CORS
app.all('*',function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, PUT');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Credentials','true');
next();
});
app.get('/', (req, res) => res.send('Hello'))
// 建立socket连接
io.on('connection', socket => {
// 发送消息标识open
socket.emit('open', '初始化连接')
// 当有用户关闭时,全体广播
socket.on('disconnect', () => {
socket.broadcast.emit('关闭')
})
})
// 这里必须http监听 否则客户端会报404
let server = http.listen(8888, '127.0.0.1', () => {
let host = server.address().address
let port = server.address().port
console.log(`Server running at http://${host}:${port}`)
})
我们启动服务端 node serve.js,可以看到,正常启动
访问 http://127.0.0.1:8888,可以看到,启动成功了
!!! 接下来,与客户端交互 !!!
客户端安装:npm i socket.io-client -S
import React, {Component} from 'react';
// 引入 socket.io-client 并建立连接 http://127.0.0.1:8888 即为node启动的服务器地址
let socket = require('socket.io-client')('http://127.0.0.1:8888')
export default class App extends Component {
constructor() {
super()
}
componentDidMount() {
// 客户端用on接受消息,open是服务端设置的标识
socket.on('open', data => {
// 如果是首次加载
if (this.state.loading) {
this.setState({
loading: false
}, () => {
// 渲染数据
...
})
}else {
// 渲染数据
...
}
})
}
render() {
return (
<div>socket.io-client 测试</div>
)
}
前后端交互基本完毕,socket 通信
相关资源
相关文章
Node.js + express + react + echarts 从零搭建数据可视化平台_nodejs数据可视化_前端小小白zyw的博客-CSDN博客
【前端可视化】如何在React中优雅的使用ECharts🍉 - 掘金
前言
这片文章由最近公司的一个可视化项目有感而发,随着前端的飞速发展,近年来数据可视化越来越火,有些公司的业务跟地图、位置、大数据等脱离不开关系,所以数据可视化甚至成了单独的一门前端行业,比如在杭州地区的前端可视化职位不但有一定的需求量且高薪,
至今为止,已经有很多的可视化框架供我们选择,比如D3、ECharts、Leaflet....等等。
本文使用的可视化框架为ECharts。
看完本文你可以学到什么?
- 如何搭建react+ts+echarts项目
- typescript基础使用
- eCharts如何在react中更安全高效的使用
- eCharts的使用
- eCharts图表尺寸自适应
- 启蒙可视化项目思想
本文的源码地址:github.com/Gexle-Tuy/e…
项目准备
技术栈为:React+TypeScript+ECharts。既然提到优雅那肯定跟TS逃离不开关系,毕竟强大的类型系统能给我的🐶💩代码保驾护航,什么?我不会TS,我不看了,别急,本文不做过于复杂的类型检查,只在组件状态(state)、属性(props)上做基本使用,不会TS也能看的懂,废话不多说,咱们开始吧。
使用的为react官方脚手架create-react-app,但是默认启动的为正常的js项目,如果想加上typescript类型检查,我们可以去它的仓库地址查看使用语法。在github上找到facebook/create-react-app。找到目录packages/cra-template-typescript。 在README中就可以看见启动命令create-react-app my-app --template typescript。
项目搭建完成之后看看src下的index文件的后缀名是否为tsx而不是jsx,为tsx就说明ts项目搭建成功了,就可以开始咱们的高雅之旅了~
初探
前面瞎吧啦半天完全跟我们本文的主角ECharts没有关系呀,伞兵作者?别急,这就开始,首先安装ECharts。
npm i echarts
安装好之后该干什么?当然是来个官方的入门例子感受一下了啦,打开官网->快速入手->绘制一个简单的图表。
可以看到,每一个图表都需要一个DOM当作容器,在React中我们可以用ref来获取到DOM实例。
发现平时正常写的ref竟然报错了,这就是强大的ts发挥了作用,我们把鼠标放上去可以发现提示框有一大堆东西。
不能将类型“RefObject<unknown>”分配给类型“LegacyRef<HTMLDivElement> | undefined”。
不能将类型“RefObject<unknown>”分配给类型“RefObject<HTMLDivElement>”。
不能将类型“unknown”分配给类型“HTMLDivElement”。
.....
可以根据它的提示来解决这个问题,将ref加上类型检查,本文不对ts做过多介绍,只使用简单的基础类型检查,我们直接给它加上一个:any。
eChartsRef:any= React.createRef();
这样报错就消失了,可以理解为any类型就是没有类型检查,跟普通的js一样没有区别。真正的重点不在这里,所以就直接使用any,其实应该按照它的提示加上真正的类型检查RefObject<HTMLDivElement>
。
拿到实例之后,直接copy官方的配置项例子过来看看效果。
import React, { PureComponent } from "react";
import * as eCharts from "echarts";
export default class App extends PureComponent {
eChartsRef: any = React.createRef();
componentDidMount() {
const myChart = eCharts.init(this.eChartsRef.current);
let option = {
title: {
text: "ECharts 入门示例",
},
tooltip: {},
legend: {
data: ["销量"],
},
xAxis: {
data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],
},
yAxis: {},
series: [
{
name: "销量",
type: "bar",
data: [5, 20, 36, 10, 10, 20],
},
],
};
myChart.setOption(option);
}
render() {
return <div ref={this.eChartsRef} style={{
width: 600,
height: 400,
margin: 100
}}></div>;
}
}
当图标的动态效果呈现在你眼前的时候是不是心动了,原来可视化这么简单,到这里你就会了最基本的使用了。
接下来就开始本文的重点!如何在react里封装图表组件动态渲染并自适应移动端。
正文
首先确定项目中我们要用到的图表,这里我选了四个最基本且常用的图表(折线图、趋势图、饼状图、柱状图)。
所有的图表都由无状态组件写(函数组件、Hooks),因为它们只负责拿到数据并渲染。并无自己维护的状态。
接下来就是封装图表组件,这里就不把四个表的代码都贴出来了,只拿一个折线图举例子。可以把拉下源码看下其他的图。
折线图:src/components/LineChart
import React, { useEffect, useRef } from 'react';
import { IProps } from "./type";
import * as echarts from "echarts";
const Index: React.FC<IProps> = (props) => {
const chartRef:any = useRef(); //拿到DOM容器
// 每当props改变的时候就会实时重新渲染
useEffect(()=>{
const chart = echarts.init(chartRef.current); //echart初始化容器
let option = { //配置项(数据都来自于props)
title: {
text: props.title ? props.title : "暂无数据",
},
xAxis: {
type: 'category',
data: props.xData,
},
yAxis: {
type: 'value'
},
series: [{
data: props.seriesData,
type: 'line'
}]
};
chart.setOption(option);
}, [props]);
return <div ref={chartRef} className="chart"></div>
}
export default Index;
同文件下新建一个type.ts,将要约束的props类型检查单独抽离出去,当然也可以直接写在index.tsx文件里面,看个人喜好。 type.ts
// 给props添加类型检查
export interface IProps {
title: string, //图表的标题(为string类型)
xData: string[], //图表x轴数据的数组(数字里面每一项都为string类型)
seriesData: number[], //跟x轴每个坐标点对应的数据(数字里面每一项都为number类型)
}
根据每张图表对应的配置项,选出你想要动态配置的属性,就可以写成props作为属性传递过来。(比如,一个项目里需要用到很多张折线图,但是每个图表的线条颜色是不一样的,就可以把color写成一个props作为属性值传递进来。)
封装好之后,我们在App.tsx中引入使用一下。
App.tsx
import React, { PureComponent } from "react";
import LineChart from "./components/LineChart/Index";
import "./App.css";
export default class App extends PureComponent {
eChartsRef: any = React.createRef();
state = {
lineChartData: {
//折线图模拟数据
xData: [
"2021/08/13",
"2021/08/14",
"2021/08/15",
"2021/08/16",
"2021/08/17",
"2021/08/18",
],
seriesData: [22, 19, 88, 66, 5, 90],
},
};
componentDidMount() {}
render() {
return (
<div className="homeWrapper">
{/* 折线图 */}
<div className="chartWrapper">
<LineChart
title="折线图模拟数据"
xData={this.state.lineChartData.xData}
seriesData={this.state.lineChartData.seriesData}
/>
</div>
</div>
);
}
}
如果使用LineChart组件的时候少传了任何一个属性,或者说属性传递的类型不对,那么就会直接报错,将报错扼杀在开发阶段,而不是运行代码阶段,而且还有一个好处就是,加上类型检查后会有强大的智能提示,普通的js项目写一个组件根本就不会提示你需要传递某些属性。
忘记传递某个属性
传递的类型不符合类型检查
效果如下:
这样一个基本的图表组件就完成了,但是都是我们模拟的数据,在真实的开发中数据都是来自于后端返回给我们,而且格式还不是我们想要的,那时候就需要我们自己处理下数据包装成需要的数据格式再传递。
这样封装成函数组件还有一个好处就是每当props改变的时候就会进行重新渲染。比如我在componentDidMount中开启一个定时器定时添加数据来模拟实时数据。
componentDidMount() {
setInterval(() => {
this.setState({
lineChartData: {
xData: [...this.state.lineChartData.xData, "2000/01/01"],
seriesData: [...this.state.lineChartData.seriesData, Math.floor(Math.random() * 100)],
}
})
}, 1500 );
}
这样就可以实现展示实时数据了,比如每秒的pv、uv数等等。我们把四个图表组件全部封装好之后的效果是这样的。
前三个图表的数据都来自实时数据模拟,最后一张饼状图直接在组件中写死数据了,有兴趣的小伙伴可以拉下源码自行把它实现成实时的,可以看option中的配置哪些需要配置的,单独抽离出来写在type.ts文件中。
移动端适配
啥?echarts没做移动端适配?当然不是,echarts的官网中就介绍了移动端的相关优化:echarts.apache.org/zh/feature.… 当然也有跨平台使用。
好像是那么回事,但感觉好像少了些什么,好像没有根据屏幕尺寸大小变化而自动发生调整尺寸。每次都要刷新一下也就是重新进入页面。
别着急,在它的API文档中,有这么一个方法,echarts创建的实例也就是通过echarts.init()之后的对象会有一个resize的方法。
我们可以监听窗口的变化,只要窗口尺寸变化了就调用resize方法。监听窗口的变化的方法很简单window.onresize
可以在创建组件对象的时候都添加上一个window.onresize方法。
注意:如果网页只有一个图表那么这么写是可以的,如果项目中图表不只一个的话,每个图表组件难道在后面都写一个window.onresize方法吗?这样写的话只有最后创建的组件会自适应屏幕尺寸大小,因为每创建一个组件都重新将window.onresize赋予为新的函数体了。
解决:我们可以写一个公用方法,每一次创建组件的时候都加入到一个数组中,当屏幕尺寸变化的时候,都去循环遍历这个数组中的每一项,然后调用resize方法。
src/util.js
const echartsDom = []; //所有echarts图表的数组
/**
* 当屏幕尺寸变化时,循环数组里的每一项调用resize方法来实现自适应。
* @param {*} eDom
*/
export function echartsResize(eDom) {
echartsDom.push(eDom);
window.onresize = () => {
echartsDom.forEach((it)=>{
it.resize();
})
};
}
写好方法之后,在每个图表组件设置好option之后将他添加到此数组内,然后当屏幕尺寸变化后就可以将每个图表变成自适应的了。
这样之后每个图表就都可以自适应屏幕尺寸啦~
结语
本文主要介绍了如何在react中更安全高效的使用eCharts,所涉及的ts都为最基础的类型检查(有兴趣的同学可以自行拓展),只是为了给各位提供一个我在写一个eCharts项目的时候如何去做和管理项目,文章有错误的地方欢迎指出,大佬勿喷,大家伙儿有更好的思路和想法欢迎大家积极留言。感谢观看~