2024-08-06

用three.js+echarts给公司写了一个站点数据大屏系统经验总结

一、背景与问题

在数字化转型浪潮下,企业数据可视化呈现需求日益增长。我们公司需要构建一个大型数据大屏系统,用于实时展示多个业务站点的运行状态和关键指标。传统二维图表难以满足立体化、动态化、交互化的展示需求,而单纯使用three.js或echarts也存在局限性。

在项目初期,我们面临三个核心挑战:

  1. 如何在保持高渲染性能的前提下实现复杂3D可视化
  2. 如何高效整合3D图形与动态数据图表
  3. 如何在大型屏幕中实现多图层的视觉统一

传统方案通常采用单一技术栈,但结合three.js和echarts的混合方案,能充分发挥两者优势:three.js处理复杂3D模型和空间可视化,echarts处理动态数据图表和交互逻辑。这种技术组合需要解决跨库通信、渲染性能优化、视觉风格统一等关键问题。

二、基本原理

three.js是基于WebGL的3D图形库,通过顶点着色器和片段着色器实现复杂的三维渲染。echarts是基于Canvas的图表库,通过数据驱动的方式生成2D图表。两者结合的关键在于:

  1. 渲染上下文分离:three.js使用WebGL上下文,echarts使用Canvas上下文,需要通过DOM元素进行空间定位
  2. 坐标系转换:WebGL和Canvas的坐标系存在差异,需要进行空间映射
  3. 性能优化:避免频繁重绘,合理管理资源加载

三、环境准备

技术栈选型

  • three.js v0.154.0(最新稳定版)
  • echarts v5.4.1(支持WebGL渲染)
  • TypeScript v4.7.4
  • Webpack v5.79.0

项目结构

.
├── src
│   ├── main
│   │   ├── components
│   │   │   ├── ThreeScene.tsx     # three.js场景组件
│   │   │   ├── EChartsChart.tsx  # echarts图表组件
│   │   │   └── DataPanel.tsx     # 数据面板组件
│   │   ├── services
│   │   │   └── DataService.ts    # 数据服务模块
│   │   ├── utils
│   │   │   └── domUtils.ts       # DOM操作工具
│   │   └── App.tsx               # 入口组件
│   └── assets
│       └── models/               # 3D模型资源
└── package.json

四、核心实现

1. three.js场景初始化

// ThreeScene.tsx
import * as THREE from 'three';

interface ThreeSceneProps {
  container: HTMLElement;
  width: number;
  height: number;
}

export default function ThreeScene({ container, width, height }: ThreeSceneProps) {
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(width, height);
  container.appendChild(renderer.domElement);

  // 创建立方体
  const geometry = new THREE.BoxGeometry();
  const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
  const cube = new THREE.Mesh(geometry, material);
  scene.add(cube);

  // 灯光设置
  const light = new THREE.DirectionalLight(0xffffff, 1);
  light.position.set(1, 1, 1);
  scene.add(light);

  // 动画循环
  function animate() {
    requestAnimationFrame(animate);
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;
    renderer.render(scene, camera);
  }
  animate();
}

关键点解释:

  • 使用PerspectiveCamera创建透视投影
  • 通过MeshStandardMaterial实现真实光照效果
  • 通过DirectionalLight模拟自然光
  • 使用requestAnimationFrame实现流畅动画

2. echarts图表集成

// EChartsChart.tsx
import * as echarts from 'echarts';

interface EChartsProps {
  container: HTMLElement;
  width: number;
  height: number;
  data: number[];
}

export default function EChartsChart({ container, width, height, data }: EChartsProps) {
  const chart = echarts.init(container);
  
  // 配置项
  const option = {
    title: { text: '实时数据' },
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: ['A', 'B', 'C', 'D', 'E'] },
    yAxis: { type: 'value' },
    series: [{
      name: '数据',
      type: 'line',
      data: data,
      showSymbol: false,
      lineStyle: { width: 2 }
    }]
  };
  
  chart.setOption(option);
  
  // 动态更新
  setInterval(() => {
    const newData = data.map((v, i) => v + (Math.random() - 0.5) * 10);
    chart.setOption({ series: [{ data: newData }] });
  }, 2000);
}

关键点解释:

  • 使用echarts.init创建图表实例
  • 配置项包含基本图表结构
  • 通过setInterval实现动态数据更新
  • 使用showSymbol: false优化图表显示

3. 跨库坐标映射

