likes
comments
collection
share

Flutter内嵌H5页面与前端通信:实现无缝交互的技术浅析

作者站长头像
站长
· 阅读数 22

前言

Flutter与H5的结合

Flutter是Google开发的一个跨平台的UI开发框架,同时也可以使用Dart编写本地代码,可以兼容各种移动端、Web和桌面端平台。H5是一种基于HTMLCSSJavascript的Web技术,广泛用于构建Web应用程序和网页。

在实际开发中,经常需要将FlutterH5进行结合,以展现更加丰富的应用场景。其中,Flutter中内嵌H5页面并且实现与之通信,可以使应用更加交互性,体验性更好,同时也能极大的扩展应用的功能。

为什么要使用Flutter内嵌H5页面?

Flutter内嵌H5页面有以下几个优势:

  • 可以快速实现Web应用中的某些功能,而无需将整个应用都重写为Flutter
  • 可以出色地展示H5页面,以便将Web内容移植到App中;
  • 可以通过H5页面与Flutter进行交互,获得更加流畅、快捷的用户体验。

综上所述,内嵌H5页面是非常有必要的。

实现方式

Flutter中的WebView组件介绍

Flutter中,可以使用flutter_webview_plugin库实现内嵌H5页面的功能。这个库是Flutter的一个用于Web视图插件的第三方库,依赖于AndroidiOS的原生Web View

安装该库:

dependencies:
  flutter_webview_plugin: ^0.4.0+1

这个库提供了两种方式加载Web视图:

  • WebviewScaffold:显示一个工具栏,并在屏幕上方显示一个带有Web视图的Scaffold。
  • Webview:直接在Widget树中包含一个Web视图。

下面是一个简单的Webview使用示例:

import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';

class MyWebView extends StatefulWidget {
  final String title;
  final String url;

  MyWebView({
    @required this.title,
    @required this.url,
  });

  @override
  _MyWebViewState createState() => _MyWebViewState();
}

class _MyWebViewState extends State<MyWebView> {
  final flutterWebViewPlugin = FlutterWebviewPlugin();

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    flutterWebViewPlugin.dispose(); // 组件销毁时需要释放
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return WebviewScaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      url: widget.url,
      initialChild: Center(
        child: CircularProgressIndicator(),
      ),
    );
  }
}

这里就生成了一个简单的带AppbarWebview视图。

WebView与H5页面通信方式的比较

在Flutter中,有两种方式实现Flutter与H5的通信:

  • JavascriptChannel:通过Javascript通信。
  • MethodChannel:通过建立Binder通道和Android原生代码通信。

JavascriptChannel在需求短时较为直观、实现相对简单,MethodChannel在处理较复杂的逻辑时,更加清晰明了。

Flutter与H5交互的核心技术WebViewJavascriptChannel

Flutter中,内嵌H5页面并且实现通信最为常见的方式就是使用WebViewJavascriptChannel了。

关于WebViewJavascriptChannel,以下是相关的介绍和使用示例。

WebViewJavascriptChannel接口定义:

abstract class WebViewJavascriptChannel {
  void postMessage(String message);
}

Flutter中,需要创建一个WebviewJavascriptChannel对象,并将其提供给WebView组件的构造器,其将在H5中调用。

下面是一个简单的使用WebViewJavascriptChannel的示例:

import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';

class MyWebView extends StatefulWidget {
  final String title;
  final String url;

  MyWebView({
    @required this.title,
    @required this.url,
  });

  @override
  _MyWebViewState createState() => _MyWebViewState();
}

class _MyWebViewState extends State<MyWebView> {
  final flutterWebViewPlugin = FlutterWebviewPlugin();
  TextEditingController controller = TextEditingController();

  @override
  void initState() {
    super.initState();
    flutterWebViewPlugin.onStateChanged.listen((WebViewStateChanged state) {
      print("onStateChanged: ${state.type} ${state.url}");
    });
    flutterWebViewPlugin.onUrlChanged.listen((String url) {
      print("onUrlChanged: $url");
      setState(() {
        controller.text = url;
      });
    });
    flutterWebViewPlugin.onDestroy.listen((_) {
      print("onDestroy");
    });
  }

  @override
  void dispose() {
    flutterWebViewPlugin.dispose(); // 组件销毁时需要释放
    super.dispose();
  }

