Flutter与WebView通信方案示例详解

目录
  • 背景
  • WebView组件选择
  • webview_flutter通信方式调研
    • Flutter -> WebView通信方式
    • 问题
    • WebView -> Flutter通信方式
  • JSBridge通信模块封装
    • 发布订阅
    • 请求响应
    • 代码实现——Flutter端
    • 代码实现——web端
  • 结尾

背景

最近做Flutter应用开发,需要通过WebView嵌入前端web页面,而且Flutter与前端web有数据通信的需求。因此,笔者关于Flutter与WebView通信方式做了调研,并封装了一套支持请求响应和发布订阅的两套通信模式的JSBridge SDK。

WebView组件选择

Flutter三方库,使用最多的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通信方案的资料请关注我们其它相关文章!

(0)

相关推荐

  • 正确在Flutter中添加webview实现详解

    目录 前言 安装 运行项目遇到的问题 前言 为什么要在flutter中引入webview?这不是废话么,当然是为了加载一个网页,这不是移动端最基本的需求么,哈哈!说的真不错,接下来我要是告诉你我的用法,你可能要大吃一惊.我的用处很简单,那就是在webview中再加载一个flutter编译成web的项目.有没有吓到你.别怕,我这么做的原因很简单,就是为了热更新.可能在flutter中实现热更新的方法有很多,但我敢说我这么做就是最好的热更新方式.当我内容发生变更是时候,我不需要继续去审核,只需要在服

  • Flutter WebView 预加载实现方法(Http Server)

    目录 背景 分析 HttpServer 接下来? 资源配置 下载解压与本地存储 版本管理与更新 获取LocalServer Url并加载Webview 兜底措施 统一管理 展示与分析 总结 Demo 背景 WebView是在APP中,可以很方便的展示web页面,并且与web交互APP的数据.方便,并且更新内容无需APP发布新版本,只需要将最新的web代码部署完成,用户重新刷新即可. 在WebView中,经常能够听到的一个需求就是:减少首次白屏时间,加快加载速度.因为加载web页面,必然会受到网络

  • Flutter webview与网页通讯交互实现

    目录 前言 预览 具体实现 flutter中使用ds_bridge 网页端使用dsbridge_flutter 总结 前言 在app开发中我们有JSBridge来实现app和网页端通讯,现参考JSBridge实现了Flutter webview和网页端通讯. 预览 flutter import 'package:ds_bridge/ds_bridge.dart'; class JsBridgeUtil { // 向H5调用接口 static executeMethod(flutterWebVie

  • Flutter WebView性能优化使h5像原生页面一样优秀

    目录 引言 服务端渲染 css 放哪里 更新 css 如何利用本地 css 快速显示页面 浏览器渲染 如何启动本地server 如何让 WebView 的页面请求走本地服务 优化图片请求 代码实现 代码逻辑 关于图片类型 关于图片地址 把图片缓存到磁盘. 总结一下 服务端染页面方案 浏览器渲染方案 图片缓存 番外 引言 WebView 的文章分两篇 在 Flutter 中使用 webview_flutter 4.0 | js 交互 Flutter WebView 性能优化,让 h5 像原生页面一

  • js 交互在Flutter 中使用 webview_flutter

    目录 正文 环境准备 最简示例 WebView 的小大 网页自己报告高度 无法修改页面 在网页中调用 Flutter 页面 拦截 url js 调用 JavaScriptChannel 定义的方法 总结 正文 已经有很多关于 Flutter WebView 的文章了,为什么还要写一篇.两个原因: Flutter WebView 是 Flutter 开发的必备技能 现有的文章都是关于老版本的,新版本 4.x 有了重要变化,基于 3.x 的代码很多要重写. WebView 的文章分两篇 在 Flut

  • Android开发之Flutter与webview通信桥梁实现

    前言 最近业务有需求需要在flutter内使用webview进行内嵌H5进行展示,此时需要涉及到H5与flutter之间额通信问题.比如发送消息或者H5调用Flutter的相机等等 webview选型 这里我们使用官方维护的插件webview_flutter 如何通信? webview在初始化的时候需要向容器内注册一个全局方法供H5进行调用 WebView( initialUrl: 'https://flutter.dev', javascriptMode: JavascriptMode.unr

  • Flutter 中 Dart的Mixin示例详解

    原文在这里.写的不错,推荐各位看原文. 这里补充一下Mixin的定义: 只要一个类是继承自Object的而且没有定义构造方法,那么这个类可以是一个Mixin了.当然,如果你想让mixin的定义更加的清晰,可以使用mixin关键字开头来定义.具体请参考这里 原文截图体会一下风格. 正文 在经典的面向对象编程语言里一定会有常规的类,抽象类和接口.当然,Dart也有它自己的接口,不过那是另外的文章要说的.有的时候阴影里潜伏者另外的野兽:Mixin!这是做什么的,如何使用?我们来一起发现. 没有mixi

  • Android Flutter实现3D动画效果示例详解

    目录 前言 AnimatedWidget 简介 3D 旋转动画的实现 总结 前言 上一篇我们介绍了 Animation 和 AnimationController 的使用,这是最基本的动画构建类.但是,如果我们想构建一个可复用的动画组件,通过外部参数来控制其动画效果的时候,上一篇的方法就不太合适了.在 Flutter 中提供了 AnimatedWidget 组件用于构建可复用的动画组件.本篇我们用 AnimatedWidget 来实现组件的3D 旋转效果,如下图所示. AnimatedWidge

  • Flutter状态管理Bloc使用示例详解

    目录 前言 两种使用模式 Cubit模式 最后 前言 目前Flutter三大主流状态管理框架分别是provider.flutter_bloc.getx,三大状态管理框架各有优劣,本篇文章将介绍其中的flutter_bloc框架的使用,他是bloc设计思想模式在flutter上的实现,bloc全程全称 business logic ,业务逻辑的意思,核心思想就是最大程度的将页面ui与数据逻辑的解耦,使我们的项目可读性.可维护性.健壮性增强. 两种使用模式 首先第一步引入插件: flutter_bl

  • Flutter之 ListView组件使用示例详解

    目录 ListView的默认构造函数定义 默认构造函数 ListView.builder ListView.separated 固定高度列表 ListView 原理 实例:无限加载列表 添加固定列表头 总结 ListView的默认构造函数定义 ListView是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件,并且它也支持列表项懒加载(在需要时才会创建).我们看看ListView的默认构造函数定义: ListView({ ... //可滚动widget公共参数 Axis scrollD

  • Flutter Widget之FutureBuilder使用示例详解

    目录 正文 正文 本质上Flutter和Dart是异步的,Dart是Futures使你能够管理IO而不用担心线程或死锁. 例如,从应用程序外部加载数据需要时间,而Futures允许Dart先处理其他任务直到请求的数据可用. 但是涉及Future时,你如何构建Flutter小部件呢? 输入FutureBuilder,这是处理Futures的构造器 FutureBuilder( future: _data, builder: _myBuilderFunction, ) FutureBuilder让你

  • DoytoQuery 聚合查询方案示例详解

    目录 1. 引言 2. 聚合查询映射 2.1. 前缀映射 2.2. 分组聚合 2.3. HAVING 2.4. 动态查询 2.5. 查询接口定义 3. 完整示例 4. 总结 1. 引言 聚合查询是数据库提供的另一种常用的用于数据的统计和计算的查询功能,它通过提供一系列聚合函数来汇总来自多个行的信息. DoytoQuery采用字段前缀映射的方式来将字段名映射为聚合函数,再配合@GroupBy注解,Having接口以及Query对象,完成整条聚合查询语句的映射. 2. 聚合查询映射 2.1. 前缀映

  • Flutter web bridge 通信总结分析详解

    目录 缘起 通信方式 APP 中 JS & dart call Flutter web 中 JS & dart call dart 调用 js js 调用 dart summary 缘起 公司医疗业务人手比较少[小而美]的团队~ 较少采用的前端技术架构是: toC:小程序 toB2C: Flutter + H5(SPA - React)[build

  • DoytoQuery中的分页排序方案示例详解

    目录 引言 分页 分页接口 排序 请求对象 响应对象 小结 引言 分页和排序是数据库提供的两项基本的查询功能. 以MySQL为例,一条典型的SQL查询语句如下: SELECT * FROM t_user ORDER BY create_time DESC, username ASC LIMIT 10 OFFSET 20 那么在前后端交互中,前端应该如何向后端传递分页和排序有关的信息呢?需要传递哪些参数?参数的意义和格式又是什么? 分页 分页的语句为LIMIT 10 OFFSET 20,其中10为

  • 在Android环境下WebView中拦截所有请求并替换URL示例详解

    需求背景 接到这样一个需求,需要在 WebView 的所有网络请求中,在请求的url中,加上一个xxx=1的标志位. 例如 http://www.baidu.com 加上标志位就变成了 http://www.baidu.com?xxx=1 寻找解决方案 从 Android API 11 (3.0) 开始,WebView 开始在 WebViewClient 内提供了这样一条 API ,如下: public WebResourceResponse shouldInterceptRequest(Web

  • Flutter的键值存储数据库使用示例详解

    目录 Flutter 键值存储数据库 unqlite unqlite_flutter 快速上手 简单键值对存储 JSON 为什么你应该使用unqlite_flutter? Flutter 键值存储数据库 键值存储是开发中十分常见的需求,在Flutter开发中,一般使用 shared_preferences 插件来实现.shared_preferences 本质上就是将键值对保存到一个XML文件中进行持久化. 而shared_preferences 实际上存在一定缺陷,譬如其性能较差,不适合处理大

随机推荐