// domUtils.ts
export function getBoundingClientRectInThree(
  threeElement: HTMLElement, 
  canvas: HTMLCanvasElement
): DOMRect {
  const threeRect = threeElement.getBoundingClientRect();
  const canvasRect = canvas.getBoundingClientRect();
  
  // 计算坐标映射
  const scaleX = threeRect.width / canvasRect.width;
  const scaleY = threeRect.height / canvasRect.height;
  
  return {
    left: threeRect.left - canvasRect.left,
    top: threeRect.top - canvasRect.top,
    width: threeRect.width,
    height: threeRect.height,
    scaleX,
    scaleY
  };
}

关键点解释:

  • 解决three.js和echarts的坐标系差异
  • 计算元素在屏幕中的相对位置
  • 为后续交互逻辑提供坐标基准

五、完整案例

1. 主界面布局

// App.tsx
import React, { useEffect, useRef } from 'react';
import ThreeScene from './components/ThreeScene';
import EChartsChart from './components/EChartsChart';

export default function App() {
  const threeRef = useRef<HTMLDivElement>(null);
  const chartRef = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    // 初始化three.js场景
    if (threeRef.current) {
      const threeScene = new ThreeScene({
        container: threeRef.current,
        width: 800,
        height: 600
      });
    }
    
    // 初始化echarts图表
    if (chartRef.current) {
      const chart = new EChartsChart({
        container: chartRef.current,
        width: 800,
        height: 400,
        data: Array(5).fill(0).map(() => Math.random() * 100)
      });
    }
  }, []);
  
  return (
    <div style={{ display: 'flex' }}>
      <div ref={threeRef} style={{ width: '800px', height: '600px', border: '1px solid #ccc' }}></div>
      <div ref={chartRef} style={{ width: '800px', height: '400px', border: '1px solid #ccc' }}></div>
    </div>
  );
}

2. 动态数据更新

// DataService.ts
export function generateMockData(count: number): number[] {
  return Array(count).fill(0).map(() => Math.random() * 100);
}

3. 性能优化策略

// 优化后的EChartsChart.tsx
export function EChartsChart({ container, width, height, data }: EChartsProps) {
  const chart = echarts.init(container);
  
  // 优化配置项
  const option = {
    title: { text: '实时数据' },
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: ['A', 'B', 'C', 'D', 'E'] },
    yAxis: { type: 'value' },
    series: [{
      name: '数据',
      type: 'line',
      data: data,
      showSymbol: false,
      lineStyle: { width: 2 },
      // 关闭动画
      animation: false
    }]
  };
  
  chart.setOption(option);
  
  // 动态更新
  setInterval(() => {
    const newData = data.map((v, i) => v + (Math.random() - 0.5) * 10);
    chart.setOption({ series: [{ data: newData }] });
  }, 2000);
}

优化点说明:

  • 关闭动画提升渲染性能
  • 限制数据更新频率
  • 使用简洁的配置项减少计算开销

六、源码解析

1. three.js渲染流程

function animate() {
  requestAnimationFrame(animate);
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  renderer.render(scene, camera);
}
  • requestAnimationFrame确保与浏览器重绘周期同步
  • 每次动画帧更新对象旋转角度
  • 调用renderer.render进行场景重绘

2. echarts数据更新机制

setInterval(() => {
  const newData = data.map((v, i) => v + (Math.random() - 0.5) * 10);
  chart.setOption({ series: [{ data: newData }] });
}, 2000);
  • 使用setInterval控制更新频率
  • 通过setOption方法更新图表数据
  • 只更新需要变化的数据部分,避免全量重绘

3. 跨库交互优化

const domUtils = {
  getBoundingClientRectInThree: (threeElement, canvas) => {
    // 实现坐标映射逻辑
  }
};
  • 解决不同渲染上下文的坐标系差异
  • 提供统一的坐标转换接口
  • 支持动态尺寸调整时的坐标校正

七、进阶使用

1. 动态模型加载

// 加载GLTF模型
const loader = new THREE.GLTFLoader();
loader.load('model.glb', (gltf) => {
  scene.add(gltf.scene);
  // 设置材质
  gltf.scene.traverse((obj) => {
    if (obj.isMesh) {
      obj.material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
    }
  });
});

2. 动态光照系统

// 创建环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

// 创建点光源
const pointLight = new THREE.PointLight(0xffffff, 1, 100);
pointLight.position.set(10, 10, 10);
scene.add(pointLight);

3. 动态数据绑定

// 将数据绑定到图表
const dataBinding = {
  update: (newData: number[]) => {
    chart.setOption({ series: [{ data: newData }] });
  }
};

八、性能与工程实践

1. 渲染性能优化

