Flutter與WebView通信方案示例詳解
背景
最近做Flutter應(yīng)用開(kāi)發(fā),需要通過(guò)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滿足筆者需求,接下來(lái)文章的內(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端通過(guò)“runJavascript”執(zhí)行web注冊(cè)的通信函數(shù)“javascriptChannel”傳值,完成通信
WebView(
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) async {
await webViewController.runJavascript('window["javascriptChannel"](${json.encode({...})})');
},
),
問(wèn)題
筆者在安卓平臺(tái),F(xiàn)lutter端使用webViewController.runJavascript('window"javascriptChannel"')傳輸json字符串參數(shù),發(fā)現(xiàn)web端允許報(bào)錯(cuò),如下:

從錯(cuò)誤信息來(lái)看,是執(zhí)行js語(yǔ)法的錯(cuò)誤。這個(gè)問(wèn)題是安卓端處理的問(wèn)題。解決方案是對(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通信方式
可以通過(guò)注冊(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端通過(guò)“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)單,不方便使用。因此基于常見(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: '訪問(wèn)超時(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編程實(shí)現(xiàn)系統(tǒng)重啟與關(guān)機(jī)的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)系統(tǒng)重啟與關(guān)機(jī)的方法,較為詳細(xì)的分析了Android運(yùn)行原理與源碼剖析,講述了Android編程實(shí)現(xiàn)系統(tǒng)重啟與關(guān)機(jī)的相關(guān)技巧與注意事項(xiàng),需要的朋友可以參考下2016-02-02
Android scrollview如何監(jiān)聽(tīng)滑動(dòng)狀態(tài)
這篇文章主要介紹了Android scrollview監(jiān)聽(tīng)滑動(dòng)狀態(tài)的實(shí)例代碼,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-12-12
Android數(shù)據(jù)存儲(chǔ)之SQLite使用
SQLite是D.Richard Hipp用C語(yǔ)言編寫的開(kāi)源嵌入式數(shù)據(jù)庫(kù)引擎。它支持大多數(shù)的SQL92標(biāo)準(zhǔn),并且可以在所有主要的操作系統(tǒng)上運(yùn)行2016-01-01
Android嵌套滾動(dòng)和協(xié)調(diào)滾動(dòng)的多種實(shí)現(xiàn)方法
嵌套的滾動(dòng)主要方式就是這些,這些簡(jiǎn)單的效果我們用協(xié)調(diào)滾動(dòng),如?CoordinatorLayout?也能實(shí)現(xiàn)同樣的效果,這篇文章主要介紹了Android嵌套滾動(dòng)和協(xié)調(diào)滾動(dòng)的多種實(shí)現(xiàn)方法,需要的朋友可以參考下2022-06-06
Android中GIF動(dòng)圖的播放控制和監(jiān)聽(tīng)詳解
android下播放gif圖片功能似乎并不常用,很多時(shí)候還是以展示靜態(tài)圖片為主,可能是由于gif圖體積比較大吧。不過(guò)像表情動(dòng)畫什么的,可能還是需要gif圖的。本文主要給大家介紹了關(guān)于Android中GIF動(dòng)圖的播放控制和監(jiān)聽(tīng)的相關(guān)資料,需要的朋友可以參考下。2017-05-05
Andorid基于ZXing實(shí)現(xiàn)二維碼生成與掃描的示例代碼
ZXing是一個(gè)開(kāi)源的條碼和二維碼掃描庫(kù),它可以用于Android開(kāi)發(fā)中,通過(guò)ZXing庫(kù)可以實(shí)現(xiàn)Android設(shè)備上的條碼和二維碼掃描功能,開(kāi)發(fā)者可以輕松地在Android應(yīng)用中集成條碼和二維碼掃描功能,本文主要給大家介紹了Andorid?ZXing實(shí)現(xiàn)二維碼,感興趣的朋友可以參考下2023-08-08