  void onPageFinished(BuildContext context) {
    final channel = MethodChannel('webViewJSChannel');
    channel.setMethodCallHandler((MethodCall call) async {
      if (call.method == 'webToFlu') {
        String para = call.arguments;
        //处理接收的参数
        print('MethodChannel接收到的内容:' + para);
        //回传数据到H5
        flutterWebViewPlugin.evalJavascript(
            "jsFunction('${para}', '${DateTime.now().toString()}')");
      }
    });
    channel.invokeMethod('fluToWeb', {'msg': 'Flutter start!'});
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        children: [
          TextField(
            controller: controller,
            readOnly: true,
            decoration: InputDecoration(
              hintText: 'current url',
              contentPadding: EdgeInsets.all(10.0),
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(4.0),
              ),
            ),
          ),
          Expanded(
            child: WebviewScaffold(
              url: widget.url,
              hidden: true,
              initialChild: Container(
                color: Colors.white,
                child: Center(
                  child: CircularProgressIndicator(),
                ),
              ),
              withJavascript: true,
              hiddenPage: true,
              appBar: AppBar(
                title: Text(widget.title),
              ),
              javascriptChannels: <JavascriptChannel>[
                JavascriptChannel(
                    name: 'FlutterEvent',
                    onMessageReceived: (JavascriptMessage msg) {
                      print(msg.message);
                    })
              ].toSet(),
              onPageFinished: onPageFinished,
              withZoom: false,
            ),
          ),
        ],
      ),
    );
  }
}

上面的示例中,在页面加载完成之后,我们首先通过回调方法onPageStartedFlutter中创建了一个MethodChannel,声明了FlutterH5推送方法的名称和需要传递的数据。随后在H5中,接收到Flutter传递的初始化参数后,就可以通过window.Flutter.postMessage()来向Flutter发送数据。当Flutter接收到H5的消息之后,就可以通过MethodChannel写入到Flutter侧的方法,进行处理。

下面是一个Web页面示例:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>test channel</title>
</head>
<body>
	<button id='btn'>Click me to send message to Flutter</button>
	<p>Tips: 点击按钮即可通过JS打印Hello World并通过MethodChannel发送到Flutter。</p>

	<script>
            var channel = null;
            window.addEventListener("load", function() {
                    channel = new WebViewJavascriptChannel(bridge);
            });

            function bridge(msg) {
                document.getElementById('log').innerHTML = msg;
                var para = "Hello World";
                channel.postMessage(JSON.stringify({'type':'channelTest', 'content': para}));
	    }

	    function jsFunction(para1, para2) {
	    	console.log('接收到的内容:' + para1);
	    	console.log('当前时间:' + para2);
	    }

            document.getElementById('btn').addEventListener('click', function(e) {
                console.log('clicked!');
                console.log(channel);
                bridge('msg from h5');
            }, false);
	</script>
</body>
</html>

这个页面中,我们声明了通信的JavaSript Channel,并通过按钮点击事件完成数据的发送,同时在jsFunction()方法中,通过flutterWebViewPlugin.evalJavascript()方法向H5页面回传数据。

实际应用

在Flutter中实现微信、支付宝等第三方授权登陆

在开发中,经常需要使用到微信、支付宝等第三方授权登陆的功能,我们可以在Flutter中内嵌H5页面,通过H5页面来完成授权登陆操作,然后回传数据到Flutter中进行后续的操作。

以下是一份简单的微信授权登陆的示例代码:

import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';

class WechatAuthorizationPage extends StatefulWidget {
  @override
  _WechatAuthorizationPageState createState() =>
      _WechatAuthorizationPageState();
}

class _WechatAuthorizationPageState extends State<WechatAuthorizationPage> {
  final flutterWebViewPlugin = FlutterWebviewPlugin();
  String _url =
      "https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxe15b267e25e3f502&redirect_uri=https%3A%2F%2Fs.jianyi-ai.com%2Fapi%2Fselfdiagnose%2Fv1%2Fwechat%2Fcallback&response_type=code&scope=snsapi_userinfo&state=state&connect_redirect=1#wechat_redirect";

  @override
  void initState() {
    super.initState();
    flutterWebViewPlugin.onStateChanged.listen((WebViewStateChanged state) {
      print("onStateChanged: ${state.type} ${state.url}");
    });
    flutterWebViewPlugin.onUrlChanged.listen((String url) {
      print("onUrlChanged: $url");
    });
    flutterWebViewPlugin.onDestroy.listen((_) {
      print("onDestroy");
    });
  }

  @override
  void dispose() {
    flutterWebViewPlugin.dispose(); // 组件销毁时需要释放
    super.dispose();
  }

  void onPageFinished(BuildContext context) {
    final channel = MethodChannel('webViewJSChannel');
    channel.setMethodCallHandler((MethodCall call) async {
      if (call.method == 'webToFlu') {
        Map para = json.decode(call.arguments);
        print("onPageFinished-para=$para");

        if (para.containsKey('code')) {
          // do something with the code
        } else {
          // do something with the error
        }
      }
    });

    flutterWebViewPlugin.evalJavascript(
        'setTimeout(function () { Flutter.postMessage(JSON.stringify({type:"pageLoaded", content:{}}));   }, 1000)');

    channel.invokeMethod('fluToWeb', {'msg': 'Flutter start!'});
  }

