Flutter与WebView通信方案示例详解
SugarTurboS 人气:0背景
最近做Flutter应用开发,需要通过WebView嵌入前端web页面,而且Flutter与前端web有数据通信的需求。因此,笔者关于Flutter与WebView通信方式做了调研,并封装了一套支持请求响应和发布订阅的两套通信模式的JSBridge SDK。
WebView组件选择
Flutter三方库,使用最多的WebView组件,如下两款:
- webview_flutter:官方提供的webview组件
- flutter_inappwebview:三方提供的webview组件
两款组件都支持WebView与Flutter通信,flutter_inappwebview 比 webview_flutter提供的原生接口更丰富一些。
由于webview_flutter满足笔者需求,接下来文章的内容,都是以webview_flutter为准。
webview_flutter通信方式调研
Flutter -> WebView通信方式
可以使用WebViewController对象的执行js脚本的函数runJavascript(String javaScriptString)。具体代码实现如下:
// web注册native端调用的通信函数“javascriptChannel” window['javascriptChannel'] = function(jsonStr) { ... }
// native端通过“runJavascript”执行web注册的通信函数“javascriptChannel”传值,完成通信 WebView( javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController webViewController) async { await webViewController.runJavascript('window["javascriptChannel"](${json.encode({...})})'); }, ),
问题
笔者在安卓平台,Flutter端使用webViewController.runJavascript('window"javascriptChannel"')传输json字符串参数,发现web端允许报错,如下:
从错误信息来看,是执行js语法的错误。这个问题是安卓端处理的问题。解决方案是对传输的字符串做编码处理,例如,base64编码,如下:
String str = Uri.encodeComponent(json.encode({...})); List<int> content = utf8.encode(str); String data = base64Encode(content); await webViewController.runJavascript('window["javascriptChannel"](${data})');
// web端收到数据对数据做解码处理 const message = JSON.parse(decodeURIComponent(atob(jsonStr)));
注:window.atob不支持中文,因此需要encodeComponent/decodeURIComponent转义中文字符,避免中文乱码。
WebView -> Flutter通信方式
可以通过注册WebView JavascriptChannel通信对象的方式。具体代码实现如下:
// native端注册web端调用的通信对象“nativeChannel” WebView( javascriptMode: JavascriptMode.unrestricted, javascriptChannels: <JavascriptChannel>[ JavascriptChannel( name: 'nativeChannel', // 注册web调用的对象 onMessageReceived: (JavascriptMessage msg) async { jsonDecode(msg.message) }, ), ].toSet(), )
// web端通过“nativeChannel”通信对象,调用函数“postMessage”传值 window['nativeChannel'].postMessage(JSON.stringify(...));
注:通信传值都是字符串的形式,native和web端需要自行解析字符串,因此建议采用json字符串的固定格式传值
JSBridge通信模块封装
对于相对复杂需要频繁进行Flutter与web通信的场景,WebView提供的Flutter与web的通信接口简单,不方便使用。因此基于常见的两种通信方式:发布订阅和请求响应,封装一套标准的JSBridge通信的SDK。
发布订阅
发布订阅是一种标准的消息通信模式,主要用于两个不相关联解耦的模块进行数据通信。“订阅方”只需要向“发布订阅模块”订阅消息,当“发布订阅模块”接收到“发布方”消息时,则把消息转发到所有“订阅方”,如下图所示:
请求响应
“请求方”发起一个请求消息,“响应方”接收到请求消息,做一些逻辑处理,回应一个响应消息到“请求方”。例如:http协议就属于请求响应模式,可以把web端作为客户端,flutter端作为服务端。如下图所示:
代码实现——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'; } } // 注册响应句柄 class RegisterResponseHandle { final ResponseCallback registerResponseCallback; // 注册的回调 final Function(BridgeMessage message) callback; // 中间触发的回调 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 = {}; /// 设置WebViewController 必须 void setWebViewController(WebViewController controller) { _controller = controller; } /// webView设置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) { // 处理请求消息 _registerResponseHandleMap[message.eventName]?.forEach((element) => element.callback(message)); } }, ), ].toSet(); } /// 发送消息 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'); } } /// 注册响应 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表示响应成功 ), ), ); }, registerResponseCallback: callback, ), ); } /// 注销响应 void logoutResponse(String eventName, ResponseCallback callback) { List<RegisterResponseHandle>? registerResponseHandle = _registerResponseHandleMap[eventName]; registerResponseHandle?.forEach( (item) { if (item.callback == callback) { registerResponseHandle.remove(item); } }, ); } /// 发布消息 Future publisher(String eventName, dynamic params) async { await postMessage(BridgeMessage( id: '', type: BridgeMessage.MESSAGE_TYPE_PUBLISHER, eventName: eventName, params: params, )); } /// 订阅消息,@return 取消订阅回调 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、创建jsBridge对象 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、设置 javascriptChannels 通道 javascriptChannels: jsBridge.getJavascriptChannel(), onWebViewCreated: (WebViewController webViewController) async { /// 3、设置jsBridge webViewController通信对象 jsBridge.setWebViewController(webViewController); /// 4、注册响应事件:"/test" jsBridge.registerResponse('/test', (value, next) { // TODO 处理响应 next('flutter响应消息'); }); Function? unsubscribe; /// 5、订阅消息事件:"test" unsubscribe = jsBridge.subscribe('test', (value) { /// TODO 处理订阅 unsubscribe?.call(); // 取消订阅 /// 6、发布消息事件:"test" jsBridge.publisher('test', '这是一条订阅消息'); }); webViewController.loadFlutterAsset('assets/webview_static/index.html'); }, ); } }
代码实现——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)); } } }; } // 请求响应 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表示响应超时 this.requestCallbackMap[id] && this.requestCallbackMap[id](JSON.stringify({ code: -1, data: '访问超时' })); }, timeout); this.native && this.native.postMessage(JSON.stringify({ type: 'request', id: id, eventName: eventName, params: params })); }); }; // 发布 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、创建JSBridge对象 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、发布订阅消息:“test”。native端订阅test消息,请参考上面原生端代码 jsBridge.publisher('test', { data: '这是H5端发布消息' }); }} > 发布消息 </Button> </div> <div styleName="delete-button"> <Button type="primary" onClick={async () => { // 5、发送请求消息:“/test”,异步接收响应数据。native端注册响应消息,请参考上面原生端代码 const res = await jsBridge.request('/test', { data: '这是H5端请求消息' }); console.info('web收到一条响应消息:eventName=/test, res=', res.data); }} > 请求消息 </Button> </div> </div> ); } export default Test;
结尾
加载全部内容