paint-brush
2023 年 React Native 制图的最佳方式经过@zhiqingchen
4,000 讀數
4,000 讀數

2023 年 React Native 制图的最佳方式

经过 zhiqingchen13m2023/03/30
Read on Terminal Reader

太長; 讀書

写图表相关需求用的最多的图表库是**echarts**。网站上的echarts性能已经相当成熟,官方也为小程序端提供了解决方案,但是在RN中并没有相应的支持。市面上大部分搜索还是基于webview实现的本质,我更喜欢基于RN的方案。
featured image - 2023 年 React Native 制图的最佳方式
zhiqingchen HackerNoon profile picture
0-item
1-item

写图表相关需求用的最多的图表库是echarts


网站上的echarts性能已经相当成熟,官方也为小程序端提供了解决方案,但是在RN中并没有相应的支持。市面上大部分搜索还是基于webview实现的本质,我更喜欢基于RN的方案。毕竟,原生体验会比 Web 更好。


后来发现@wuba/react-native-echarts可以满足我的需求,就试试吧;结果还不错。对实现原理感兴趣的可以点击这里

尖端

  • 如果你已经有APP包,可以忽略前面的打包过程,直接从第4步开始。
  • 试用的完整代码位于 GitHub 上:https: //github.com/iambool/TestApp

使用步骤如下

Step 1. 开发环境搭建

这是搭建本地RN开发环境的过程,网上已经有,不再赘述。你可以在谷歌上搜索它:)

步骤 2. 创建 RN 项目

因为是试用,所以我用expo新初始化了一个叫TestApp的rn项目。

 npx create-expo-app TestApp 

创建测试应用

第 3 步。在移动设备上构建应用程序

使用命令行生成 ios 和 android 应用程序包。


iOS推荐使用模拟器(无需匹配证书)。在 Android 上,我连接到真机。


 yarn android yarn ios


生成包后,手机上已经安装了下图这样的app,说明生成成功。

图片

第四步,安装相关依赖

yarn add @wuba/react-native-echarts echarts yarn add @shopify/react-native-skia yarn add react-native-svg

注意:如果是在已有项目中安装,安装完成后必须重新构建一个包,否则缺少原生依赖会报错。

第 5 步。试用 Skia 模型

@wuba/react-native-echarts 支持两种渲染模式(Skia 和 Svg) ,先用 Skia 做一个简单的图表试试。


分为这几个小步骤:


  • 引入echarts、图表组件等依赖。
  • 注册图表组件。
  • 创建一个图表实例并设置一个选项。
  • 销毁页面时同步销毁图表实例。


