響應式 Echarts Flutter 元件

entronad發表於2020-01-19

配置擴充套件

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,
    }));
  }
});
複製程式碼

相關文章