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. 性能优化和内存管理

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

相关推荐
Rockson4 分钟前
使用Ruby接入实时行情API教程
javascript·python
前端小巷子1 小时前
Web开发中的文件上传
前端·javascript·面试
上单带刀不带妹2 小时前
手写 Vue 中虚拟 DOM 到真实 DOM 的完整过程
开发语言·前端·javascript·vue.js·前端框架
前端风云志2 小时前
typescript结构化类型应用两例
javascript
杨进军2 小时前
React 创建根节点 createRoot
前端·react.js·前端框架
gnip3 小时前
总结一期正则表达式
javascript·正则表达式
爱分享的程序员3 小时前
前端面试专栏-算法篇:18. 查找算法(二分查找、哈希查找)
前端·javascript·node.js
翻滚吧键盘4 小时前
vue 条件渲染(v-if v-else-if v-else v-show)
前端·javascript·vue.js
你这个年龄怎么睡得着的4 小时前
为什么 JavaScript 中 'str' 不是对象,却能调用方法?
前端·javascript·面试
南屿im4 小时前
JavaScript 手写实现防抖与节流:优化高频事件处理的利器
前端·javascript