具体代码如下:

 import { useRef, useEffect } from 'react'; import { View } from 'react-native'; /** * 1. Import the echarts dependency, this example first tries the line chart */ import * as echarts from 'echarts/core'; import { LineChart } from 'echarts/charts'; import { GridComponent } from 'echarts/components'; import { SVGRenderer, SkiaChart } from '@wuba/react-native-echarts'; /** * 2. Register the required components * SVGRenderer: it is required to register * LineChart: because we want to show the line chart, we have to import LineChart * - If you don't know which components to import, just look at the error report and add whatever the error says is missing * GridComponent: This is the prompt when the error is reported, and then I added the, ha ha */ echarts.use([SVGRenderer, LineChart, GridComponent]); export default () => { const skiaRef = useRef(null); // Ref for saving chart instances useEffect(() => { /** * 3. chart option */ const option = { xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], }, yAxis: { type: 'value', }, series: [ { data: [150, 230, 224, 218, 135, 147, 260], type: 'line', }, ], }; let chart; if (skiaRef.current) { /** * 4. Initialize the chart, specifying the lower width and height */ chart = echarts.init(skiaRef.current, 'light', { renderer: 'svg', width: 400, height: 400, }); chart.setOption(option); } /** * 5. To destroy the chart instance after the page is closed */ return () => chart?.dispose(); }, []); return ( <View className='index'> <SkiaChart ref={skiaRef} /> </View> ); };


写完代码,摇晃手机,重新加载bundle包,报错:

错误不变违规:requireNativeComponent:在 UIManager 中找不到“SkiaDomView”。


我在谷歌上搜索了一下,它说它需要版本降级。应该对应expo版本,安装依赖的时候会有类似的提示,安装提示的版本就可以了。

警告

所以我按照说明进行了版本降级:

 @shopify/[email protected] [email protected]

它在重建应用程序后加载,这很好。 (不过Android把重点给掩盖了,看来屏幕宽度应该是自适应的。)

iOS

安卓

iOS

安卓

第 6 步。尝试 Svg 模型

用Svg方式写一个比较复杂的动态排序条形图,对比Svg和Skia。完整代码在这里

 // ...Some unimportant code is omitted here // Register the required components, such as BarChart and LegendComponent echarts.use([SVGRenderer, BarChart, LegendComponent, GridComponent]); export default () => { const skiaRef = useRef(null); const svgRef = useRef(null); useEffect(() => { // Skia mode const skiaChartData = getData(); // Generate chart bar data let skiaChart; let skiaInter; if (skiaRef.current) { skiaChart = echarts.init(skiaRef.current, 'light', { renderer: 'svg', width: 300, height: 300, }); skiaChart.setOption(getDefaultOption(skiaChartData)); setTimeout(function () { run(skiaChart, skiaChartData); }, 0); skiaInter = setInterval(function () { run(skiaChart, skiaChartData); }, 3000); } // Svg mode const svgChartData = getData(); let svgChart; let svgInter; if (svgRef.current) { svgChart = echarts.init(svgRef.current, 'light', { renderer: 'svg', width: 300, height: 300, }); svgChart.setOption(getDefaultOption(svgChartData)); setTimeout(function () { run(svgChart, svgChartData); }, 0); svgInter = setInterval(function () { run(svgChart, svgChartData); }, 3000); } return () => { skiaChart?.dispose(); svgChart?.dispose(); // The timer has to be cleaned up, otherwise it will still run after exiting the page clearInterval(skiaInter); clearInterval(svgInter); }; }, []); return ( <View> <Text>skia</Text> <SkiaChart ref={skiaRef} /> <Text>svg</Text> <SvgChart ref={svgRef} /> </View> ); };


我无法亲眼看到这两种模式之间的区别。

iOS

安卓

图片

图片

第 7 步。包装图表组件

目前为止效果还是不错的,但是每次都是用一堆东西导入,很困扰我。


让我们简单地总结一下:

 import { useRef, useEffect } from 'react'; import * as echarts from 'echarts/core'; import { BarChart, LineChart, PieChart } from 'echarts/charts'; import { DataZoomComponent, GridComponent, LegendComponent, TitleComponent, ToolboxComponent, TooltipComponent, } from 'echarts/components'; import { SVGRenderer, SvgChart as _SvgChart, SkiaChart as _SkiaChart, } from '@wuba/react-native-echarts'; import { Dimensions } from 'react-native'; // Register the required components echarts.use([ DataZoomComponent, SVGRenderer, BarChart, GridComponent, LegendComponent, ToolboxComponent, TooltipComponent, TitleComponent, PieChart, LineChart, ]); // Default width and height of the chart const CHART_WIDTH = Dimensions.get('screen').width; // Default with the phone screen width const CHART_HEIGHT = 300; const Chart = ({ option, onInit, width = CHART_WIDTH, height = CHART_HEIGHT, ChartComponent, }) => { const chartRef = useRef(null); useEffect(() => { let chart; if (chartRef.current) { chart = echarts.init(chartRef.current, 'light', { renderer: 'svg', width, height, }); option && chart.setOption(option); onInit?.(chart); } return () => chart?.dispose(); }, [option]); return <ChartComponent ref={chartRef} />; }; const SkiaChart = (props) => <Chart {...props} ChartComponent={_SkiaChart} />; const SvgChart = (props) => <Chart {...props} ChartComponent={_SvgChart} />; // Just export these two guys export { SkiaChart, SvgChart };

步骤 8. 使用多个图表

包装好后,让我们写一个包含多个图表的页面,看看它是如何工作的。这是“电子商务数据分析”的页面,包括折线图、条形图和饼图。下面是用svg方式写的主要代码,详细代码点这里


 import { SkiaChart } from '../../components/Chart'; import { ScrollView, Text, View } from 'react-native'; import { StatusBar } from 'expo-status-bar'; import { useCallback, useEffect, useState } from 'react'; import { defaultActual, lineOption, salesStatus, salesVolume, userAnaly, getLineData, } from './contants'; import styles from './styles'; // Turn on chart loading const showChartLoading = (chart) => chart.showLoading('default', { maskColor: '#305d9e', }); // Close chart loading const hideChartLoading = (chart) => chart.hideLoading(); export default () => { const [actual, setActual] = useState(defaultActual); // Recording real-time data useEffect(() => { // Assuming a recurring request for data const interv = setInterval(() => { const newActual = []; for (let it of actual) { newActual.push({ ...it, num: it.num + Math.floor((Math.random() * it.num) / 100), }); } setActual(newActual); }, 200); return () => clearInterval(interv); }, [actual]); const onInitLineChart = useCallback((myChart) => { showChartLoading(myChart); // Simulation of data requests setTimeout(() => { myChart.setOption({ series: getLineData, }); hideChartLoading(myChart); }, 1000); }, []); const onInitUserChart = useCallback((myChart) => { // Simulate data request, similar to onInitLineChart }, []); const onInitSaleChart = useCallback((myChart) => { // Simulate data request, similar to onInitLineChart }, []); const onInitStatusChart = useCallback((myChart) => { // Simulate data request, similar to onInitLineChart }, []); const chartList = [ ['订单走势', lineOption, onInitLineChart], ['用户统计', userAnaly, onInitUserChart], ['各品类销售统计', salesVolume, onInitSaleChart], ['订单状态统计', salesStatus, onInitStatusChart], ]; return ( <ScrollView style={styles.index}> <StatusBar style='light' /> <View> <View style={styles.index_panel_header}> <Text style={styles.index_panel_title}>实时数据</Text> </View> <View style={styles.index_panel_content}> {actual.map(({ title, num, unit }) => ( <View key={title} style={styles.sale_item}> <View style={styles.sale_item_cell}> <Text style={styles.sale_item_text}>{title}</Text> </View> <View style={[styles.sale_item_cell, styles.num]}> <Text style={styles.sale_item_num}>{num}</Text> </View> <View style={[styles.sale_item_cell, styles.unit]}> <Text style={styles.sale_item_text}>{unit}</Text> </View> </View> ))} </View> </View> {chartList.map(([title, data, callback]) => ( <View key={title}> <View style={styles.index_panel_header}> <Text style={styles.index_panel_title}>{title}</Text> </View> <View style={styles.index_panel_content}> <SkiaChart option={data} onInit={callback} /> </View> </View> ))} </ScrollView> ); };


重新加载包并查看结果

iOS

安卓

图片

图片

渲染后iOS交互很流畅,Android交互时有卡顿的感觉(不是我手机太差吧?...)


再次尝试 Skia 模式……

图片

嗯,虽然可以,但是好像不能正常显示中文,安卓中文不显示,iOS是乱码。


看了文档,目前skia在安卓端不支持中文,


我们可以通过将字体设置为'PingFang SC'来在iOS上显示中文,例如:

 const option = { title: { text: '我是中文', textStyle: { fontFamily: 'PingFang SC', // setting the font type }, }, };

但是每一个显示中文的地方都要设置字体……那还是先用svg吧,我懒。

概括

使用了一段时间后,我总结了以下几点:

  • 在支持方面,@wuba/react-native-echarts 支持除GL系列和地图图表暂不支持外的所有类型图表,对于日常业务来说已经非常足够了。在echarts中实现各种图表的代码可以在taro-playground中找到。

  • 交互方面,iOS非常丝滑,Android有时会出现掉帧的情况。

  • 性能:官方报告的性能优于其他解决方案。

    • 我试了一下,不是数据量很大不会有什么问题,但是当数据量过大时(比如绘制大量数据热图),渲染速度明显下降很多,这是一个点等待官方优化。
    • 另外,如果页面上的图表较多,在真机调试时加载速度会很慢,所以建议先使用模拟器。
  • 中文支持,Svg模式支持中文,但是Skia模式还没有。


以上仅代表个人观点,欢迎在下方提出问题和意见!