react+redux+router+echarts可视化项目总结

写在前面

项目来自于今年中国软件杯赛题:基于WIFI探针的商业大数据分析技术
赛题包括探针程序,数据分析程序,展示界面等,我做的是可视化展示部分和后台管理页面
这个仓库是数据可视化程序,将分析好的数据可视化的展示出来
因为后台是java写的,为了简单的产生数据,我用node写了个小后台,提供数据
可直接到我的服务器看:47.93.254.91:3000
源码地址:https://github.com/ooooevan/react-redux-echarts

功能结构

调试运行

1
npm run dev

打开数据后台

1
2
cd server/demo1
npm start

我已经把打包好的dist文件拷贝到server的静态目录下,所以打开数据后台可以直接访问localhost:3000看到页面

修改后打包

1
npm run deploy

生成的文件在dist目录中。
因为是windows下,需要手动复制静态文件:
lib目录复制到dist目录下,将favicon.ico复制到dist目录下。

页面截图



使用的相关技术

  • 使用es6写法,安装一堆babel插件(现在有babel-preset-env可以少很多了)
  • react、react-redux、react-router,页面结构较多,这样用是比较合适的。当然也可以用mobx
  • 使用了不可变数据,因为要渲染的数据层级深,用immutable很有必要
  • 使用webpack2做模块化,打包,做了代码分割加快首屏渲染
  • 使用百度的echarts做可视化

总体分析

目录结构

1
2
3
4
lib:存放一些静态资源,这里我放了logo图片和字体文件
server:调试时提供数据的node后台
src:放代码的地方
test:编写测试用例


再看src目录下的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
根据功能分为4个模块,分别为:firstPage、sellers、statics、compare
对应概况、商家、统计分析、数据对比4个模块
后面改了页面结构,所以这里的结构和调试页面会不一样,47.93.254.91:3000的页面是旧代码的,和这个结构完全对应
─ src
├── actions //分别是4个模块各自的action
│ ├── compareAction.js
│ ├── firstPageAction.js
│ ├── sellersAction.js
│ ├── staticsAction.js
├── components //放各个模块的组件
│ ├── compare
│ ├── firstPage
│ ├── sellers
│ ├── statistics
│ ├── app.js //入口文件
│ ├── calendar.js //一个日历组件
│ ├── devTool.js //一个调试插件,代码已经注释
│ ├── general.js //进入4个个模块的Nav组件
│ ├── notFindPage.js //404页面
│ ├── tools.js //几个工具函数
├── constants //放actionapi和actionType
├── options //echarts对不同图形需要不同配置,都写在这里
├── reducers
├── store
├── styles

数据流向

总体流向

使用的是redux,当然是 components->actions->reducers->components

举个具体的例子

打开一个组件

要经过的地方:

1
2
3
4
5
6
1. component执行生命周期,componentWillMount里产生一个获取数据的action
2. action中发送一个异步请求
3. component继续生命周期函数,render出页面骨架,调用echarts的init和showLoading,此时可以看到页面转圈提示加载中
4. action中服务器返回数据,调用dispatch把此次action交给reducer处理
5. reducer把原state和服务器的数据更新合并,交给component以渲染
6. component中更新了数据,更新组件,执行echarts的setOption和hideLoading方法,渲染出了数据

所有的数据流向都是这样,很清晰

项目遇到的问题

1. echarts实例对象放哪里

因为ehcarts要先初始化才能显示一个loading的图像,等数据返回才能渲染数据,但是这个实例化的对象放哪里呢?

1
2
3
4
5
echartsExample = echarts.init(targetDom);
echartsExample.showLoading();
...
echartsExample.setOption(Data);
echartsExample.hideLoading();

一开始我把这个对象跟数据的流向一起,把对象当做参数,带到action再到reducer。
虽然这样也成功了,但是我发现,reducer是纯函数啊,怎么可以做这么不纯洁的操作呢,然后才想到,应该存在组件的state中。。

1
2
3
4
5
this.state.echartsExample = echarts.init(targetDom);
this.state.echartsExample.showLoading();
...
this.state.echartsExample.setOption(Data);
this.state.echartsExample.hideLoading();

因为这个对象本身不是用来显示数据的,所以不用this.setState()而是直接赋值
这应该是个很弱智的问题,但是当时就是困扰了我。。

2. 数据层级深导致数据出问题

echarts要求的数据格式层级很深,随便就3,4层嵌套(应该所有的可视化数据都是这么深)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sellersNum: {
...
color: ['#c23531', '#de9a48'],
title: {
text: '客流量峰值对比',
show: false
},
toolbox: {
feature: {
dataView: {show: true, readOnly: false},
magicType: {show: true, type: ['line', 'bar']},
restore: {show: true},
saveAsImage: {show: true}
}
},
...
}

在没有引入immutable时,我想让数据尽量快点渲染,在componentWillReceiveProps里执行渲染操作,却犯了个错误

1
2
3
4
5
componentWillReceiveProps(){
//这个错误就是不应该用this.props.data,这是旧的数据,应该用nextProps.data
this.state.echartsExample.setOption(this.props.data);
this.state.echartsExample.hideLoading();
}

