一、交互总览:H5 ↔ Flutter 有哪几条"通道"?
通道 | 方向 | 场景 | 核心 API |
---|---|---|---|
① JSChannel | H5 → Flutter | H5 主动把事件或数据回传给原生 | addJavaScriptChannel() → channel.postMessage() medium.com |
② runJavaScript | Flutter → H5 | 原生调用 JS 方法、读 DOM、注入脚本 | runJavaScript() / runJavaScriptReturningResult() pub.dev |
③ Navigation 拦截 | 双向 | 用 URL scheme 作为信号(支付宝、分享、关闭页等) | NavigationDelegate.onNavigationRequest |
④ postMessage (未来) | H5 ↔ Flutter | WebMessageChannel/Listener;目前官方还未完备,依赖社区或 InAppWebView inappwebview.dev |
在 2025 年中,官方
webview_flutter
的稳妥做法仍是 ①+②+③ 。④ 正在讨论阶段,若需要 window.postMessage 全特性,可临时转用flutter_inappwebview
。
二、最常用的"JSChannel"双向通信
1. Dart 侧注册通道
dart
late final WebViewController ctrl;
@override
void initState() {
super.initState();
ctrl = WebViewController()
..addJavaScriptChannel(
'NativeBridge', // JS 端调用的名字
onMessageReceived: (msg) {
final data = jsonDecode(msg.message); // {"type":"pay","payload":{...}}
_handleFromWeb(data);
},
)
..loadRequest(Uri.parse('https://your.h5.com'));
}
2. H5 侧发送数据
html
<script>
function notifyPaySuccess(orderId) {
NativeBridge.postMessage(JSON.stringify({
type: 'paySuccess',
payload: { orderId }
}));
}
</script>
3. Flutter 调 JS 并拿结果
dart
final price = await ctrl
.runJavaScriptReturningResult('getCartTotal()'); // JS 返回值自动转 Dart
print('总价 = $price 元');
三、导航拦截:最兼容但"笨一点"的方案
dart
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (req) {
if (req.url.startsWith('myapp://close')) {
Navigator.pop(context);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
- 优点 :所有前端框架(Vue/React/Pure JS)都能
window.location.href = "myapp://close"
; - 缺点:只能传字符串,复杂参数需 Base64/JSON 编码。
四、实战技巧 & 深坑提示
主题 | 要点 |
---|---|
参数打包 | 多字段请 JSON.stringify(obj) ,Dart 侧统一 jsonDecode ,别用逗号拼接。 |
异步节流 | JS 端高频事件(scroll、drag)不要直接 postMessage,前端 throttle 每 100 ms 发一次。 |
安全 | 只暴露白名单通道;runJavaScript 内容拼接用户输入前务必 jsonEncode 。 |
iOS Cookie | iOS 14+ 第三方 Cookie 默认阻断,建议走同域访问或改 Token 方案。 |
Android 低端机卡顿 | 在瀑布流页面用 Virtual Display,会出现掉帧;可在启动页 WebViewPlatform.instance = SurfaceAndroidWebView(); 切 Hybrid Composition。 |
文件上传 | 需要 <input type="file"> ?别忘了在 Android 13+ 申请 READ_MEDIA_* 权限。 |
webview_flutter
webview_flutter
是官方维护、稳定性最佳的 Flutter WebView 插件。iOS 内部封装 WKWebView
,Android 则直接调用系统 WebView
。优点 :跨端一致 API、可沉浸式集成;局限:依赖原生内核版本,体积不可避免增大 2--5 MB。
二、快速上手(4 步)
-
添加依赖
bashflutter pub add webview_flutter
-
导入包
dartimport 'package:webview_flutter/webview_flutter.dart';
-
初始化
WebViewController
dartlate final WebViewController controller; @override void initState() { super.initState(); controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) // 允许 JS ..setBackgroundColor(const Color(0x00000000)) // 透明背景 ..setNavigationDelegate( NavigationDelegate( onProgress: (p) => debugPrint('progress = $p%'), onPageStarted: (url) => debugPrint('start $url'), // onPageFinished: (url) => _handleBackForbid(), onWebResourceError: (err) => debugPrint('error $err'), onNavigationRequest: (req) { // 拦截跳转示例 if (req.url.startsWith('myapp://')) return NavigationDecision.prevent; return NavigationDecision.navigate; }, ), ) ..loadRequest(Uri.parse('https://www.geekailab.com')); }
-
渲染 WebView
dart@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('WebView Demo')), body: WebViewWidget(controller: controller), ); }

