React Native 接入 eCharts

React Native 图表接入指南

概述

本文档详细介绍了在React Native项目中接入ECharts图表的完整步骤,包括依赖安装、组件配置、数据获取、图表渲染等各个环节。

目录

  • [1. 环境准备](#1. 环境准备)
  • [2. 依赖安装](#2. 依赖安装)
  • [3. 图表组件创建](#3. 图表组件创建)
  • [4. 数据获取Hook](#4. 数据获取Hook)
  • [5. 图表配置](#5. 图表配置)
  • [6. 组件集成](#6. 组件集成)
  • [7. 国际化支持](#7. 国际化支持)
  • [8. 最佳实践](#8. 最佳实践)
  • [9. 常见问题](#9. 常见问题)

1. 环境准备

1.1 项目要求

  • React Native 0.76.9+
  • Expo SDK 52+
  • TypeScript 5.3.3+
  • Node.js 18.18.0+

1.2 开发环境

确保已安装以下工具:

  • Node.js 和 npm/yarn
  • React Native CLI 或 Expo CLI
  • iOS Simulator (macOS) 或 Android Emulator

2. 依赖安装

2.1 核心依赖

bash 复制代码
# 安装图表核心库
npm install echarts@^5.6.0

# 安装React Native图表渲染器
npm install @wuba/react-native-echarts@^2.0.3

# 安装SVG支持(图表渲染需要)
npm install react-native-svg@^15.8.0

# 安装日期处理库(用于图表时间轴格式化)
npm install date-fns@^4.1.0

这个过程是这样的:

  1. ECharts 核心库 ( echarts ) :负责所有的数据处理、图表逻辑计算和配置项解析。它会生成一个虚拟的、与平台无关的渲染指令。
  2. SVG 渲染器 ( SVGRenderer ) :这个渲染器来自于 @wuba/react-native-echarts 包,它的作用是接收 ECharts 核心库生成的渲染指令,并将其转换成 SVG 元素。
  3. React Native SVG ( react-native-svg ) :这个库提供了在 React Native 中渲染 SVG 的能力。它会将 SVGRenderer 生成的 SVG 元素真正在原生视图上绘制出来。
    所以,整个流程可以看作是 ECharts (逻辑) -> SVGRenderer (转换为 SVG) -> react-native-svg (在屏幕上绘制) 。这种方式使得强大的 ECharts 库能够跨平台运行在没有原生 Canvas 和 DOM 环境的 React Native 中

2.2 可选依赖

bash 复制代码
# 如果需要健康数据(如心率图表示例)
npm install react-native-health@^1.19.0

# 如果需要更多图表类型
npm install @types/echarts

2.3 依赖说明

依赖包 版本 作用
echarts ^5.6.0 图表核心库,提供丰富的图表类型
@wuba/react-native-echarts ^2.0.3 React Native适配的ECharts渲染器
react-native-svg ^15.8.0 SVG渲染支持,图表显示必需
date-fns ^4.1.0 日期处理工具,用于时间轴格式化

3. 图表组件创建

3.1 基础组件结构

创建文件:app/components/HeartRateChart.tsx

typescript 复制代码
import { useRef, useEffect, useMemo } from "react"
import { StyleProp, View, ViewStyle } from "react-native"
import { echarts } from "../utils/echarts" // 导入集中的 echarts 实例
import SvgChart from "@wuba/react-native-echarts/svgChart"
import { useHealthData } from "../hooks/useHealthData"
import { HealthValue } from "react-native-health"
import { format } from "date-fns"
import { ECharts } from "echarts/core"

// 不再需要在此处注册组件,已在 app/utils/echarts.ts 中集中处理

interface HeartRateChartProps {
  style?: StyleProp<ViewStyle>
}

const CHART_HEIGHT = 300
const CHART_WIDTH = 350

export function HeartRateChart(props: HeartRateChartProps) {
  const { style } = props
  const chartRef = useRef<any>(null)
  const { heartRateSamples, fetchHeartRateSamples, permissionsGranted } = useHealthData({
    requestPermissions: true,
    autoFetch: false,
  })

  // 数据获取
  useEffect(() => {
    if (permissionsGranted) {
      const endDate = new Date()
      const startDate = new Date()
      startDate.setDate(endDate.getDate() - 1)
      fetchHeartRateSamples(startDate, endDate)
    }
  }, [permissionsGranted, fetchHeartRateSamples])

  // 图表配置
  const chartOption = useMemo(() => {
    if (!heartRateSamples || heartRateSamples.length === 0) {
      return {}
    }

    const data = heartRateSamples.map((sample: HealthValue) => [
      new Date(sample.startDate),
      sample.value,
    ])

    return {
      tooltip: {
        trigger: "axis",
        formatter: (params: any) => {
          const param = params[0]
          const date = new Date(param.axisValue)
          const value = param.data[1]
          return `${format(date, "MM-dd HH:mm")}<br/>心率: ${value}`
        },
      },
      xAxis: {
        type: "time",
        axisLabel: {
          formatter: (value: number) => {
            return format(new Date(value), "HH:mm")
          },
        },
      },
      yAxis: {
        type: "value",
        name: "心率 (bpm)",
        min: (value: { min: number }) => Math.floor(value.min / 10) * 10,
      },
      series: [
        {
          data,
          type: "line",
          smooth: true,
          showSymbol: false,
          lineStyle: {
            width: 2,
          },
        },
      ],
      grid: {
        left: "12%",
        right: "5%",
        bottom: "10%",
        top: "10%",
      },
    }
  }, [heartRateSamples])

  // 图表初始化和更新
  useEffect(() => {
    let chartInstance: ECharts | undefined
    if (chartRef.current && Object.keys(chartOption).length > 0) {
      chartInstance = echarts.init(chartRef.current, "light", {
        renderer: "svg",
        width: CHART_WIDTH,
        height: CHART_HEIGHT,
      })
      chartInstance.setOption(chartOption)
    }
    return () => {
      chartInstance?.dispose()
    }
  }, [chartOption])

  return (
    <View style={style}>
      <SvgChart ref={chartRef} />
    </View>
  )
}

3.2 组件导出

app/components/index.ts 中添加导出:

typescript 复制代码
export * from "./HeartRateChart"

4. 数据获取Hook

4.1 创建数据Hook

创建文件:app/hooks/useHealthData.ts

typescript 复制代码
import { useState, useCallback, useEffect } from "react"
import AppleHealthKit, { HealthKitPermissions, HealthValue } from "react-native-health"

interface HealthDataState {
  stepCount: number | null
  heartRateSamples: HealthValue[]
  isLoading: boolean
  error: string | null
  permissionsGranted: boolean
}

interface UseHealthDataOptions {
  requestPermissions?: boolean
  autoFetch?: boolean
}

export const useHealthData = (options: UseHealthDataOptions = {}) => {
  const { requestPermissions = true, autoFetch = true } = options

  const [healthData, setHealthData] = useState<HealthDataState>({
    stepCount: null,
    heartRateSamples: [],
    isLoading: false,
    error: null,
    permissionsGranted: false,
  })

  // 初始化 HealthKit 并请求权限
  const initializeHealthKit = useCallback(async () => {
    if (!requestPermissions) return

    setHealthData((prev) => ({ ...prev, isLoading: true, error: null }))

    const permissions = {
      permissions: {
        read: [
          AppleHealthKit.Constants.Permissions.Steps,
          AppleHealthKit.Constants.Permissions.HeartRate,
          AppleHealthKit.Constants.Permissions.StepCount,
        ],
        write: [],
      },
    } as HealthKitPermissions

    return new Promise<void>((resolve, reject) => {
      AppleHealthKit.initHealthKit(permissions, (error: string) => {
        if (error) {
          const errorMessage = `无法授予 HealthKit 权限: ${error}`
          console.error("HealthKit 权限请求失败:", errorMessage)
          setHealthData((prev) => ({
            ...prev,
            isLoading: false,
            error: errorMessage,
            permissionsGranted: false,
          }))
          reject(new Error(errorMessage))
          return
        }

        setHealthData((prev) => ({
          ...prev,
          isLoading: false,
          permissionsGranted: true,
        }))
        resolve()
      })
    })
  }, [requestPermissions])

  // 获取心率样本
  const fetchHeartRateSamples = useCallback(
    async (startDate?: Date, endDate?: Date) => {
      if (!healthData.permissionsGranted) {
        const errorMsg = "HealthKit 权限未授予,无法获取心率数据"
        console.error(errorMsg)
        throw new Error(errorMsg)
      }

      setHealthData((prev) => ({ ...prev, isLoading: true, error: null }))

      const heartRateOptions = {
        startDate: (
          startDate || new Date(new Date().getTime() - 24 * 60 * 60 * 1000)
        ).toISOString(),
        endDate: (endDate || new Date()).toISOString(),
      }

      return new Promise<HealthValue[]>((resolve, reject) => {
        AppleHealthKit.getHeartRateSamples(
          heartRateOptions,
          (err: string, results: HealthValue[]) => {
            if (err) {
              const errorMessage = `获取心率样本时出错: ${err}`
              console.error(errorMessage)
              setHealthData((prev) => ({
                ...prev,
                isLoading: false,
                error: errorMessage,
              }))
              reject(new Error(errorMessage))
              return
            }

            setHealthData((prev) => ({
              ...prev,
              heartRateSamples: results,
              isLoading: false,
            }))
            resolve(results)
          },
        )
      })
    },
    [healthData.permissionsGranted],
  )

  // 获取所有健康数据
  const fetchAllHealthData = useCallback(async () => {
    if (!healthData.permissionsGranted) {
      return
    }

    try {
      await fetchHeartRateSamples()
    } catch (error) {
      console.error("获取健康数据时出错:", error)
    }
  }, [fetchHeartRateSamples, healthData.permissionsGranted])

  // 清除错误
  const clearError = useCallback(() => {
    setHealthData((prev) => ({ ...prev, error: null }))
  }, [])

  // 初始化
  useEffect(() => {
    if (requestPermissions) {
      initializeHealthKit()
        .then(() => {
          setTimeout(() => {
            if (autoFetch) {
              fetchAllHealthData()
            }
          }, 100)
        })
        .catch((error) => {
          console.error("初始化 HealthKit 失败:", error)
        })
    }
  }, [requestPermissions, autoFetch, initializeHealthKit, fetchAllHealthData])

  return {
    // 状态
    ...healthData,
    // 方法
    initializeHealthKit,
    fetchHeartRateSamples,
    fetchAllHealthData,
    clearError,
  }
}

4.2 Hook导出

app/hooks/index.ts 中添加导出:

typescript 复制代码
export * from "./useHealthData"

5. 图表配置

5.1 基础配置

typescript 复制代码
const baseChartOption = {
  // 提示框配置
  tooltip: {
    trigger: "axis",
    backgroundColor: "rgba(0, 0, 0, 0.8)",
    borderColor: "rgba(255, 255, 255, 0.2)",
    textStyle: {
      color: "#fff",
    },
  },
  
  // 网格配置
  grid: {
    left: "12%",
    right: "5%",
    bottom: "10%",
    top: "10%",
    containLabel: true,
  },
  
  // 动画配置
  animation: true,
  animationDuration: 1000,
  animationEasing: "cubicOut",
}

5.2 时间轴配置

typescript 复制代码
const timeAxisConfig = {
  xAxis: {
    type: "time",
    axisLabel: {
      formatter: (value: number) => format(new Date(value), "HH:mm"),
      color: "#666",
      fontSize: 12,
    },
    axisLine: {
      lineStyle: {
        color: "#ddd",
      },
    },
    splitLine: {
      show: true,
      lineStyle: {
        color: "#f0f0f0",
        type: "dashed",
      },
    },
  },
}

5.3 数值轴配置

typescript 复制代码
const valueAxisConfig = {
  yAxis: {
    type: "value",
    name: "心率 (bpm)",
    nameTextStyle: {
      color: "#666",
      fontSize: 12,
    },
    axisLabel: {
      color: "#666",
      fontSize: 12,
    },
    axisLine: {
      lineStyle: {
        color: "#ddd",
      },
    },
    splitLine: {
      show: true,
      lineStyle: {
        color: "#f0f0f0",
        type: "dashed",
      },
    },
    min: (value: { min: number }) => Math.floor(value.min / 10) * 10,
  },
}

5.4 数据系列配置

typescript 复制代码
const seriesConfig = {
  series: [
    {
      data: chartData,
      type: "line",
      smooth: true,
      showSymbol: false,
      lineStyle: {
        width: 2,
        color: "#ff6b6b",
      },
      areaStyle: {
        color: {
          type: "linear",
          x: 0,
          y: 0,
          x2: 0,
          y2: 1,
          colorStops: [
            { offset: 0, color: "rgba(255, 107, 107, 0.3)" },
            { offset: 1, color: "rgba(255, 107, 107, 0.1)" },
          ],
        },
      },
    },
  ],
}

6. 组件集成

6.1 创建演示组件

创建文件:app/screens/DemoShowroomScreen/demos/DemoHeartRateChart.tsx

typescript 复制代码
import { HeartRateChart } from "../../../components"
import { View, StyleSheet } from "react-native"
import { Text } from "../../../components"

export const DemoHeartRateChart = () => {
  return (
    <View style={styles.container}>
      <Text preset="heading" style={styles.title}>
        24小时心率图表
      </Text>
      <HeartRateChart />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    alignItems: "center",
    padding: 20,
  },
  title: {
    marginBottom: 20,
    textAlign: "center",
  },
})

6.2 添加到演示页面

app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx 中:

typescript 复制代码
import { FC } from "react"
import { Screen } from "../../components"
import { DemoHeartRateChart } from "./demos"

export const DemoShowroomScreen: FC = function DemoShowroomScreen() {
  return (
    <Screen preset="fixed" safeAreaEdges={["top"]}>
      <DemoHeartRateChart />
    </Screen>
  )
}

6.3 导出演示组件

app/screens/DemoShowroomScreen/demos/index.ts 中:

typescript 复制代码
export * from "./DemoHeartRateChart"

7. 国际化支持

7.1 中文翻译

app/i18n/zh.ts 中添加:

typescript 复制代码
const zh = {
  // ... 其他翻译
  demoShowroomScreen: {
    // ... 其他翻译
    demoHeartRateChart: "心率图表",
    demoHeartRateChartDesc: "一个显示心率随时间变化的图表。",
  },
}

7.2 英文翻译

app/i18n/en.ts 中添加:

typescript 复制代码
const en = {
  // ... 其他翻译
  demoShowroomScreen: {
    // ... 其他翻译
    demoHeartRateChart: "Heart Rate Chart",
    demoHeartRateChartDesc: "A chart showing heart rate over time.",
  },
}

8. 最佳实践

8.1 性能优化

  1. 使用useMemo缓存图表配置
typescript 复制代码
const chartOption = useMemo(() => {
  // 图表配置逻辑
}, [data, theme])
  1. 合理管理图表实例
typescript 复制代码
useEffect(() => {
  let chartInstance: ECharts | undefined
  if (chartRef.current && data.length > 0) {
    chartInstance = echarts.init(chartRef.current, "light", {
      renderer: "svg",
      width: CHART_WIDTH,
      height: CHART_HEIGHT,
    })
    chartInstance.setOption(chartOption)
  }
  return () => {
    chartInstance?.dispose()
  }
}, [chartOption])
  1. 避免不必要的重新渲染
typescript 复制代码
const MemoizedChart = React.memo(HeartRateChart)

8.2 错误处理

  1. 数据验证
typescript 复制代码
const chartOption = useMemo(() => {
  if (!data || data.length === 0) {
    return {}
  }
  // 图表配置
}, [data])
  1. 权限检查
typescript 复制代码
useEffect(() => {
  if (permissionsGranted) {
    fetchData()
  }
}, [permissionsGranted])
  1. 加载状态
typescript 复制代码
{isLoading && <LoadingSpinner />}
{error && <ErrorMessage error={error} onRetry={fetchData} />}

8.3 响应式设计

  1. 动态尺寸
typescript 复制代码
const [chartSize, setChartSize] = useState({ width: 350, height: 300 })

useEffect(() => {
  const updateSize = () => {
    const { width } = Dimensions.get('window')
    setChartSize({
      width: width - 40, // 减去padding
      height: 300,
    })
  }
  
  updateSize()
  Dimensions.addEventListener('change', updateSize)
  
  return () => {
    Dimensions.removeEventListener('change', updateSize)
  }
}, [])
  1. 主题适配
typescript 复制代码
const chartOption = useMemo(() => ({
  // ... 其他配置
  backgroundColor: theme.colors.background,
  textStyle: {
    color: theme.colors.text,
  },
}), [theme, data])

8.4 统一ECharts实例管理

为了避免在多个组件中重复初始化ECharts模块导致 [ReferenceError: Property 'document' doesn't exist] 等问题,建议创建一个中心化的echarts.ts文件来统一管理ECharts实例和模块注册。

创建 app/utils/echarts.ts:

typescript 复制代码
import * as echarts from "echarts/core"
import { SVGRenderer } from "@wuba/react-native-echarts/svgChart"
import {
  LineChart,
  BarChart,
  GaugeChart,
  CustomChart,
} from "echarts/charts"
import {
  GridComponent,
  TooltipComponent,
  LegendComponent,
} from "echarts/components"

// 注册所有需要的组件
echarts.use([
  SVGRenderer,
  LineChart,
  BarChart,
  GaugeChart,
  CustomChart,
  GridComponent,
  TooltipComponent,
  LegendComponent,
])

export { echarts }

在组件中使用:

typescript 复制代码
import { echarts } from "../utils/echarts" // 导入集中的 echarts 实例
// ...
// 不再需要 echarts.use(...)

9. 常见问题

9.1 图表不显示

问题:图表组件渲染但图表内容不显示

解决方案

  1. 检查模块注册 :确保所有需要的图表类型(如 LineChart)和组件(如 TooltipComponent)都已在 app/utils/echarts.ts 中导入并使用 echarts.use() 注册。这是最常见的原因,尤其是在添加新图表类型后。
  2. 确认容器尺寸 :确保 <SvgChart> 组件或其父容器具有明确的 widthheight 样式。没有尺寸,图表将无法渲染。
  3. 验证数据格式 :检查传递给 setOption 的配置对象中的 series.data 格式是否符合 ECharts 的要求。
  4. 查看初始化错误 :在 useEffectecharts.init 的部分添加 try...catch 来捕获任何初始化时抛出的错误。

示例:

typescript 复制代码
// 1. 检查 app/utils/echarts.ts
echarts.use([
  SVGRenderer,
  LineChart, // 确保已添加
  GridComponent,
  TooltipComponent,
])

// 2. 在组件中设置明确的容器尺寸
<SvgChart ref={chartRef} style={{ width: 350, height: 300 }} />

9.2 数据更新不生效

问题:数据更新后图表没有重新渲染

解决方案

  1. 检查useMemo的依赖数组
  2. 确保数据引用发生变化
  3. 验证图表实例是否正确更新
typescript 复制代码
const chartOption = useMemo(() => {
  // 图表配置
}, [data, theme]) // 确保包含所有依赖

useEffect(() => {
  if (chartInstance && chartOption) {
    chartInstance.setOption(chartOption, true) // 第二个参数为true表示完全替换
  }
}, [chartOption])

9.3 性能问题

问题:图表渲染导致性能问题

解决方案

  1. 使用数据采样减少数据点
  2. 实现虚拟滚动
  3. 使用Web Worker处理大量数据
typescript 复制代码
// 数据采样
const sampledData = data.length > 1000 
  ? data.filter((_, index) => index % 10 === 0)
  : data

9.4 内存泄漏

问题:组件卸载后图表实例未正确清理

解决方案

  1. 在useEffect的清理函数中销毁图表实例
  2. 使用useRef确保引用一致性
typescript 复制代码
useEffect(() => {
  let chartInstance: ECharts | undefined
  
  if (chartRef.current) {
    chartInstance = echarts.init(chartRef.current)
    chartInstance.setOption(chartOption)
  }
  
  return () => {
    if (chartInstance) {
      chartInstance.dispose()
    }
  }
}, [chartOption])

总结

通过以上步骤,您可以在React Native项目中成功接入ECharts图表。关键要点包括:

  1. 正确的依赖安装和配置
  2. 合理的数据获取和管理
  3. 优化的图表配置和渲染
  4. 完善的错误处理和用户体验
  5. 性能优化和内存管理

这个解决方案提供了一个完整的图表集成框架,可以根据具体需求进行扩展和定制。

相关推荐
全宝8 分钟前
🎨前端实现文字渐变的三种方式
前端·javascript·css
yanlele32 分钟前
前端面试第 75 期 - 2025.07.06 更新前端面试问题总结(12道题)
前端·javascript·面试
妮妮喔妮38 分钟前
【无标题】
开发语言·前端·javascript
fie888943 分钟前
浅谈几种js设计模式
开发语言·javascript·设计模式
巴巴_羊2 小时前
React Ref使用
前端·javascript·react.js
徊忆羽菲2 小时前
Echarts3D柱状图-圆柱体-文字在柱体上垂直显示的实现方法
javascript·ecmascript·echarts
轻语呢喃3 小时前
JavaScript :字符串模板——优雅编程的基石
前端·javascript·后端
杨进军3 小时前
React 协调器 render 阶段
前端·react.js·前端框架
归于尽3 小时前
智能前端小魔术,让图片开口说单词
前端·react.js
杨进军3 小时前
React 中 root.render 与 unmount 函数的流程
前端·react.js·前端框架