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🍉 - 掘金

14007

前言

这片文章由最近公司的一个可视化项目有感而发,随着前端的飞速发展,近年来数据可视化越来越火,有些公司的业务跟地图、位置、大数据等脱离不开关系,所以数据可视化甚至成了单独的一门前端行业,比如在杭州地区的前端可视化职位不但有一定的需求量且高薪,

截屏2021-08-25 下午10.57.37.png

至今为止,已经有很多的可视化框架供我们选择,比如D3EChartsLeaflet....等等。

本文使用的可视化框架为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。 image

项目搭建完成之后看看src下的index文件的后缀名是否为tsx而不是jsx,为tsx就说明ts项目搭建成功了,就可以开始咱们的高雅之旅了~

初探

前面瞎吧啦半天完全跟我们本文的主角ECharts没有关系呀,伞兵作者?别急,这就开始,首先安装ECharts。

 
js
复制代码
npm i echarts

安装好之后该干什么?当然是来个官方的入门例子感受一下了啦,打开官网->快速入手->绘制一个简单的图表。

可以看到,每一个图表都需要一个DOM当作容器,在React中我们可以用ref来获取到DOM实例。

image

发现平时正常写的ref竟然报错了,这就是强大的ts发挥了作用,我们把鼠标放上去可以发现提示框有一大堆东西。

 
js
复制代码
不能将类型“RefObject<unknown>”分配给类型“LegacyRef<HTMLDivElement> | undefined”。
不能将类型“RefObject<unknown>”分配给类型“RefObject<HTMLDivElement>”。
不能将类型“unknown”分配给类型“HTMLDivElement”。
.....

可以根据它的提示来解决这个问题,将ref加上类型检查,本文不对ts做过多介绍,只使用简单的基础类型检查,我们直接给它加上一个:any。

 
js
复制代码
eChartsRef:any= React.createRef();

这样报错就消失了,可以理解为any类型就是没有类型检查,跟普通的js一样没有区别。真正的重点不在这里,所以就直接使用any,其实应该按照它的提示加上真正的类型检查RefObject<HTMLDivElement>

拿到实例之后,直接copy官方的配置项例子过来看看效果。

 
js
复制代码
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>;
  }
}

gif

当图标的动态效果呈现在你眼前的时候是不是心动了,原来可视化这么简单,到这里你就会了最基本的使用了。

接下来就开始本文的重点!如何在react里封装图表组件动态渲染并自适应移动端

正文

首先确定项目中我们要用到的图表,这里我选了四个最基本且常用的图表(折线图趋势图饼状图柱状图)。

所有的图表都由无状态组件写(函数组件、Hooks),因为它们只负责拿到数据并渲染。并无自己维护的状态。

接下来就是封装图表组件,这里就不把四个表的代码都贴出来了,只拿一个折线图举例子。可以把拉下源码看下其他的图。

折线图:src/components/LineChart

 
js
复制代码
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

 
js
复制代码
// 给props添加类型检查
export interface IProps {
    title: string,          //图表的标题(为string类型)
    xData: string[],        //图表x轴数据的数组(数字里面每一项都为string类型)
    seriesData: number[],   //跟x轴每个坐标点对应的数据(数字里面每一项都为number类型)
}

根据每张图表对应的配置项,选出你想要动态配置的属性,就可以写成props作为属性传递过来。(比如,一个项目里需要用到很多张折线图,但是每个图表的线条颜色是不一样的,就可以把color写成一个props作为属性值传递进来。)

封装好之后,我们在App.tsx中引入使用一下。

App.tsx

 
js
复制代码
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项目写一个组件根本就不会提示你需要传递某些属性。

忘记传递某个属性 image

传递的类型不符合类型检查 image

效果如下:

gif

这样一个基本的图表组件就完成了,但是都是我们模拟的数据,在真实的开发中数据都是来自于后端返回给我们,而且格式还不是我们想要的,那时候就需要我们自己处理下数据包装成需要的数据格式再传递。

这样封装成函数组件还有一个好处就是每当props改变的时候就会进行重新渲染。比如我在componentDidMount中开启一个定时器定时添加数据来模拟实时数据。

 
js
复制代码
componentDidMount() {
    setInterval(() => {
        this.setState({
            lineChartData: {
                xData: [...this.state.lineChartData.xData, "2000/01/01"],
                seriesData: [...this.state.lineChartData.seriesData, Math.floor(Math.random() * 100)],
            }
        })
    }, 1500 );
}

gif

这样就可以实现展示实时数据了,比如每秒的pv、uv数等等。我们把四个图表组件全部封装好之后的效果是这样的。

gif

前三个图表的数据都来自实时数据模拟,最后一张饼状图直接在组件中写死数据了,有兴趣的小伙伴可以拉下源码自行把它实现成实时的,可以看option中的配置哪些需要配置的,单独抽离出来写在type.ts文件中。

移动端适配

啥?echarts没做移动端适配?当然不是,echarts的官网中就介绍了移动端的相关优化:echarts.apache.org/zh/feature.… 当然也有跨平台使用。

gif

好像是那么回事,但感觉好像少了些什么,好像没有根据屏幕尺寸大小变化而自动发生调整尺寸。每次都要刷新一下也就是重新进入页面。

别着急,在它的API文档中,有这么一个方法,echarts创建的实例也就是通过echarts.init()之后的对象会有一个resize的方法。

我们可以监听窗口的变化,只要窗口尺寸变化了就调用resize方法。监听窗口的变化的方法很简单window.onresize可以在创建组件对象的时候都添加上一个window.onresize方法。

注意:如果网页只有一个图表那么这么写是可以的,如果项目中图表不只一个的话,每个图表组件难道在后面都写一个window.onresize方法吗?这样写的话只有最后创建的组件会自适应屏幕尺寸大小,因为每创建一个组件都重新将window.onresize赋予为新的函数体了。

解决:我们可以写一个公用方法,每一次创建组件的时候都加入到一个数组中,当屏幕尺寸变化的时候,都去循环遍历这个数组中的每一项,然后调用resize方法。

src/util.js

 
js
复制代码
const echartsDom = [];  //所有echarts图表的数组
/**
 * 当屏幕尺寸变化时,循环数组里的每一项调用resize方法来实现自适应。
 * @param {*} eDom 
 */
export function echartsResize(eDom) {   
    echartsDom.push(eDom);
    window.onresize = () => {
        echartsDom.forEach((it)=>{
            it.resize();
        })
    };
}

写好方法之后,在每个图表组件设置好option之后将他添加到此数组内,然后当屏幕尺寸变化后就可以将每个图表变成自适应的了。

这样之后每个图表就都可以自适应屏幕尺寸啦~

gif

结语

本文主要介绍了如何在react中更安全高效的使用eCharts,所涉及的ts都为最基础的类型检查(有兴趣的同学可以自行拓展),只是为了给各位提供一个我在写一个eCharts项目的时候如何去做和管理项目,文章有错误的地方欢迎指出,大佬勿喷,大家伙儿有更好的思路和想法欢迎大家积极留言。感谢观看~

posted @ 2023-08-11 16:32  CharyGao  阅读(367)  评论(0编辑  收藏  举报