Flutter与H5页面的交互

1,086 阅读4分钟

一、交互总览:H5 ↔ Flutter 有哪几条“通道”?

通道方向场景核心 API
① JSChannelH5 → FlutterH5 主动把事件或数据回传给原生addJavaScriptChannel()channel.postMessage() medium.com
② runJavaScriptFlutter → H5原生调用 JS 方法、读 DOM、注入脚本runJavaScript() / runJavaScriptReturningResult() pub.dev
③ Navigation 拦截双向用 URL scheme 作为信号(支付宝、分享、关闭页等)NavigationDelegate.onNavigationRequest
④ postMessage (未来)H5 ↔ FlutterWebMessageChannel/Listener;目前官方还未完备,依赖社区或 InAppWebView inappwebview.dev

在 2025 年中,官方 webview_flutter稳妥做法仍是 ①+②+③。④ 正在讨论阶段,若需要 window.postMessage 全特性,可临时转用 flutter_inappwebview


二、最常用的“JSChannel”双向通信

1. 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 侧发送数据

<script>
function notifyPaySuccess(orderId) {
  NativeBridge.postMessage(JSON.stringify({
    type: 'paySuccess',
    payload: { orderId }
  }));
}
</script>

3. Flutter 调 JS 并拿结果

final price = await ctrl
    .runJavaScriptReturningResult('getCartTotal()'); // JS 返回值自动转 Dart
print('总价 = $price 元');

三、导航拦截:最兼容但“笨一点”的方案

..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 CookieiOS 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 步)

  1. 添加依赖

    flutter pub add webview_flutter
    
  2. 导入包

    import 'package:webview_flutter/webview_flutter.dart';
    
  3. 初始化 WebViewController

    late 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'));
    }
    
  4. 渲染 WebView

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(title: const Text('WebView Demo')),
        body: WebViewWidget(controller: controller),
      );
    }
    

20250622205248_rec_.gif

三、常用 API 速览

功能方法
JS 开关setJavaScriptMode(JavaScriptMode)
背景色setBackgroundColor(Color)
拦截导航setNavigationDelegate(NavigationDelegate)
加载 URL/POSTloadRequest(Uri, method, headers, body)
缩放开关enableZoom(bool)
自定义 UAsetUserAgent(String)
前进/后退/刷新goBack(), goForward(), reload()
注入 JS 并取结果runJavaScript(), runJavaScriptReturningResult()

除了注入 JS、拦截导航和 POST 请求以外,其余 API 多半是“锦上添花”。别为了“能用”把控制器写成一坨神对象,按需引用即可。

四、NavigationDelegate 深度解读

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 再做动作。简单粗暴但最兼容。
  • 自定义错误页onWebResourceErrorcontroller.loadHtmlString(customHtml) 即可替换 404/SSL 失败页面。

示例演示

  1. JS 与 Dart 双向通信

    • Dart → JSrunJavaScript('alert("hello")')

    • JS → Dart(iOS 专属 WKScriptMessageHandler / Android JavaScriptChannel):

      controller.addJavaScriptChannel(
        'FlutterBridge',
        onMessageReceived: (msg) => print(msg.message),
      );
      

      JS 侧:FlutterBridge.postMessage('Hi from JS')

  2. 文件上传 / 下载

    Android 13+ 需要在 AndroidManifest.xml 添加 <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
    如需自定义下载,可拦截 onNavigationRequest 中的 Content-Disposition: attachment,改由 diourl_launcher 处理。

  3. Cookie 与登录态同步

    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',
    );
    
  4. Android Hybrid Composition(性能与兼容权衡)

    • Flutter 3.19 起默认启用 Virtual Display;复杂页面可强制 Hybrid:

      WebViewPlatform.instance = SurfaceAndroidWebView(); // Android 主工程
      
    • 前瞻:官方 roadmap 计划在 2025 H1 合并两种模式,统一 GPU 合成路径,提升手势流畅度。

  5. 多窗口 / 弹窗

    Web 侧 window.open 会触发 onCreateWindow(未开放 API)。目前只能拦截 URL 自行 launchUrl 到浏览器或新页面。

  6. 安全与隐私

    • iOS 14+ 默认启用【智能防跟踪】,可能导致第三方 Cookie 失效。必要时后端改用 First-party Token。
    • Android WebView 会跟随系统更新,旧设备 < 7.0 建议提示升级或降级页面功能。

注意点

  • 异步加载状态:搭配 ValueListenableBuilderRx 管理 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(精简)

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