Flutter + Web:深度剖析双向通信的混合应用开发实践

[复制链接]
发表于 昨天 09:46 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
移动端要跨平台又要快速改 UI,"Flutter 当容器、Web 当界面"是一种常见解法,但真正决定这套方案好不好用的,是 Flutter 与 Web 之间那条双向通信通道。这篇以 flutter-web-interaction 项目为例,把"拉模式"(Web 调原生)和"推模式"(原生推 Web)两条链路、从点按钮到拿到图片字节的完整代码走一遍,顺便讲讲 develop 时的调试体验。

项目背景与整体思路

原生开发性能好,但 iOS / Android 两套代码成本太高;Web 技术迭代快、生态繁荣,可原生能力和离线体验又不够。flutter-web-interaction 就是给这个矛盾找的一个折中:用 Flutter 做高性能的原生容器,用 Web 做灵活的 UI 层,并在两者之间搭了一套完整的双向通信机制。

这种结构特别适合 UI 要频繁改、业务逻辑变化多,同时又得用部分原生能力的场景,比如营销活动页、内容型应用、内部工具。



一、架构:原生层和 UI 层各干什么

整个项目按"职责分离"来搭,原生能力和 UI 显示是明确解耦的。


  • flutter_app — 原生容器层(The "Shell")

    • 角色:一个标准的 Flutter 应用,是整个应用的底层框架和原生能力的"超级供给方";
    • 核心技术:flutter_inappwebview。它不只是个简易 WebView,而是在 Flutter 和 Web 之间架了一座功能很全的桥;
    • 职责:

      • 承载 Web:作为浏览器内核,加载并渲染 web 项目;
      • 封装原生能力:把平台相关的原生功能(如 image_picker、printing)封装成统一的 Dart 接口;
      • 暴露 API:通过 JavaScript Handler 把封装好的 Dart 接口暴露给 Web 调用;
      • 主动推送:监听应用生命周期等原生事件,并把信息主动推给 Web。


  • web — UI 表现层(The "View")

    • 角色:一个基于 Vue 3、Vite 和 TypeScript 构建的现代化 Web 应用;
    • 核心技术:Vue 3(<script setup>)、Vant 4(UI)、UnoCSS(CSS)、vConsole(调试);
    • 职责:

      • 界面渲染与交互:负责所有用户界面的展示和用户操作的响应;
      • 调用原生 API:通过统一的请求模块(request.ts)调用 Flutter 暴露的接口;
      • 监听原生事件:通过 window.addEventListener 接收来自 Flutter 的主动推送。




架构图
  1. graph TD
  2.     subgraph "用户设备"
  3.         subgraph "Flutter App (原生容器层)"
  4.             A[InAppWebView]
  5.             B[原生能力封装 <br/> (image_picker, printing, etc.)]
  6.             C[生命周期监听 <br/> (AppLifecycleState)]
  7.         end
  8.         subgraph "Web App (UI表现层)"
  9.             D[Vue 3 / Vant UI]
  10.             E[JavaScript Bridge <br/> (request.ts)]
  11.         end
  12.     end
  13.     D -- 用户操作 --> E
  14.     E -- 1. "拉"模式: 调用原生API --> A
  15.     A -- JS Handler --> B
  16.     B -- Dart/Native --> F[相机/相册/打印机]
  17.     C -- 2. "推"模式: 主动推送事件 --> A
  18.     A -- postWebMessage --> E
  19.     B -- 返回结果 --> A
  20.     A -- evaluateJavascript --> E
复制代码

二、双向通信两条通道详解

一个混合应用能不能立得住,关键就看原生和 Web 之间通信顺不顺。这个项目实现了两种方向,合起来是一套完整的双向数据流。

1. 从 Web 到 Flutter("拉"模式,Web 主动调用)

这是最核心的一条路。Web 端像调后端接口一样,异步请求 Flutter 提供的原生能力。

实现原理


  • 注册处理器(Flutter):创建 InAppWebView 时,通过 addJavaScriptHandler 注册一个全局的 JS 处理器,名字叫 FlutterApp;
  • 封装请求(Web):request.ts 里的 serverApi 函数是关键。它把请求参数(method、params)包起来,并为每个请求生成一个唯一的 callbackId。这个 ID 的作用,是等 Flutter 处理完之后能准确回调到 Web 端对应的那个 Promise;
  • 发起调用(Web):调用 window.flutter_inappwebview.callHandler('FlutterApp', message),把封装好的 message 对象发出去;
  • 分发处理(Flutter):FlutterApp 处理器收到 message。在 bridge.dart 的 handerWebMessage 方法里,用一个 switch 语句按 message 的 method 字段,把请求分发到不同的原生功能实现模块(例如 common_utils.dart 里的 chooseImage);
  • 返回结果(Flutter):原生功能执行完拿到结果(或错误),Flutter 把结果和 callbackId 一起打包,通过 webViewController.evaluateJavascript() 执行一段预设的全局 JS 函数(如 window.appJSBridge.callback(result));
  • 响应回调(Web):这个全局 JS 函数按 callbackId 找到对应的 Promise,调用 resolve 或 reject,一次异步调用就闭环了。