优化策略说明
使用requestAnimationFrame同步浏览器重绘周期
避免频繁创建DOM元素使用虚拟DOM或缓存机制
使用WebGL纹理压缩减少内存占用
限制动画帧率使用performance.now()控制帧间隔

2. 代码组织规范

  • 使用TypeScript增强类型安全
  • 采用模块化设计
  • 使用Webpack进行资源打包
  • 实现热更新机制
  • 添加错误边界处理

3. 异常处理方案

try {
  // 重要操作
} catch (error) {
  console.error('发生异常:', error);
  // 记录日志
  // 显示错误提示
}

4. 安全考虑

  • 防止XSS攻击:对用户输入进行过滤
  • 防止CSRF攻击:使用Token验证机制
  • 防止DDoS攻击:限制请求频率
  • 加密敏感数据:使用HTTPS传输

九、常见问题与踩坑

1. 坐标系转换错误

错误示例:

const x = (event.clientX - canvas.offsetLeft) * scaleX;

问题分析:

  • 忽略了容器的滚动偏移量
  • 忘记考虑元素的尺寸变化

解决方案:

const rect = canvas.getBoundingClientRect();
const x = (event.clientX - rect.left) * scaleX;

2. 动画卡顿问题

错误表现:

  • 图标闪烁
  • 动画不流畅

解决方法:

  • 使用performance.now()控制动画帧
  • 使用requestIdleCallback处理非关键任务
  • 启用WebGL的抗锯齿功能

3. 资源加载问题

错误示例:

loader.load('model.glb', (gltf) => { /* ... */ });

问题分析:

  • 未处理加载错误
  • 未设置加载进度提示

改进方案:

loader.load('model.glb', (gltf) => { /* ... */ }, 
  (xhr) => {
    console.log(`加载进度: ${xhr.loaded}/${xhr.total}`);
  },
  (err) => {
    console.error('加载失败:', err);
  }
);

十、最佳实践

1. 技术选型建议

场景推荐方案说明
复杂3D可视化three.js支持WebGL特性
动态数据图表echarts数据驱动更新
跨库交互自定义坐标映射解决坐标系差异
大屏展示响应式布局自适应不同尺寸

2. 性能优化策略

  • 使用WebGL纹理压缩
  • 启用抗锯齿功能
  • 限制动画帧率
  • 避免频繁DOM操作
  • 使用缓存机制

3. 工程实践规范

  • 使用TypeScript进行类型校验
  • 采用模块化开发模式
  • 实现热更新机制
  • 添加错误边界处理
  • 使用性能分析工具

十一、总结

通过结合three.js和echarts,我们成功构建了一个功能完善的站点数据大屏系统。这种技术组合在以下场景中表现尤为出色:

  • 需要展示复杂3D空间关系的业务场景
  • 需要同时展示静态图表和动态数据
  • 需要进行多维度数据对比分析

但也要注意其局限性:

  • 对WebGL特性依赖较高
  • 需要处理跨库交互的复杂性
  • 对前端工程师的综合能力要求较高

在实际项目中,我们通过以下方式确保系统稳定运行:

  1. 使用TypeScript进行严格类型校验
  2. 实现完善的错误处理机制
  3. 采用模块化开发模式
  4. 进行持续的性能优化
  5. 添加详细的文档说明

建议在以下情况下使用这种方案:

  • 需要展示立体化数据的业务场景
  • 需要同时进行多维度数据对比
  • 需要进行动态数据更新的实时展示

建议避免在以下情况下使用:

  • 对性能要求极高的实时系统
  • 需要处理大量离线数据的场景
  • 需要进行复杂交互逻辑的系统

通过合理的技术选型和工程实践,three.js+echarts的组合能够为数据可视化提供强大的支持,帮助我们更直观地理解和分析业务数据。

2024-08-04

疫情统计页面 H5 vue3+TypeScript+Echarts

一、背景与问题

在疫情防控常态化背景下,疫情数据可视化成为公共信息展示的重要手段。传统的静态图表难以满足动态数据更新、多维度分析和交互式探索需求。基于Vue3的响应式体系、TypeScript的类型安全以及ECharts的可视化能力,构建一个高性能、可维护的疫情统计页面,是现代Web开发的典型场景。

当前面临的核心挑战包括:

  1. 实时数据更新与性能平衡
  2. 多数据源整合与类型安全
  3. 交互式图表的可维护性
  4. 移动端适配与性能优化
  5. 数据可视化与业务逻辑的解耦

二、基本原理

1. Vue3响应式系统

Vue3采用Proxy实现的响应式系统,通过refreactive创建响应式数据。在疫情统计场景中,数据更新时会自动触发视图重绘,确保图表状态与数据同步。

