背景 Hybrid App 开发中,WebView 内嵌 H5 页面是很常见的架构。H5 需要告诉 Flutter”我要跳转某个页面”,Flutter 需要告诉 H5”用户已登录”或”支付完成”。这就需要一个双向通信通道 。
通信原理 Flutter 和 WebView 的通信分两个方向:
方向
机制
H5 → Flutter
addJavaScriptChannel + postMessage
Flutter → H5
runJavaScript 执行 JS 代码
1 2 3 4 5 6 7 8 9 H5 (JavaScript) Flutter (Dart) ┌────────────────────┐ ┌──────────────────────────┐ │ window │ postMessage │ WebViewController │ │ .MyChannel │ ────────────→ │ .addJavaScriptChannel() │ │ .postMessage(json) │ │ ↓ │ └────────────────────┘ │ onMessageReceived │ │ ↓ │ runJavaScript ←───────│ _handleMessage(json) │ ('window.xxx=123') └──────────────────────────┘
环境搭建 依赖 1 2 3 4 dependencies: webview_flutter: ^4.13.1 url_launcher: ^6.3.2
1 flutter pub add webview_flutter url_launcher
平台注册 webview_flutter 4.x 需要显式注册平台实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import 'package:webview_flutter_android/webview_flutter_android.dart' ;import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart' ;import 'dart:io' ;void main() { if (Platform.isAndroid) { WebView.platform = AndroidWebViewPlatform(); } else if (Platform.isIOS) { WebView.platform = WebKitWebViewPlatform(); } runApp(MyApp()); }
核心实现 一、Flutter 接收 H5 消息(H5 → Flutter) 1. Flutter 端:注册 JavaScript Channel 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import 'dart:convert' ;import 'package:webview_flutter/webview_flutter.dart' ;class WebViewPage extends StatefulWidget { final String url; const WebViewPage({super .key, required this .url}); @override State<WebViewPage> createState() => _WebViewPageState(); } class _WebViewPageState extends State <WebViewPage > { WebViewController? _controller; Future<void > _initWebView() async { _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate(_buildNavigationDelegate()); await _controller!.addJavaScriptChannel( 'MyAppChannel' , onMessageReceived: (JavaScriptMessage msg) { _handleMessage(msg.message); }, ); await _controller!.loadRequest(Uri .parse(widget.url)); } void _handleMessage(String rawMessage) { Map <String , dynamic >? payload; try { payload = jsonDecode(rawMessage) as Map <String , dynamic >; } catch (e) { debugPrint('消息格式错误: $rawMessage ' ); return ; } final String action = payload['action' ]?.toString() ?? '' ; final dynamic data = payload['data' ]; switch (action) { case 'navigateTo' : final String page = data?['page' ] ?? '' ; debugPrint('H5 请求跳转: $page ' ); case 'goBack' : Navigator.of(context).pop(); case 'openExternal' : final String url = data?['url' ] ?? '' ; debugPrint('H5 请求打开外部链接: $url ' ); } } NavigationDelegate _buildNavigationDelegate() { return NavigationDelegate( onPageFinished: (String url) async { await _controller?.runJavaScript( 'window.__APP_FLUTTER_WEBVIEW__=true;' , ); }, ); } }
2. H5 端:发送消息 1 2 3 4 5 6 7 8 9 const message = { action : 'navigateTo' , data : { page : '/user/123' , params : { from : 'h5' }, }, }; MyAppChannel .postMessage (JSON .stringify (message));
注意 :JS Channel 名称 'MyAppChannel' 对应 Flutter 端 addJavaScriptChannel 的第一个参数,必须一致。
二、Flutter 向 H5 发送消息(Flutter → H5) 通过 runJavaScript 在 WebView 中执行任意 JS 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Future<void > notifyPayResult({required bool success}) async { await _controller?.runJavaScript( 'window.dispatchEvent(new CustomEvent("onPayResult", {' ' detail: { success: ${success ? 'true' : 'false' } }' '}));' , ); } Future<void > injectEnvFlag() async { await _controller?.runJavaScript( 'window.__APP_FLUTTER_WEBVIEW__=true;' 'window.__APP_VERSION__="1.0.0";' , ); }
H5 端接收:
1 2 3 4 5 6 7 8 9 10 11 12 window .addEventListener ('onPayResult' , (event ) => { console .log ('支付结果:' , event.detail ); if (event.detail .success ) { } }); if (window .__APP_FLUTTER_WEBVIEW__ ) { console .log ('当前运行在 App WebView 中' ); }
三、消息路由器(解耦版) 当业务复杂时,建议将消息解析和分发从页面中抽离:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 abstract final class H5Bridge { static void handleMessage( BuildContext context, String rawMessage, { WebViewController? controller, }) { Map <String , dynamic >? payload; try { payload = jsonDecode(rawMessage) as Map <String , dynamic >; } catch (e) { debugPrint('[H5Bridge] 消息解析失败: $e ' ); return ; } final String action = payload?['action' ]?.toString() ?? '' ; final dynamic data = payload?['data' ]; switch (action) { case 'navigateTo' : _handleNavigate(context, data); case 'goBack' : _handleGoBack(context); case 'openExternal' : _handleOpenExternal(data); case 'setTitle' : _handleSetTitle(data); } } static void _handleNavigate(BuildContext context, dynamic data) { final String path = data?['path' ] ?? '' ; } static void _handleGoBack(BuildContext context) { if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); } } static void _handleOpenExternal(dynamic data) async { final String url = data?['url' ] ?? '' ; final Uri? uri = Uri .tryParse(url); if (uri == null ) return ; await launchUrl(uri, mode: LaunchMode.externalApplication); } static void _handleSetTitle(dynamic data) { debugPrint('H5 请求设置标题: ${data?['title' ]} ' ); } }
WebView 页面中调用更简洁:
1 2 3 4 5 6 await _controller!.addJavaScriptChannel( 'MyAppChannel' , onMessageReceived: (JavaScriptMessage msg) { H5Bridge.handleMessage(context, msg.message, controller: _controller); }, );
进阶技巧 1. 注入时机:onPageStarted vs onPageFinished
时机
特点
onPageStarted
早,但 DOM 可能未就绪,runJavaScript 可能不生效
onPageFinished
DOM 已就绪,但 JS 执行会更晚
推荐 :两个回调都注入一份,作为兜底。
1 2 3 4 5 6 7 8 NavigationDelegate( onPageStarted: (url) async { await controller?.runJavaScript('window.__FLAG__=true;' ); }, onPageFinished: (url) async { await controller?.runJavaScript('window.__FLAG__=true;' ); }, );
2. 域名白名单 只允许可信域名加载,防止恶意 H5 利用 JS Channel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 bool isDomainWhitelisted(Uri uri) { const whitelist = ['your-domain.com' , 'localhost' ]; final host = uri.host.trim().toLowerCase(); return whitelist.any((d) => host == d || host.endsWith('.$d ' )); } onNavigationRequest: (NavigationRequest request) { final uri = Uri .parse(request.url); if (!isDomainWhitelisted(uri)) { return NavigationDecision.prevent; } return NavigationDecision.navigate; },
3. User-Agent 注入 有些场景 H5 需要在 JS Channel 注册前就知道自己运行在 App 中(比如页面首屏逻辑),可以通过自定义 UA 让服务端或 JS 识别:
1 2 3 4 5 6 final userAgent = await controller.getUserAgent();await controller.setUserAgent('$userAgent MyAppFlutter/1' );const isInApp = navigator.userAgent.includes('MyAppFlutter' );
完整的通信协议设计建议 定义一个标准的消息格式:
1 2 3 4 5 6 interface H5Message { action : string ; data ?: any ; callbackId ?: string ; }
Flutter 端对应解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void _handleMessage(String raw) { final msg = jsonDecode(raw) as Map <String , dynamic >; final action = msg['action' ] as String ; final data = msg['data' ]; final callbackId = msg['callbackId' ] as String? ; if (callbackId != null ) { _controller?.runJavaScript( 'window._nativeCallback("$callbackId ", {ok: true});' ); } }
参考