Flutter與WebView通信方案示例詳解
背景
最近做Flutter應(yīng)用開發(fā),需要通過WebView嵌入前端web頁(yè)面,而且Flutter與前端web有數(shù)據(jù)通信的需求。因此,筆者關(guān)于Flutter與WebView通信方式做了調(diào)研,并封裝了一套支持請(qǐng)求響應(yīng)和發(fā)布訂閱的兩套通信模式的JSBridge SDK。
WebView組件選擇
Flutter三方庫(kù),使用最多的WebView組件,如下兩款:
- webview_flutter:官方提供的webview組件
- flutter_inappwebview:三方提供的webview組件
兩款組件都支持WebView與Flutter通信,flutter_inappwebview 比 webview_flutter提供的原生接口更豐富一些。
由于webview_flutter滿足筆者需求,接下來文章的內(nèi)容,都是以webview_flutter為準(zhǔn)。
webview_flutter通信方式調(diào)研
Flutter -> WebView通信方式
可以使用WebViewController對(duì)象的執(zhí)行js腳本的函數(shù)runJavascript(String javaScriptString)。具體代碼實(shí)現(xiàn)如下:
// web注冊(cè)native端調(diào)用的通信函數(shù)“javascriptChannel” window['javascriptChannel'] = function(jsonStr) { ... }
// native端通過“runJavascript”執(zhí)行web注冊(cè)的通信函數(shù)“javascriptChannel”傳值,完成通信 WebView( javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController webViewController) async { await webViewController.runJavascript('window["javascriptChannel"](${json.encode({...})})'); }, ),
問題
筆者在安卓平臺(tái),F(xiàn)lutter端使用webViewController.runJavascript('window"javascriptChannel"')傳輸json字符串參數(shù),發(fā)現(xiàn)web端允許報(bào)錯(cuò),如下:
從錯(cuò)誤信息來看,是執(zhí)行js語(yǔ)法的錯(cuò)誤。這個(gè)問題是安卓端處理的問題。解決方案是對(duì)傳輸?shù)淖址鼍幋a處理,例如,base64編碼,如下:
String str = Uri.encodeComponent(json.encode({...})); List<int> content = utf8.encode(str); String data = base64Encode(content); await webViewController.runJavascript('window["javascriptChannel"](${data})');
// web端收到數(shù)據(jù)對(duì)數(shù)據(jù)做解碼處理 const message = JSON.parse(decodeURIComponent(atob(jsonStr)));
注:window.atob不支持中文,因此需要encodeComponent/decodeURIComponent轉(zhuǎn)義中文字符,避免中文亂碼。
WebView -> Flutter通信方式
可以通過注冊(cè)WebView JavascriptChannel通信對(duì)象的方式。具體代碼實(shí)現(xiàn)如下:
// native端注冊(cè)web端調(diào)用的通信對(duì)象“nativeChannel” WebView( javascriptMode: JavascriptMode.unrestricted, javascriptChannels: <JavascriptChannel>[ JavascriptChannel( name: 'nativeChannel', // 注冊(cè)web調(diào)用的對(duì)象 onMessageReceived: (JavascriptMessage msg) async { jsonDecode(msg.message) }, ), ].toSet(), )
// web端通過“nativeChannel”通信對(duì)象,調(diào)用函數(shù)“postMessage”傳值 window['nativeChannel'].postMessage(JSON.stringify(...));
注:通信傳值都是字符串的形式,native和web端需要自行解析字符串,因此建議采用json字符串的固定格式傳值
JSBridge通信模塊封裝
對(duì)于相對(duì)復(fù)雜需要頻繁進(jìn)行Flutter與web通信的場(chǎng)景,WebView提供的Flutter與web的通信接口簡(jiǎn)單,不方便使用。因此基于常見的兩種通信方式:發(fā)布訂閱和請(qǐng)求響應(yīng),封裝一套標(biāo)準(zhǔn)的JSBridge通信的SDK。
發(fā)布訂閱
發(fā)布訂閱是一種標(biāo)準(zhǔn)的消息通信模式,主要用于兩個(gè)不相關(guān)聯(lián)解耦的模塊進(jìn)行數(shù)據(jù)通信。“訂閱方”只需要向“發(fā)布訂閱模塊”訂閱消息,當(dāng)“發(fā)布訂閱模塊”接收到“發(fā)布方”消息時(shí),則把消息轉(zhuǎn)發(fā)到所有“訂閱方”,如下圖所示:
請(qǐng)求響應(yīng)
“請(qǐng)求方”發(fā)起一個(gè)請(qǐng)求消息,“響應(yīng)方”接收到請(qǐng)求消息,做一些邏輯處理,回應(yīng)一個(gè)響應(yīng)消息到“請(qǐng)求方”。例如:http協(xié)議就屬于請(qǐng)求響應(yīng)模式,可以把web端作為客戶端,flutter端作為服務(wù)端。如下圖所示:
代碼實(shí)現(xiàn)——Flutter端
1.JSBridge
import 'dart:convert'; import 'package:webview_flutter/webview_flutter.dart'; typedef SubscribeCallback = void Function(dynamic value); typedef ResponseCallback = void Function(dynamic value, Function(dynamic value) next); // 傳輸消息體 class BridgeMessage { static const String MESSAGE_TYPE_REQUEST = 'request'; static const String MESSAGE_TYPE_PUBLISHER = 'publisher'; String id = ''; String type = ''; String eventName = ''; dynamic params; BridgeMessage({ required this.id, required this.type, required this.eventName, required this.params, }); BridgeMessage.fromJson(json) { id = json['id'] ?? ''; type = json['type']; eventName = json['eventName']; params = json['params']; } dynamic toJson() { return { 'id': id, 'type': type, 'eventName': eventName, 'params': params, }; } String toString() { return 'id=$id type=$type eventName=$eventName params=$params'; } } // 注冊(cè)響應(yīng)句柄 class RegisterResponseHandle { final ResponseCallback registerResponseCallback; // 注冊(cè)的回調(diào) final Function(BridgeMessage message) callback; // 中間觸發(fā)的回調(diào) RegisterResponseHandle({ required this.registerResponseCallback, required this.callback, }); } class JSBridge { static const String NATIVE_CHANNEL = 'nativeChannel'; // 原生通信通道名稱 static const String JAVASCRIPT_CHANNEL = 'javascriptChannel'; // js通信通道名稱 WebViewController? _controller; Map<String, List<SubscribeCallback>> _subscribeCallbackMap = {}; Map<String, List<RegisterResponseHandle>> _registerResponseHandleMap = {}; /// 設(shè)置WebViewController 必須 void setWebViewController(WebViewController controller) { _controller = controller; } /// webView設(shè)置JavascriptChannel Set<JavascriptChannel> getJavascriptChannel() { return <JavascriptChannel>[ JavascriptChannel( name: NATIVE_CHANNEL, onMessageReceived: (JavascriptMessage msg) async { BridgeMessage message = BridgeMessage.fromJson(jsonDecode(msg.message)); if (message.type == BridgeMessage.MESSAGE_TYPE_PUBLISHER) { // 處理訂閱消息 _subscribeCallbackMap[message.eventName]?.forEach((callback) => callback(message.params)); } else if (message.type == BridgeMessage.MESSAGE_TYPE_REQUEST) { // 處理請(qǐng)求消息 _registerResponseHandleMap[message.eventName]?.forEach((element) => element.callback(message)); } }, ), ].toSet(); } /// 發(fā)送消息 Future postMessage(BridgeMessage bridgeMessage) async { String str = Uri.encodeComponent(json.encode(bridgeMessage.toJson())); List<int> content = utf8.encode(str); String data = base64Encode(content); try { await _controller?.runJavascript("""window['$JAVASCRIPT_CHANNEL']('$data')"""); } catch (e) { print('runJavascript error: $e'); } } /// 注冊(cè)響應(yīng) void registerResponse(String eventName, ResponseCallback callback) { if (_registerResponseHandleMap[eventName] == null) { _registerResponseHandleMap[eventName] = []; } _registerResponseHandleMap[eventName]?.add( RegisterResponseHandle( callback: (BridgeMessage message) { callback( message.params, (dynamic params) => postMessage( BridgeMessage( id: message.id, type: message.type, eventName: message.eventName, params: {'code': 0, 'data': params}, // code == 0表示響應(yīng)成功 ), ), ); }, registerResponseCallback: callback, ), ); } /// 注銷響應(yīng) void logoutResponse(String eventName, ResponseCallback callback) { List<RegisterResponseHandle>? registerResponseHandle = _registerResponseHandleMap[eventName]; registerResponseHandle?.forEach( (item) { if (item.callback == callback) { registerResponseHandle.remove(item); } }, ); } /// 發(fā)布消息 Future publisher(String eventName, dynamic params) async { await postMessage(BridgeMessage( id: '', type: BridgeMessage.MESSAGE_TYPE_PUBLISHER, eventName: eventName, params: params, )); } /// 訂閱消息,@return 取消訂閱回調(diào) Function subscribe(String eventName, SubscribeCallback callback) { if (_subscribeCallbackMap[eventName] == null) { _subscribeCallbackMap[eventName] = []; } _subscribeCallbackMap[eventName]?.add(callback); return () => unsubscribe(eventName, callback); } /// 取消訂閱 void unsubscribe(String eventName, SubscribeCallback callback) { _subscribeCallbackMap[eventName]?.remove(callback); } }
2.使用方式
class WebViewWidget extends StatefulWidget { @override _WebViewWidget createState() => _WebViewWidget(); } class _WebViewWidget extends State<WebViewWidget> { /// 1、創(chuàng)建jsBridge對(duì)象 JSBridge jsBridge = JSBridge(); @override void initState() { super.initState(); if (Platform.isAndroid) WebView.platform = AndroidWebView(); } @override Widget build(BuildContext context) { return WebView( debuggingEnabled: true, javascriptMode: JavascriptMode.unrestricted, /// 2、設(shè)置 javascriptChannels 通道 javascriptChannels: jsBridge.getJavascriptChannel(), onWebViewCreated: (WebViewController webViewController) async { /// 3、設(shè)置jsBridge webViewController通信對(duì)象 jsBridge.setWebViewController(webViewController); /// 4、注冊(cè)響應(yīng)事件:"/test" jsBridge.registerResponse('/test', (value, next) { // TODO 處理響應(yīng) next('flutter響應(yīng)消息'); }); Function? unsubscribe; /// 5、訂閱消息事件:"test" unsubscribe = jsBridge.subscribe('test', (value) { /// TODO 處理訂閱 unsubscribe?.call(); // 取消訂閱 /// 6、發(fā)布消息事件:"test" jsBridge.publisher('test', '這是一條訂閱消息'); }); webViewController.loadFlutterAsset('assets/webview_static/index.html'); }, ); } }
代碼實(shí)現(xiàn)——web端
1.JSBridge
import { v1 as uuid } from 'uuid'; export type SubscribeCallback = (params?: any) => void; const MESSAGE_TYPE_REQUEST = 'request'; const MESSAGE_TYPE_PUBLISHER = 'publisher'; const NATIVE_CHANNEL = 'nativeChannel'; // 原生通信通道名稱 const JAVASCRIPT_CHANNEL = 'javascriptChannel'; // js通信通道名稱 const REQUEST_TIME_OUT = 20000; interface BridgeMessage { id: string; type: string; eventName: string; params: any; } class JSBridge { private native: any = window[NATIVE_CHANNEL]; private subscribeCallbackMap = {}; private requestCallbackMap = {}; constructor() { window[JAVASCRIPT_CHANNEL] = (jsonStr) => { const message = JSON.parse(decodeURIComponent(atob(jsonStr))) as BridgeMessage; const id = message.id; const type = message.type; const eventName = message.eventName; const params = message.params; if (type === MESSAGE_TYPE_REQUEST) { this.requestCallbackMap[id] && this.requestCallbackMap[id](params); } else if (type === MESSAGE_TYPE_PUBLISHER) { const callbacks = this.subscribeCallbackMap[eventName]; if (callbacks) { callbacks.forEach((callback) => callback(params)); } } }; } // 請(qǐng)求響應(yīng) request = (eventName: string, params: any, timeout = REQUEST_TIME_OUT): Promise<any> => { return new Promise((resolve: any) => { const id: string = uuid(); let timer; this.requestCallbackMap[id] = (params) => { clearTimeout(timer); delete this.requestCallbackMap[id]; resolve(params); }; timer = setTimeout(() => { // code == -1表示響應(yīng)超時(shí) this.requestCallbackMap[id] && this.requestCallbackMap[id](JSON.stringify({ code: -1, data: '訪問超時(shí)' })); }, timeout); this.native && this.native.postMessage(JSON.stringify({ type: 'request', id: id, eventName: eventName, params: params })); }); }; // 發(fā)布 publisher = (eventName: string, params: any): void => { this.native && this.native.postMessage(JSON.stringify({ type: 'publisher', eventName: eventName, params: params })); }; // 訂閱 subscribe = (eventName: string, callback: SubscribeCallback): SubscribeCallback => { if (!this.subscribeCallbackMap[eventName]) { this.subscribeCallbackMap[eventName] = []; } this.subscribeCallbackMap[eventName].push(callback); return () => this.unsubscribe(eventName, callback); }; // 取消訂閱 unsubscribe = (eventName: string, callback: SubscribeCallback): void => { const callbacks = this.subscribeCallbackMap[eventName]; if (callbacks) { callbacks.forEach((item, index) => { if (item === callback) { callbacks.splice(index, 1); } }); } }; } export default JSBridge;
2.使用方式
import React, { useEffect } from 'react'; import { Button } from 'antd'; import JSBridge from '@common/JSBridge'; import './index.less'; // 1、創(chuàng)建JSBridge對(duì)象 const jsBridge = new JSBridge(); function Test() { useEffect(() => { // 2、訂閱消息:“test” const unsubscribe = jsBridge.subscribe('test', (params) => { console.info('web收到一條訂閱消息:eventName=test, params=', params); }); return () => { // 3、取消訂閱消息:“test” unsubscribe(); }; }); return ( <div styleName="container"> <div styleName="add-button"> <Button type="primary" onClick={() => { // 4、發(fā)布訂閱消息:“test”。native端訂閱test消息,請(qǐng)參考上面原生端代碼 jsBridge.publisher('test', { data: '這是H5端發(fā)布消息' }); }} > 發(fā)布消息 </Button> </div> <div styleName="delete-button"> <Button type="primary" onClick={async () => { // 5、發(fā)送請(qǐng)求消息:“/test”,異步接收響應(yīng)數(shù)據(jù)。native端注冊(cè)響應(yīng)消息,請(qǐng)參考上面原生端代碼 const res = await jsBridge.request('/test', { data: '這是H5端請(qǐng)求消息' }); console.info('web收到一條響應(yīng)消息:eventName=/test, res=', res.data); }} > 請(qǐng)求消息 </Button> </div> </div> ); } export default Test;
結(jié)尾
以上就是Flutter與WebView通信方案示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Flutter WebView通信方案的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
android studio與手機(jī)連接調(diào)試步驟詳解
這篇文章主要為大家詳細(xì)介紹了android studio與手機(jī)連接調(diào)試步驟,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07Android ViewPager實(shí)現(xiàn)智能無限循環(huán)滾動(dòng)回繞效果
這篇文章主要為大家詳細(xì)介紹了Android ViewPager實(shí)現(xiàn)智能無限循環(huán)滾動(dòng)回繞效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07Android Compose實(shí)現(xiàn)底部按鈕以及首頁(yè)內(nèi)容詳細(xì)過程
這篇文章主要介紹了如何利用compose框架制作app底部按鈕以及首頁(yè)內(nèi)容的詳細(xì)代碼,具有一定價(jià)值,感興趣的可以了解一下2021-11-11Android編程實(shí)現(xiàn)動(dòng)態(tài)更新ListView的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)動(dòng)態(tài)更新ListView的方法,結(jié)合實(shí)例形式詳細(xì)分析了ListView的布局及動(dòng)態(tài)更新實(shí)現(xiàn)方法,需要的朋友可以參考下2016-02-02