// 响应式数据定义
const chartData = ref<{
  confirmed: number;
  deaths: number;
  recovered: number;
  active: number;
}>({
  confirmed: 0,
  deaths: 0,
  recovered: 0,
  active: 0
});

2. TypeScript类型系统

通过类型定义确保数据结构的健壮性,特别是在处理异步数据时防止类型错误:

interface EpidemicData {
  province: string;
  confirmed: number;
  deaths: number;
  recovered: number;
  active: number;
  updateTime: string;
}

3. ECharts图表渲染机制

ECharts通过DOM操作和Canvas渲染实现图表,支持动态更新和配置项管理。在疫情统计场景中,需要处理:

  • 动态数据绑定
  • 多图表类型切换
  • 响应式布局
  • 数据过滤和聚合

三、环境准备

1. 项目初始化

npm create vue@latest
cd pandemic-statistics
npm install typescript @types/echarts

2. 依赖配置

// tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "rootDir": "."
  }
}

四、核心实现

1. 数据获取与处理

// src/services/epidemic.ts
import axios from 'axios';

export async function fetchEpidemicData(): Promise<EpidemicData[]> {
  const response = await axios.get('https://api.example.com/epidemic-data');
  return response.data;
}

2. 图表初始化与配置

<template>
  <div ref="chart" class="chart-container"></div>
</template>

<script lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';

export default {
  setup() {
    const chart = ref<HTMLDivElement | null>(null);
    const chartData = ref<EpidemicData[]>([]);

    const initChart = () => {
      if (!chart.value) return;
      
      const chartInstance = echarts.init(chart.value);
      
      // 配置项
      const option = {
        title: {
          text: '疫情统计'
        },
        tooltip: {
          trigger: 'axis'
        },
        xAxis: {
          type: 'category',
          data: chartData.value.map(d => d.province)
        },
        yAxis: {
          type: 'value'
        },
        series: [
          {
            name: '确诊',
            type: 'bar',
            data: chartData.value.map(d => d.confirmed)
          },
          {
            name: '死亡',
            type: 'bar',
            data: chartData.value.map(d => d.deaths)
          }
        ]
      };
      
      chartInstance.setOption(option);
    };

    onMounted(() => {
      initChart();
    });

    onUnmounted(() => {
      if (chart.value) {
        echarts.getInstanceByDom(chart.value)?.dispose();
      }
    });
  }
};
</script>

3. 响应式布局处理