当我发现这个错误时,却发现这么大错误没有导致页面任何异常。。
通过debugger才知道,原来我执行reducer时更改了原state,所以到这里时,this.props就等于nextProps
而更改了原state,肯定就是因为数据层级嵌套太深导致的。所以才引入的immutable,不改变原数据,返回新数据

1
2
3
4
5
//若层级不深时,可以用Object.assign、slice、concat等方法更新state
//当层级太深,怎么都显得太麻烦,用了immutable就很清晰了:
return state.setIn(['customerNum', 'xAxis', 0, 'data'], data.time)
.setIn(['customerNum', 'series', 0, 'data'], data.num1)
.setIn(['customerNum', 'series', 1, 'data'], data.num2);

immutable和原生js对象不一样,不能互相使用方法的方法,所以引入immutable要多记几个api

1
2
3
let immutableObj = immutable.fromJS(obj); //原始对象转为immutable对象
let nativeObj = immutableObj.toJS(); //immutable对象转为原始对象
...

两种对象互转比较消耗性能,能不转就不转,而且如果要在shouldComponentUpdate手动判断更新的话,用immutable很容易。
我这里的转化:reducer拿到服务器数据即转为immutable->更新state->组件使用->echarts渲染函数接收的参数要原始对象,所以在这里转为原始对象
immutable对象也有像原始对象那样一系列函数如map、forEach等,基本满足数据渲染要求,所以如果项目中没有echarts这样必须使用原始对象的,可以一直用immutable对象
还有就是combinereducer不支持immutable,需要用redux-immutable库重写了的combinereducer

3. 打包文件过大

打包生成最终文件,好几兆,太大,于是寻找减少大小的方法

  1. webpack配置压缩文件

    1
    2
    3
    4
    5
    6
    7
    8
    new webpack.optimize.UglifyJsPlugin({
    output:{
    comments:false, //去掉所以注释
    },
    compress:{
    warnings:false
    }
    }),
  2. 代码切割,根据路由分成多个小文件,按需加载
    有两个地方需要改:
    1、路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //原来
    import Statistics from './statistics/statistics';
    <Route path="statistics" component={Statistics} />
    //现在
    const Statistics = (location, callback) => {
    require.ensure([], (require) => {
    callback(null, require('./statistics/statistics').default);
    }, 'statistics');
    };

2.webpack配置

1
2
3
4
5
6
7
8
9
10
entry: {
app: './src/components/app.js',
vendors:['react','react-dom']
},
output: {
path: path.join(__dirname, 'dist/'),
publicPath: '',
filename: '[name]-[chunkhash:8].js',
chunkFilename: '[name]-[chunkhash:8].js' //路由对应文件,chunkhash是将文件内容哈希
},

这样能分隔出多个文件,加载时只需加载公共的vendors和此路由需要的js文件,大大加快了首屏渲染
并且,用chunkhash能做版本控制,在修改代码后,只会使修改的部分文件的哈希改变,其他文件都没有变,这样,只需要看名字有没有变就可以知道哪些文件更新了,对部署很有帮助,请看:大公司里怎样开发和部署前端代码?

其他

函数去抖

因为echarts图像是canvas绘制的,当改变浏览器窗口大小时发现canvas不会变,此时要重新绘制图像。调用echarts提供的方法。并且是在window.resize触发时执行

1
this.state.allSellersLineChart.resize();

仅仅这样会发现,改变窗口大小时,会一直触发这个函数,就行onmousemove那样一直触发,这样就很浪费性能,严重会引起假死状态,所以要用函数去抖(类似还有函数节流)

1
2
3
4
5
6
7
8
9
10
11
window.addEventListener('resize', this.resizeFun);
resizeFun = () => {
if (this.state.resizeHandler) {
clearTimeout(this.state.resizeHandler);
}
if (this.state.allSellersLineChart) {
this.state.resizeHandler = setTimeout(() => {
this.state.allSellersLineChart.resize();
}, 100);
}
}

代码很简单,监听resize事件,然后设置定时器,只要一定时间范围内又触发,则重新计时,知道最后不触发了再执行

缺点

由于是已经结束的项目,就想不再修改了,但是缺点还是在的

  • 最大的缺点就是组件重用,大部分图形组件都是一样的,是可以共用一个组件,将数据传入进去的,我却一个图形一个组件,所以项目的这么多图像组件基本都是一样的
  • scss没有模块化,开始想共用一些代码就没有分离,然后变成了全部样式全挤在一个文件里。。
  • propTypes没写好,没有用typescript,类型检查是很有必要的,但是我偷懒很多都没写
  • 代码比较乱,很多代码和注释没有去掉

这个部分是比较简单的,业务很简单(其实还有个管理后台,因为没有写后台提供数据就不放出来了)。但对react的理解还是挺有帮助的,写代码时另一大问题就是react生命周期、路由等问题,写着写着就懂一点了。
这是过了挺久才做的总结,所以有些都忘记了,只能写这么多,如果有错,望指正