开发者

Flutter与WebView通信方案示例详解

开发者 https://www.devze.com 2023-01-05 10:18 出处:网络 作者: SugarTurboS
目录背景WebView组件选择webview_flutter通信方式调研Flutter -> WebView通信方式问题WebView -> Flutter通信方式jsBridge通信模块封装发布订阅请求响应代码实现——Flutter端代码实现—&mdash
目录
  • 背景
  • WebView组件选择
  • webview_flutter通信方式调研
    • Flutter -> WebView通信方式
    • 问题
    • WebView -> Flutter通信方式
  • jsBridge通信模块封装
    • 发布订阅
    • 请求响应
    • 代码实现——Flutter端
    • 代码实现——web端
  • 结尾

    背景

    最近做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端允许报错,如下:

    Flutter与WebView通信方案示例详解

    从错误信息来看,是执行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。

    发布订阅

    发布订阅是一种标准的消息通信模式,主要用于两个不相关联解耦的模块进行数据通信。“订阅方”只需要向“发布订阅模块”订阅消息,当“发布订阅模块”接收到“发布方”消息时,则把消息转发到所有“订阅方”,如下图所示:

    Flutter与WebView通信方案示例详解

    请求响应

    “请求方”发起一个请求消息,“响应方”接收到请求消息,做一些逻辑处理,回应一个响应消息到“请求方”。例如:http协议就属于请求响应模式,可以把web端作为客户端,flutter端作为服务端。如下图所示:

    Flutter与WebView通信方案示例详解

    代码实现——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 {
      stati开发者_C入门c const String NATIVE_CHANNEL = 'nativeChannel'; // 原生通信通道名称
      static const String JAVASCRIPT_CHANNEL = 'javascriptChannel'; // js通信通道名称
      WebViewController? _controller;
      Map<String, List<SubscribeCallback>> _subscribeCallbackMajsp = {};
      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: jsBridgjse.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;
          conjsst 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));
            }
          }
        };
      }
      // 请求响应
    js  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

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号