我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Flutter与WebView通信方案示例详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Flutter与WebView通信方案示例详解

背景

最近做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;

结尾

以上就是Flutter与WebView通信方案示例详解的详细内容,更多关于Flutter WebView通信方案的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Flutter与WebView通信方案示例详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Flutter与WebView通信方案示例详解

这篇文章主要为大家介绍了Flutter与WebView通信方案示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-04

Python实现UDP与TCP通信的示例详解

UDP是一种无连接的、不可靠的传输协议;TCP是一种可靠的、面向连接的传输协议。这篇文章主要介绍了Python实现UDP与TCP通信的方法,需要的可以参考一下
2023-03-23

JavaNIO通信基础示例详解

这篇文章主要为大家介绍了JavaNIO通信基础使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2022-12-26

Android中webview与JS交互、互调方法实例详解

Android中webview与JS交互、互调方法实例详解 前言: 对于试水的功能,一般公司都会采用H5的方式来开发,可以用很少的资源与很短的项目工期来完成。 但许多情况下,H5页面会需要一些原生持有的一些如用户信息之类的数据,一些交互也
2022-06-06

SharedWorker 多页面相互通信示例详解

这篇文章主要为大家介绍了SharedWorker 多页面相互通信示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2022-12-08

Android AIDL通信DeadObjectException解决方法示例

这篇文章主要为大家介绍了Android AIDL通信DeadObjectException解决的方法示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-03-09

C#实现TCP和UDP通信的示例详解

这篇文章主要为大家详细介绍了C#实现TCP和UDP通信的相关知识,文中的示例代码讲解详细,具有一定的学习价值,感兴趣的小伙伴可以了解一下
2023-03-01

实时通信Socket io的使用示例详解

这篇文章主要为大家介绍了实时通信Socket io的使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2022-11-16

DoytoQuery聚合查询方案示例详解

这篇文章主要为大家介绍了DoytoQuery聚合查询方案示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2022-12-27

C++ BoostAsyncSocket实现异步反弹通信的案例详解

这篇文章主要为大家详细介绍了C++ BoostAsyncSocket如何实现异步反弹通信,文中的示例代码讲解详细,具有一定的学习价值,感兴趣的可以了解一下
2023-03-19

redis分布式ID解决方案示例详解

目录常用的分布式ID解决方案UUIDSnowflakeSnowflake算法的Java代码:LeafLeaf算法的Java代码:基于数据库自增ID生成基于UUID生成基于Redis生成基于ZooKeeper生成常用的分布式ID解决方案在分布
2023-03-07

Node.js高级编程使用RPC通信示例详解

这篇文章主要为大家介绍了Node.js高级编程使用RPC通信示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-12

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录