  void onStateChanged(BuildContext context, WebViewStateChanged state) {
    print(
        '[INFO]onStateChanged: ${state.type} ${state.url} ${state.canGoBack} ${state.canGoForward}');
    if (state.type == WebViewState.finishLoad) {
      flutterWebViewPlugin.evalJavascript(
          'setTimeout(function () { Flutter.postMessage(JSON.stringify({type:"pageLoaded", content:{}}));   }, 1000)');
    }

    if (state.type == WebViewState.abortLoad) {
      // _progressBarVisibility = false;
    }

    if (state.type == WebViewState.startLoad) {
      // _progressBarVisibility = true;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: WebviewScaffold(
        url: _url,
        hidden: true,
        initialChild: Container(
          color: Colors.white,
          child: Center(
            child: CircularProgressIndicator(),
          ),
        ),
        withJavascript: true,
        hiddenPage: true,
        appBar: AppBar(
          title: Text('微信授权登陆'),
        ),
        javascriptChannels: <JavascriptChannel>[
          JavascriptChannel(
              name: 'FlutterEvent',
              onMessageReceived: (JavascriptMessage msg) {
                print(msg.message);
              })
        ].toSet(),
        onPageFinished: onPageFinished,
        withZoom: false,
        //allowFileAccess: true,
        userAgent:
            "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/37.0.0.0 Mobile Safari/537.36",
      ),
    );
  }
}

在H5中与Flutter原生端交互:与Flutter全局变量通信、调用原生组件

H5页面中与Flutter原生端进行交互也是非常重要的。在H5中可以通过window调用FlutterAPI,以便获取Flutter全局的变量或者调用原生组件进行处理。

在这里我们介绍两个示例:

1.与Flutter全局变量通信

首先需要在Flutter中注册一个通信事件,例如我们可以创建一个名为globalEvent的事件,用于H5传递数据到Flutter全局变量。

import 'package:flutter/services.dart';

const MethodChannel _channel = MethodChannel('globalEvent');

void main() {
  _channel.setMethodCallHandler((MethodCall call) async {
    if(call.method == 'setGlobalVariable'){
      // 在这里处理H5传递的数据
      String data = call.arguments;
      globalVariable = data;
    }
  });
}

// 声明一个全局变量
String globalVariable = '';

接着,在H5中通过window对象调用我们注册的事件globalEvent,将数据传递过来。

// 在H5中传递数据给Flutter
window.flutter_inject_global_variable = function(data) {
  window.flutter_inject_global_variable_callback(data);
};
window.flutter_inject_global_variable_callback = function(data) {
  window.flutter_inject_global_variable_callback = null;
}

// H5传递数据给Flutter全局变量
window.flutter_inject_global_variable('Hello Flutter!');

当H5调用了window.flutter_inject_global_variable方法时,Flutter会接收到一个名为setGlobalVariable的事件,并将H5传递的数据存储到全局变量globalVariable中。

  1. 调用原生组件

Flutter中可以通过MethodChannel调用原生组件,同时需要在原生端注册一个Flutter的通信事件,例如我们创建一个名为callNativeComponent的事件。

import 'package:flutter/services.dart';

const MethodChannel _channel = MethodChannel('callNativeComponent');

void main() {
  _channel.setMethodCallHandler((MethodCall call) async {
    if(call.method == 'showDialog'){
      // 在这里调用原生组件
    }
  });
}

// 声明一个全局变量
String globalVariable = '';

接着,在H5中通过window对象调用callNativeComponent事件,使Flutter调用原生组件进行处理。

// H5中调用原生组件
window.call_native_component = function(type, data) {
  var params = JSON.stringify({
    "type": type,
    "data": data
  });
  return window.flutter_call_native_component(params);
};

// 在H5中调用原生组件
window.call_native_component('show_dialog', {'content': 'Hello Native!'});

H5调用window.call_native_component方法时,Flutter接收到名为showDialog的事件并使用原生组件处理传递的数据。原生端同时需要实现MethodChannel的相应方法,例如showDialog方法。

总结与展望

在本文中,我们介绍了Flutter内嵌H5页面的实现方式和与前端通信的技术。通过使用Flutterflutter_webview_plugin插件,我们能够很方便地在Flutter应用中内嵌H5页面,并且通过JavaScript Channel机制实现与前端的通信。

在前端与Flutter应用之间实现无缝的交互可以带来很多的好处。例如,在Flutter应用中使用H5页面可以方便地引入第三方的页面和组件,并且可以快速构建复杂的组合界面。同时,基于Flutter强大的性能和Flutter Widget的可组合性,可以实现更加复杂和优雅的界面效果。

不过,在实际的开发过程中,还需要注意一些问题。例如,FlutterH5之间图形渲染的差异和性能问题,以及在调试方面可能遇到的困难。同时,由于FlutterH5都在不断发展中,未来仍然需要持续关注技术的变化和趋势。不过随着Flutter应用的普及和技术的不断进步,我们相信Flutter内嵌H5页面将在移动应用开发中发挥更加重要和广泛的作用。

总之,在实际的项目中,应根据具体情况选择使用Flutter内嵌H5页面的方案。在合适的场景下,该技术能够帮助我们更好地利用各类资源,构建更加高效和优秀的移动应用。