配置擴充套件
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)
處理 extraScript
中 Messager.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,
}));
}
});
複製程式碼