<style scoped>
.chart-container {
  width: 100%;
  height: 400px;
  aspect-ratio: 16 / 9;
  background: #f0f0f0;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

五、完整案例

1. 案例需求

实现一个支持:

  • 实时更新的疫情数据展示
  • 多维度数据筛选
  • 动态图表类型切换
  • 移动端适配

2. 项目结构

src/
├── components/
│   └── EpidemicChart.vue
├── services/
│   └── epidemic.ts
├── types/
│   └── epidemic.d.ts
└── App.vue

3. 完整代码示例

<!-- src/App.vue -->
<template>
  <div class="app">
    <h1>疫情统计系统</h1>
    <div class="controls">
      <select v-model="chartType">
        <option value="bar">柱状图</option>
        <option value="line">折线图</option>
      </select>
      <button @click="refreshData">刷新数据</button>
    </div>
    <EpidemicChart :chartType="chartType" :data="chartData" />
  </div>
</template>

<script lang="ts">
import { ref, onMounted } from 'vue';
import EpidemicChart from './components/EpidemicChart.vue';
import { fetchEpidemicData } from './services/epidemic';

export default {
  components: { EpidemicChart },
  setup() {
    const chartData = ref<EpidemicData[]>([]);
    const chartType = ref<'bar' | 'line'>('bar');

    const refreshData = async () => {
      try {
        chartData.value = await fetchEpidemicData();
      } catch (error) {
        console.error('数据获取失败:', error);
      }
    };

    onMounted(() => {
      refreshData();
    });

    return { chartData, chartType, refreshData };
  }
};
</script>

<style>
.app {
  padding: 20px;
  font-family: Arial, sans-serif;
}

.controls {
  margin-bottom: 20px;
}
</style>

六、源码解析

1. 响应式系统深度解析

Vue3的响应式系统通过Proxy实现,当数据变化时会自动触发视图更新。在疫情统计场景中,需要特别注意:

  • 使用ref而非reactive来处理嵌套数据
  • 使用watch监听数据变化进行图表更新
  • 避免在模板中直接操作DOM

2. ECharts配置项优化

ECharts的配置项需要根据图表类型动态调整,例如:

const getOption = (type: 'bar' | 'line') => ({
  title: { text: '疫情统计' },
  tooltip: { trigger: 'axis' },
  xAxis: { type: 'category', data: chartData.value.map(d => d.province) },
  yAxis: { type: 'value' },
  series: [
    {
      name: '确诊',
      type: type,
      data: chartData.value.map(d => d.confirmed)
    },
    {
      name: '死亡',
      type: type,
      data: chartData.value.map(d => d.deaths)
    }
  ]
});

3. 图表销毁机制

在组件卸载时需要正确销毁ECharts实例,避免内存泄漏:

onUnmounted(() => {
  if (chartInstance) {
    chartInstance.dispose();
    chartInstance = null;
  }
});

七、进阶使用

1. 动态数据处理

对于大数据量场景,需要实现数据分页和虚拟滚动:

const processData = (rawData: EpidemicData[]) => {
  return rawData
    .map(d => ({
      ...d,
      confirmed: Math.floor(Math.random() * 1000),
      deaths: Math.floor(Math.random() * 100)
    }))
    .sort((a, b) => b.confirmed - a.confirmed);
};

2. 深度定制图表

通过自定义渲染器实现特殊数据展示:

const customRender = (params: any) => {
  return {
    label: { show: true, formatter: '{c}' },
    itemStyle: { color: '#ff4500' }
  };
};

3. 多图表类型联动

实现不同图表类型的数据联动展示:

const updateChart = (type: 'bar' | 'line') => {
  if (!chartInstance) return;
  
  const option = getOption(type);
  chartInstance.setOption(option);
};

八、性能与工程实践

1. 性能优化策略

  1. 数据聚合:对大数据量进行预处理
  2. 懒加载:按需加载图表
  3. 虚拟滚动:使用vue-virtual-scroll-list
  4. Canvas优化:使用will-change属性
  5. 缓存机制:缓存常用图表配置

2. 异常处理机制

try {
  await fetchEpidemicData();
} catch (error) {
  console.error('数据获取失败:', error);
  // 显示错误提示
  alert('无法获取疫情数据,请检查网络连接');
}

3. 安全考虑

  1. 数据来源合法性验证
  2. 防止XSS攻击(对用户输入进行过滤)
  3. 设置CORS策略
  4. 使用HTTPS传输数据
  5. 对敏感数据进行脱敏处理

4. 维护性设计

  1. 使用TypeScript类型定义
  2. 模块化组件结构
  3. 独立配置文件
  4. 使用TypeScript装饰器
  5. 添加单元测试

九、常见问题与踩坑

1. 常见错误

  1. 图表不更新:未使用refreactive创建响应式数据
  2. 内存泄漏:未正确销毁ECharts实例
  3. 性能问题:大数据量时未做优化
  4. 类型错误:未定义类型导致运行时错误
  5. 响应式失效:未正确使用watch监听数据变化

2. 解决方案

  1. 使用ref创建响应式数据
  2. onUnmounted中销毁图表
  3. 实现数据分页和虚拟滚动
  4. 添加类型定义文件
  5. 使用watch监听数据变化

3. 典型问题

问题:图表在移动端显示不全
原因:未处理响应式布局
解决方案:使用aspect-ratiovw/vh单位

十、最佳实践

  1. 数据处理:使用TypeScript定义数据结构,实现数据清洗和格式化
  2. 图表管理:封装图表组件,实现配置项解耦
  3. 性能优化:对大数据量进行分页和虚拟滚动处理
  4. 异常处理:添加全面的错误处理和用户提示
  5. 安全措施:验证数据来源,防止XSS攻击
  6. 可维护性:使用模块化组件,添加单元测试
  7. 性能监控:添加性能监控和资源释放机制

十一、总结

疫情统计页面的开发展示了Vue3+TypeScript+ECharts的综合应用。通过深入理解响应式系统、类型安全和图表渲染机制,可以构建出高性能、可维护的可视化系统。在实际开发中,需要根据具体场景选择合适的方案:对于需要动态更新的场景,推荐使用响应式数据绑定和图表自动更新;对于大数据量场景,需要引入分页和虚拟滚动技术;对于需要高安全性的场景,需要加强数据验证和安全防护。

需要注意的是,这种方案适用于需要动态展示和交互的统计场景,但不适合对性能要求极高或需要复杂数据处理的场景。在开发过程中,需要特别注意响应式系统的使用规范,避免内存泄漏和性能问题。通过合理的架构设计和性能优化,可以构建出稳定可靠的疫情统计系统。