响应式 Echarts Flutter 组件

配置扩展

Echarts 有很丰富的 扩展 ,包括图表、地图、WebGL 等,在 Web 开发中,它们可以以脚本的形式引入代码,从而扩展 Echarts 的功能。为满足开箱即用, flutter_echarts 内置了最新版的 Echarts 脚本,无需额外引入,同时提供了 extensions 参数,方便使用者引入所需的扩展脚本。 extensions 参数类型为字符串数组,使用者可直接拷贝脚本作为字符串到源码中,避免了文件读写操作和繁琐的 asset 目录。

组件参数

封装功能性的组件,其易用性往往比完备性更重要,要让任意水平的开发者都能开箱即用。Echarts 本身在设计时也是遵循易用性的原则,尽可能的将所有配置工作,交给 option 这一个参数 去完成( 详见论文 )。因此 flutter_echarts 在设计时也尽量简化组件参数:

option

String

字符串形式的 JavaScript Echarts Option。Echarts 图表主要就是通过这个参数配置的。你可以通过 dart:convert 中的 jsonEncode() 来转换 Dart 对象类型的数据:

source: ${jsonEncode(_data1)},
复制代码

由于 JavaScript 没有 ''' 符号,你可以使用它来包裹字符串,以省掉一些引号的转义:

Echarts(
  option: '''
  
    // option string
    
  ''',
),
复制代码

extraScript

String

Echarts.init() 和任意 chart.setOption() 之间执行的 JavaScript 脚本。在组件中我们已经内置了一个 名为 Messager 的 JavascriptChennel,所以你可以使用这个标识符来进行 JavaScript 向 Flutter 的通信:

extraScript: '''
  chart.on('click', (params) => {
  if(params.componentType === 'series') {
  	Messager.postMessage('anything');
  }
  });
''',
复制代码

onMessage

void Function(String)

处理 extraScriptMessager.postMessage() 发送的消息的函数。

extensions

List

从 Echarts 扩展中拷贝的脚本字符串组成的数组,比如各种组件、WebGl、语言等。可以从 这里 下载。将它们作为原始字符串(raw string)引入:

const liquidPlugin = r'''

  // copy from liquid.min.js

''';
复制代码

目前仅有以上 4 个参数,控制更新等由内部机制完成,争取做到用起来就像个简单的表现型 StatelessWidget,只要使用者熟悉 Echarts 本身而不需要额外的学习成本。

当然,如果有建议或要求,请发起 issue

源码解析

html 的加载

对于跨平台的开发方案,由于不同的底层操作系统,文件资源目录一直是个麻烦的事情,在 React Native 中有时甚至必须手动将 html 拷贝到 Android 对应的目录。Flutter 虽然有了完善的 asset 系统,但也需要额外的依赖和配置。直接将本地 html 作为源码中的文本字符串加载是解决这些问题的好办法,webview_flutter 的官方示例也比较推荐用这种办法处理本地 html 。

因此我们将模板 html 、Echarts 脚本、扩展脚本、初始化逻辑等在组件初始化时拼接成字符串,作为 uri 资源供 WebView 加载:

@override
  void initState() {
    super.initState();
    _htmlBase64 = 'data:text/html;base64,' + base64Encode(
      const Utf8Encoder().convert(_getHtml(
        echartsScript,
        widget.extensions ?? [],
        widget.extraScript ?? '',
      ))
    );
    _currentOption = widget.option;
  }
  
  ...
  
  @override
  Widget build(BuildContext context) {
    return WebView(
      initialUrl: _htmlBase64,
      
      ...
    );
  }
复制代码

值得注意的是,作为 uri 资源的字符串,是有一些特殊字符限制的,因此加载时我们将字符串转为 Base64 编码。

这里有一个小技巧由于 JavaScript 中没有 ''' 这个符号,因此在 Dart 中用 ''' 包裹 JavaScript 脚本字符串可以减少很多转义工作。

图表更新

响应式更新基本的实现机制就是在 State.didUpdateWidget 方法中通过 setOption 通知 Echarts 更新图表:

void update(String preOption) async {
    _currentOption = widget.option;
    if (_currentOption != preOption) {
      await _controller?.evaluateJavascript('''
        chart && chart.setOption($_currentOption, true);
      ''');
    }
  }

  @override
  void didUpdateWidget(Echarts oldWidget) {
    super.didUpdateWidget(oldWidget);
    update(oldWidget.option);
  }
复制代码

这其中比较麻烦的是在组件刚刚初始化的时候。

我们知道 WebView 加载 html 和外部数据的获取都是异步的,事先并不知道谁会先完成。WebView 初始化时生命周期的顺序是:

onWebViewCreated --> 加载html --> onPageFinished
复制代码

而 WebViewController 一般是在 onWebViewCreated 中获取的。换言之,当组件拿到 WebViewController 时,并不能确保 WebView 中的 html 已经加载完成,所以 didUpdateWidget 不能仅依据是否已经拿到 WebViewController 决定是否可以更新了。

解决办法是将“外部数据更新时更新图表”解耦为“外部数据更新时更新内部 _currentOption ” 和 ”当需要更新图表时调用 _currentOption “两步,从而确保 html 加载完成前获取的数据也能被记录更新:

String _currentOption;
  
  void init() async {
    await _controller?.evaluateJavascript('''
      chart.setOption($_currentOption, true);
    ''');
  }

  void update(String preOption) async {
    _currentOption = widget.option;
    ...
  }
  
  @override
  Widget build(BuildContext context) {
    return WebView(
      ...
      onPageFinished: (String url) {
        init();
      },
      ...
    );
  }
复制代码

内置信道

webview_flutter 提供了 javascriptChannels 参数,可以设置多路命名信道。不过为了使不熟悉 webview_flutter 的使用者也能快速上手, flutter_echarts 并没有暴露这个参数来管理通信,而是内置建立了一个名为“ Messager ”的信道:

@override
  Widget build(BuildContext context) {
    return WebView(
      ...
      javascriptChannels: <JavascriptChannel>[
        JavascriptChannel(
          name: 'Messager',
          onMessageReceived: (JavascriptMessage javascriptMessage) {
            widget?.onMessage(javascriptMessage.message);
          }
        ),
      ].toSet(),
    );
  }
复制代码

使用者如果有多种事件需要通信,可以像 redux action 那样进行设置:

chart.on('click', (params) => {
  if(params.componentType === 'series') {
    Messager.postMessage(JSON.stringify({
      type: 'select',
      payload: params.dataIndex,
    }));
  }
});
复制代码
我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章