通信数据流图

2. 从 Flutter 到 Web("推"模式,Flutter 主动推送)

当状态变化是从原生那侧发起时,就需要主动通知 Web。

实现原理


  • 监听事件(Flutter):通过 WidgetsBindingObserver 监听 AppLifecycleState(paused、resumed 等);
  • 推送消息(Flutter):状态变化时调用 webViewController.postWebMessage(),这个方法会向 Web 页面的 window 对象派发一个标准的 MessageEvent;
  • 接收消息(Web):Web 端在全局监听 message 事件(window.addEventListener('message', ...))就能拿到 Flutter 推过来的数据。


这条路简单直接,很适合状态同步、事件通知这类场景。

三、"从相册选图"的完整代码走查

拿"从相册选择图片"为例,把整条链路走一遍。


  • 用户点击(Web)

    • /web/src/views/index.vue
    1. <van-button @click="chooseImage('')">从相册选择</van-button>
    复制代码
    1. const chooseImage = (source = 'camera') => {
    2.   serverApi({ method: 'chooseImage', params: { source } }) // 触发调用
    3.     .then(res => {
    4.       // 步骤 7: 接收到Flutter返回的数据并处理
    5.       const blob = new Blob([new Uint8Array(res.bytes)], { type: getMimeType(res.name) });
    6.       imageUrl.value = URL.createObjectURL(blob);
    7.     });
    8. }
    复制代码
  • JS Bridge 发送请求(Web)

    • /web/src/utils/request.ts
    1. // serverApi 内部调用 callAppMethod, callAppMethod 内部...
    2. window.flutter_inappwebview.callHandler('FlutterApp', {
    3.   method: 'chooseImage',
    4.   params: { source: '' },
    5.   callbackId: 'uuid-1234' // 假设生成了一个唯一ID
    6. });
    复制代码
  • Handler 接收与分发(Flutter)

    • /flutter_app/lib/main.dart
    1. webViewController.addJavaScriptHandler(
    2.   handlerName: "FlutterApp",
    3.   callback: (args) async {
    4.     // 步骤 4: args[0] 就是 JS 发送的 message 对象
    5.     return handerWebMessage(args[0], (runJSFunctionString) {
    6.       // 步骤 6: 得到结果后,通过此回调执行JS
    7.       webViewController.evaluateJavascript(source: runJSFunctionString);
    8.     });
    9.   },
    10. );
    复制代码

    • /flutter_app/lib/common/bridge.dart
    1. // handerWebMessage 内部
    2. switch (method) {
    3.   case 'chooseImage':
    4.     // 步骤 5: 分发到具体实现
    5.     result = await CommonUtils.chooseImage(params);
    6.     break;
    7.   // ... other cases
    8. }
    9. // ... 组装JS回调字符串并执行
    复制代码
  • 原生功能执行(Flutter → Native)

    • /flutter_app/lib/common/common_utils.dart
    1. static Future<Map<String, dynamic>> chooseImage(Map<String, dynamic> params) async {
    2.   final ImagePicker picker = ImagePicker();
    3.   final XFile? image = await picker.pickImage(source: ...);
    4.   if (image != null) {
    5.     final Uint8List bytes = await image.readAsBytes();
    6.     return {'name': image.name, 'bytes': bytes}; // 返回包含图片字节的数据
    7.   }
    8.   return {};
    9. }
    复制代码


走到这里,一条完整调用链就闭合了:数据从 Web UI 发出,穿过 WebView 边界,由 Flutter 执行原生操作,再把结果安全送回 Web,全程清楚可控。

四、开发体验(DX)


  • 双重热重载:Web 端的 Vite HMR 和 Flutter 端的 Hot Reload / Restart 可以同时生效。改 UI 代码网页立刻刷新,改 Dart 代码 Flutter 逻辑也即时更新,效率很高;
  • 清晰的调试:Web 端集成了 vconsole,App 内能直接打开 Web 控制台,查看日志、网络请求和存储;Flutter 端则可以用标准的 Flutter DevTools 或 IDE 的 Debugger 打断点;
  • 生态复用:可以放心复用 npm / pnpm 生态里海量的前端库来堆界面,同时也用得上 pub.dev 上丰富的 Flutter 插件拿原生能力。


总结与后续方向

flutter-web-interaction 不只是一个功能演示,更像是一种高效的混合应用开发范式:它把 Flutter 当"原生胶水层"的能力和 Web 生态的快速迭代能力接在了一起,对那种 UI 需要频繁更新、同时又对原生性能有要求的场景,吸引力相当大。

未来可以探索的方向:


  • 离线化:把 Web 资源打进 App 包内,实现离线访问;
  • 性能极致化:对动画、手势这类要求极高的交互,可以用 Platform Channels 直接跟原生视图通信,绕开 WebView;
  • 统一状态管理:探索在 Web 和 Flutter 之间同步状态,支撑更复杂的数据交互。


通读下来最值得记住的一点:callbackId 那套"请求 ID + 全局回调"的设计,是让 Web 端保持 Promise 写法的关键,换成别的功能模块时照抄这个模式就行。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表