三、常用 API 速览
功能 | 方法 |
---|---|
JS 开关 | setJavaScriptMode(JavaScriptMode) |
背景色 | setBackgroundColor(Color) |
拦截导航 | setNavigationDelegate(NavigationDelegate) |
加载 URL/POST | loadRequest(Uri, method, headers, body) |
缩放开关 | enableZoom(bool) |
自定义 UA | setUserAgent(String) |
前进/后退/刷新 | goBack() , goForward() , reload() |
注入 JS 并取结果 | runJavaScript() , runJavaScriptReturningResult() |
除了注入 JS、拦截导航和 POST 请求以外,其余 API 多半是"锦上添花"。别为了"能用"把控制器写成一坨神对象,按需引用即可。
四、NavigationDelegate
深度解读
dart
NavigationDelegate(
onNavigationRequest: (req) { ... }, // 拦截跳转
onPageStarted: (url) { ... },
onPageFinished: (url) { ... },
onProgress: (p) { ... }, // 加载进度 0~100
onWebResourceError: (err) { ... }, // 资源加载失败
onUrlChange: (change) { ... }, // 重定向等
);
- 防止某些 Scheme 跳转 :检查
req.url
,遇到第三方支付、拨号等自定义协议可prevent
。 - H5 与原生交互 :H5 可通过改变 URL
location.href = "myapp://close"
触发拦截,Native 再做动作。简单粗暴但最兼容。 - 自定义错误页 :
onWebResourceError
中controller.loadHtmlString(customHtml)
即可替换 404/SSL 失败页面。
示例演示
JS 与 Dart 双向通信
-
Dart → JS :
runJavaScript('alert("hello")')
-
JS → Dart (iOS 专属
WKScriptMessageHandler
/ AndroidJavaScriptChannel
):dartcontroller.addJavaScriptChannel( 'FlutterBridge', onMessageReceived: (msg) => print(msg.message), );
JS 侧:
FlutterBridge.postMessage('Hi from JS')
文件上传 / 下载
Android 13+ 需要在 AndroidManifest.xml
添加 <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
。
如需自定义下载,可拦截 onNavigationRequest
中的 Content-Disposition: attachment
,改由 dio
或 url_launcher
处理。
3.
Cookie 与登录态同步
dart
import 'package:webview_cookie_manager/webview_cookie_manager.dart';
final cookieManager = WebviewCookieManager();
await cookieManager.setCookie(
url: 'https://your.site',
name: 'token',
value: 'abc123',
domain: 'your.site',
);
Android Hybrid Composition(性能与兼容权衡)
-
Flutter 3.19 起默认启用 Virtual Display;复杂页面可强制 Hybrid:
kotlinWebViewPlatform.instance = SurfaceAndroidWebView(); // Android 主工程
-
前瞻:官方 roadmap 计划在 2025 H1 合并两种模式,统一 GPU 合成路径,提升手势流畅度。
多窗口 / 弹窗
Web 侧 window.open
会触发 onCreateWindow
(未开放 API)。目前只能拦截 URL 自行 launchUrl
到浏览器或新页面。
6.
安全与隐私
- iOS 14+ 默认启用【智能防跟踪】,可能导致第三方 Cookie 失效。必要时后端改用 First-party Token。
- Android WebView 会跟随系统更新,旧设备 < 7.0 建议提示升级或降级页面功能。
注意点
- 异步加载状态 :搭配
ValueListenableBuilder
或Rx
管理progress
,避免全局setState()
。 - 资源释放 :
State.dispose()
中无需手动销毁 WebView,Controller 生命周期随 Widget 自动回收,但避免在列表中频繁创建/销毁。 - 滚动冲突 :对于嵌套在
NestedScrollView
内的 WebView,记得把外层设置physics: const NeverScrollableScrollPhysics()
,或使用GestureDetector
透传。 - iOS 导航返回 :若页面自带返回逻辑,先
if (await controller.canGoBack()) controller.goBack(); else Navigator.pop(context);
。 - SEO / UA 伪装 :
setUserAgent
可模拟 PC 浏览器,配合meta viewport
可以获得更佳排版,但有时会触发广告/反爬虫检测。
小结
webview_flutter
能覆盖 90% H5 容器需求,但复杂 Hybrid App 仍需原生桥或第三方插件补位。- 未来趋势是 "Mini-Browser-as-a-Service" ------官方正在推进统一多媒体播放、下载管理和窗口控制接口;
- 真正的坑常在"JS 与原生通信"与"多端 Cookie 同步"两处,尽量提前做兼容测试。
五、最小可运行 Demo(精简)
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class WebDemoPage extends StatefulWidget {
const WebDemoPage({super.key});
@override
State<WebDemoPage> createState() => _WebDemoPageState();
}
class _WebDemoPageState extends State<WebDemoPage> {
late final WebViewController _c;
@override
void initState() {
super.initState();
_c = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel('NativeBridge',
onMessageReceived: (m) => debugPrint('From JS: ${m.message}'))
..loadRequest(Uri.parse('https://flutter.dev'));
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(actions: [
IconButton(
icon: const Icon(Icons.code),
onPressed: () =>
_c.runJavaScript('NativeBridge.postMessage("Ping from Dart")'),
)
]),
body: WebViewWidget(controller: _c),
);
}
结语
- 项目上线前 Checklist:消息格式统一、权限声明、Cookie 测试、iOS back 手势与物理返回键兼容、弱网体验。
- 如果需求只是"打开一个活动页 + 与原生分享/支付沟通",用 JSChannel + scheme 已足够。
- 若要 复杂表单、长连接、离线缓存 ,采用
flutter_inappwebview
。