React Web 图表 Native 化
本文探讨了 Web 图表 Native 化的可行性与 React SVG 图表 Native 化的具体操作过程,文中涉及的改造项目目前并不成熟,仅供参考。
引子
图表,作为数据可视化表达具有很重要的作用。在 React Native 中,官方提供的绘图方案只有 ART 组件,而 ART 组件类似于 Web 中的阉割版的 SVG ,只是一个基础绘图组件,并不是一个成熟的图表解决方案。而对于复杂图表的技术选型,主要有两个常见方案可以选择:WebView 与 原生端组件 。
- WebView
这种方案是使用 Web 图表库,将图表功能用网页实现 ,然后在 React Native 中利用 WebView 将网页展示出来。这种做法非常简单,稍微封装一下 WebView 组件,就能直接使用 Web 现有的成熟的图表如:Echarts,highCharts 来实现功能。
- 原生端组件
这种方案分两种情况,其中:
一种是绘制逻辑由 React Native 端负责:Android 与 IOS 成熟的图表组件也很多,也可以将这些成熟的图表组件封装给 React Native 使用,由 React Native 端实现绘制逻辑。这种做法性能比 WebView 方式性能好,但是 React Native 生态中有两端都封装好的成熟的方案较少。
另一种绘制逻辑完全由原生端负责:Android 与 IOS 端分别实现图表功能,React Native 端直接展示。这种方式性能最好,但是要写两套代码成本也很高。
在实际项目当中,笔者也是用了 react-native-echarts 这种采用 WebView 的图表解决方案,对笔者而言,这应该是目前最为简便的方案。可是,除了使用 WebView ,还有没有别的方式能利用这些成熟的 Web 图表库呢?或者,如果要将 Web 图表库复刻到 React Native 有没有什么别的可行的方案?
为了解答这些疑问,笔者将在下文详细介绍 Web 图表 Native 化的可行性 与 React SVG 图表 Native 化 的具体操作过程。
Web 图表 Native 化的可行性
在网页上进行绘图有两种方式:Canvas 与 SVG。使用 Canvas 来绘图是利用命令式方式调用 CanvasRenderingContext2D API。使用 SVG 来绘图是在 DOM 节点里使用相关标签如:svg,path,line 等声明式方式来绘制图形。
如果能在 React Native 中使用 Canvas 或者 SVG,那么复刻 Web 图表技术就是可行的。
从 Canvas 角度看可行性
React Native 需要 Canvas 组件并且需要能提供 CanvasRenderingContext2D API。然而,社区并没有原生端支持的 Canvas 组件,虽然有一个 react-native-canvas 但是也是基于 WebView 的。此外,阿里开源的 GCanvas 项目想做跨平台的渲染引擎,但是目前提供的 Canvas API 也还不全,不够成熟。所以,目前,对于利用 Canvas 实现的 Web 图表库,还没有能用的方案。
从 SVG 角度看可行性
SVG 绘图是声明式的,这点与 React 组件的特点是一样的。恰好官方也提供了 ART 组件,社区也有 react-native-svg 这种组件,所以,将用 React + SVG 来实现的 Web 图表组件转换到 React Native 端应该是非常可行。
其实,React 生态里有一个 Web 图表库 victory,并且也有基于 react-native-svg 的 React Native 端的支持 victory-native 。但是 victory 在使用上比较难,文档示例较少且没有复杂示例,而 victory-native 也有不少因为 react-native-svg 的 bug 导致的问题,这也可能是 stars 数不多,不太流行的原因。
有了 victory-naitve 作为成功示例,笔者觉得 React SVG 图表 Native 化成功率更高了。那么,具体要怎么做?
React SVG 图表 Native 化
整个 Native 化的目标非常简单,只要让在 Web 端功能正常的图表能在 React Native 端展示出来就行了。
虽然 react-native-svg 提供的组件 比 ART 组件更接近 SVG, 但是鉴于 react-native-svg 还有很多 issue 还没解决,而且也不是官方组件,所以笔者选用了 ART 。接着需要选择一个合适的 React Web 基于 SVG 的图表库来进行 Native 化。
笔者调研了两个项目:recharts 与 react-charts ,其中:
recharts:star 数较多,比较流行,实现和架构大体同 victory,但没有 native 支持,项目结构稍复杂。
react-charts:star 数很少,但是基本类型图表都有,文档实例也完善,项目稍简单。
笔者认为先从简单的项目入手比较容易,因此就选择 react-charts 这个项目来进行实验。整个实验性项目处理了相关的兼容性问题,主要涉及到 底层组件转换,css 转换 ,Dom API 替换等。下图是改动总览:
以下详细介绍这几个填坑要点:
底层组件转换
哪些组件需要转换?对于一些抽象的高层组件如 LineChart 等折线图组件是无需做转换的,而底层组件如 HTML 标签如:svg ,这些标签组件就需要做转换,因为 React Native 里面没有,平台不兼容性。转换工作主要是需要将 Web 里面的 HTML 标签,尤其是 SVG 标签转换到 React Native 对应的组件。
- SVG 标签的转换
笔者选用 React Native 里的 ART 组件对于 Web 的 SVG 标签,来做对应的组件替换。
react-charts 里面用到的 SVG 标签有: svg,g,circle,line,rect,path,text。而在 ART 里,只有 Surface,Group,Shape,Text 等组件。
svg 可以对应到 Surface,g 对应到 Group, text 对应到 Text ,其他的 circle,line,rect,path 都要用 Shape 通过创建 path 路径来绘制相应的组件:Circle,Line,Rect,Path 。比如,依据 SVG 的 line 标签的使用方法,可以通过 ART 的 Shape 组件来创建与之对应的 Line 组件。
line.js 的源码如下:
// @flow
import React from "react";
import { ART } from "react-native";
const { Path, Shape } = ART;
// ...
export default class Line extends React.Component {
render() {
const { x1 = 0, x2 = 0, y1 = 0, y2 = 0, ...rest } = this.props;
const path = new Path()
.moveTo(x1, y1)
.lineTo(x2, y2)
.close();
// ...
return (
<Shape {...rest} d={path} />
);
}
}
所以,这些 SVG 标签组件大致都能得到对应的 ART 替换组件。
- 其他 HTML 标签的转换
除了 SVG 标签,react-charts 里面还有用到一些 HTML 标签,如 div,table,tbody,tr,td,strong,span 等标签,这些标签可以用 React Native 里简单的 View,Text 标签来作替换。
html.js 源码如下:
// @flow
import React from "react";
import { View as RawView, Text as RawText } from "react-native";
import transCssStyle from "../utils/transCssStyle";
class View extends React.Component {
render() {
const { style, ...rest } = this.props;
return <RawView {...rest} {...transCssStyle(style)} />;
}
}
class Text extends React.Component {
render() {
const { style, ...rest } = this.props;
return <RawText {...rest} {...transCssStyle(style)} />;
}
}
export default {
Div: View,
Table: View,
Tbody: View,
Tr: View,
Td: View,
Strong: Text,
Span: Text
};
css 转换
Web css 样式也是不能直接在 React Native 里面用的,因此也需要转换。关于 css 转换到 React Native 社区已有比较成熟的方案了,就是:css-to-react-native。好在 react-charts 很多都是用内联样式,如果是用 className + 单独的 css 样式文件的话,处理起来会更麻烦。简单已有成熟方案,但是在实际操作中还是出现了一些问题,主要问题是在对 transform 的处理上。
css-to-react-native 对 trasform 的支持性不好,而且对于 ART 组件,不能直接在 style prop 中用 transform,而是有个单独的 transform prop 与单独的 Tranform 接口来做 tranform 。因此对于 ART 组件,需要应用 ART 的 Tranform 接口生成的 transform。
transTransform.js 的源码如下:
// @flow
import { ART } from "react-native";
const { Transform } = ART;
export default function transTransform(transforms?: Array<number>) {
if (!transforms) {
return undefined;
}
const transformStyle = transforms.reduce((acc, transform) => {
if (transform.rotate !== undefined) {
transform.rotate = transform.rotate.replace(
/(\d+)deg/g,
($input, $1) => $1
);
}
return Object.assign(acc, transform);
}, {});
const { translateX, translateY, scaleX, scaleY, rotate } = transformStyle;
const transform = new Transform();
if (translateX !== undefined || translateY !== undefined) {
transform.move(translateX || 0, translateY || 0);
}
if (rotate !== undefined) {
transform.rotate(rotate || 0);
}
if (scaleX !== undefined || scaleY !== undefined) {
transform.scale(scaleX || 1, scaleY || 1);
}
return transform;
}
将上文的 ART Line 组件应用到 style 的转换:
// ...
const resolvedStyle = {
...defaultStyle,
...transCssStyle(style)
};
const transform = transTransform(resolvedStyle.transform);
const path = new Path()
.moveTo(x1, y1)
.lineTo(x2, y2)
.close();
return (
<Shape {...rest} {...resolvedStyle} d={path} transform={transform} />
);
Dom API 替换
对于 react-charts 的 Dom API 的替换主要涉及到有 getBoundingClientRect 与点击事件的处理。
- getBoundingClientRect
在 React Native 中可以用 UIManager 来提供类似的接口,但是在使用上需要进行调整:
由 react-charts 的 el.getBoundingClientRect()
到 React Native 平台上的 getBoundingClientRect(el)
。
getBoundingClientRect.js 源码如下:
import { findNodeHandle, UIManager } from "react-native";
function transformBoundingClientRect(x, y, width, height, pageX, pageY) {
return {
left: pageX,
top: pageY,
right: pageX + width,
bottom: pageY + height,
width,
height,
offsetLeft: x,
offsetTop: y
};
}
export async function getBoundingClientRect(ref) {
return new Promise(resolve => {
UIManager.measure(findNodeHandle(ref), (...args) => {
resolve(transformBoundingClientRect(...args));
});
});
}
- 点击事件
React Native 处理点击事件需要用 PanResponder 来处理,因此涉及点击事件处理的部分也需要有所调整。
react-charts 里的写法:
<Svg onTouchStart={this.this.onTouchStart} {...otherProps}/>
需要改为:
panHandlers = createPanResponder({
start: this.onTouchStart,
move: this.onTouchMove,
end: this.onTouchEnd
});
<Svg {...panHandlers} {...otherProps}/>
createPanResponder.js 源码如下:
// @flow
import { PanResponder } from "react-native";
type ResponderFunction = (evt: Object, gestureState: Object) => void;
export default function createPanResponder({
start,
move,
end,
cancel
}: {
start?: ResponderFunction,
move?: ResponderFunction,
end?: ResponderFunction,
cancel?: ResponderFunction
}) {
return PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
// start
start && start(evt, gestureState);
},
onPanResponderMove: (evt, gestureState) => {
// move
move && move(evt, gestureState);
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
// end
end && end(evt, gestureState);
},
onPanResponderTerminate: (evt, gestureState) => {
// cancel
cancel && cancel(evt, gestureState);
},
onShouldBlockNativeResponder: (evt, gestureState) => {
return true;
}
}).panHandlers;
}
成果
经过这一番折腾,笔者最终达成了基本目标。这个改造项目可以在: react-charts-native 进行查看 ,读者可以查看 commit 记录了解具体的改动过程。以下是成果截图:
总结
对笔者而言,本文的这番探索非常有意义。Web 图表库如果能复刻到 React Native 平台,React Native 图表技术选型将会有更多的选择。此外,在有了相对统一的图表框架的基础上,React Native 图表 Web 化也将非常方便。
不过,目前笔者对 react-charts Native 化的进展只是停留在实验性的阶段,距离完全复刻 Web 上的成熟型功能还有一段距离。该项目也还有几个部分需要完善的地方:
- 坐标的精细化
标记过长时展示会有重叠,这点不够完善。
- 点击交互 (点击事件定位)处理
react-charts 的点击事件处理,不是将事件监听在 svg 元素上而是监听在 svg 里面的 path 等元素,而 ART 组件中除了 Surface 能监听点击事件,其他的 Group,Shape 组件都不行。所以,目前根据手势动态展示图例等功能还不能很好地支持。
- 支持动画
react-charts 的动画依赖 react-show ,react-show 使用了 css3 transition 动画,这部分改造还没有做,所以没有支持。后期可以动态构造 path d 并结合 Animated 组件来实现动画。
以上部分涉及到具体的绘制逻辑,改动较大,需要在对 react-charts 核心代码有进一步的理解,才能很好地进行改造。笔者之后如有精力,将会持续完善。