美文网首页
Flutter:加载本地Html、WebView与JS交互

Flutter:加载本地Html、WebView与JS交互

作者: 爱抖腿的飞飞 | 来源:发表于2021-11-21 16:46 被阅读0次

    本次教程使用的是Flutter官方提供的WebView组件webview_flutter 2.3.1flutter_android 2.2.1

    一. WebView介绍

    以下为Flutter WebView官方的介绍,在Android采用原生的WebView实现,在IOS上采用WKWebView实现。可以看出Flutter目前没有自己的WebView引擎,可能若干年后会开发出属于Flutter的引擎,所以遇到问题多看Plugin源码

    On iOS the WebView widget is backed by a WKWebView; On Android the WebView widget is backed by a WebView.

    目前Flutter WebView提供的功能较少,文档中没写到的,可以理解为暂时不支持,如果就想做,建议修改Plugin代码。如果想换内核,比如Android端换腾讯X5内核,也可以修改Plugin端代码(修改Plugin代码只会修改本地对应版本的缓存,修改不能提交到Git)。本文就有修改Plugin代码需求,请往下看。

    由于我本人是Android出身,所以更多的是从Android开发的视角来说明。

    二. WebView使用

    添加依赖
    dependencies: webview_flutter: ^2.3.1

    引用包

    import 'package:webview_flutter/webview_flutter.dart';
    

    webview_flutter要求android minSdkVersion 19

    1. 加载URL

    WebView(initialUrl: "https://flutterchina.club/")
    

    2. 加载本地文件

    本地文件index.html在Flutter项目的路径为./assets/index.html

    2.1 Android加载本地文件

    Android WebView本身支持加载本地文件,上述路径在Android APK中的路径为android_asset/flutter_assets/assets/index.html,所以代码如下:

    String url = "";
    if (Platform.isAndroid) {
      url = "file:///android_asset/flutter_assets/assets/index.html";
    }
    ...
    WebView(initialUrl: url)
    

    2.2 IOS加载本地文件

    IOS WebView Plugin本身不支持加载本地文件,需要修改Plugin FlutterWebView.m代码,Plugin源码如下:

    - (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary<NSString*, NSString*>*)headers {
      NSURL* nsUrl = [NSURL URLWithString:url];
      if (!nsUrl) {
        return false;
      }
      NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl];
      [request setAllHTTPHeaderFields:headers];
      [_webView loadRequest:request];
      return true;
    }
    

    修改后IOS Plugin代码如下:

    - (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary<NSString*, NSString*>*)headers {
      NSURL* nsUrl = [NSURL URLWithString:url];
      NSLog(@"webview_flutter:  %@", nsUrl);
      NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl];
      [request setAllHTTPHeaderFields:headers];
      if([url hasPrefix:@"http"]) {
          [_webView loadRequest:request];
      }else{
          if (@available(iOS 9.0, *)) {
              NSURL *findUrl = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/Frameworks/App.framework/flutter_assets/webres/%@", [[NSBundle mainBundle] bundlePath], @"index.html"]];
              NSLog(@"Debug >>>> %@", findUrl);
              
              NSString *loadUrl = [findUrl.absoluteString stringByReplacingOccurrencesOfString:@"index.html" withString:url];
                        NSURL * url = [NSURL URLWithString:loadUrl];
              NSLog(@"Debug >>>> load url %@", url);
              [_webView loadFileURL:url allowingReadAccessToURL:[url URLByDeletingLastPathComponent]];
          } else {
              // Fallback on earlier versions
              NSLog(@"webview_flutter:  loadFileUrl error");
          }
      }
      return true;
    }
    

    Flutter代码如下:

    String url = "";
    if (Platform.isIOS) {
      url = "file://Frameworks/App.framework/flutter_assets/assets/index.html";
    }
    ...
    WebView(initialUrl: url)
    

    由于Flutter Dependencies 依赖版本规则问题,webview_flutter_wkwebview可能不定期升级,请以官方代码FlutterWebView.m为准,如果代码不一致,请按照以上思路修改代码。

    三. WebView详细说明

    1. WebView

    使用起来很简单,看一下WebView的完整参数,以下是整理简写的伪代码:

     WebView(
       onWebViewCreated : void Function(WebViewController controller),
       initialUrl : String?,
       javascriptMode : JavascriptMode = JavascriptMode.disabled,
       javascriptChannels : Set<JavascriptChannel>?,
       navigationDelegate : NavigationDelegate?,
       gestureRecognizers : Set<Factory<OneSequenceGestureRecognizer>>?,
       onPageStarted : (void Function(String url))?,
       onPageFinished : (void Function(String url))?,
       onProgress : (void Function(int progress))?,
       onWebResourceError : (void Function(WebResourceError error))?,
       debuggingEnabled : bool = false,
       gestureNavigationEnabled : bool = false,
       userAgent : String?,
       zoomEnabled : bool = true,
       initialMediaPlaybackPolicy : AutoMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
       allowsInlineMediaPlayback : bool  = false,
      )
    
    1. onWebViewCreated: 创建WebView时调用,返回WebViewController对象。

    2. initialUrl: WebView加载的URL,也可以指定本地文件,如assets/index.html

      • Android上的路径file:///android_asset/flutter_assets/assets/index.html,
      • IOS上的路径file://Frameworks/App.framework/flutter_assets/assets/index.html(由于IOS端不支持加载本地HTML,所以需要修改IOS端Plugin代码)。
    3. javascriptMode: 是否启用JavaScript,默认为JavascriptMode.disabled

      • JavascriptMode.disabled: 禁用JavaScript
      • JavascriptMode.unrestricted: 启用JavaScript
    4. javascriptChannels : JavaScript调用Flutter的方法渠道配置,常用方式。

    5. navigationDelegate : WebView导航拦截,点击链接跳转时触发。可以通过拦截指定特征的URL,用作与JavaScript交互。(个人不推荐使用:1. 不安全 2. HTTP1.1以下有长度限制)

      目前发现设置javascriptChannels后,navigationDelegate不会再触发,原因不得而知。

    6. gestureRecognizers: 处理WebView与Wideget嵌套时的手势交互。

    7. onPageStarted: 页面开始加载时触发,可以用来显示进度条。

    8. onPageFinished: 页面加载结束时触发,可以隐藏进度条。

    9. onProgress: 加载进度。

    10. onWebResourceError: 资源加载失败时触发,返回的数据因平台而异(就是包装了原生平台的错误信息)。

    11. debuggingEnabled: 调试开关。

      • Android可以使用Chrome调试WebView加载,使用方法
      • IOS可以使用Safari调试。
    12. gestureNavigationEnabled: 是否开始手势返回功能,默认关闭,只在IOS上有效

    13. userAgent: HTTP请求头User Agent配置。

    14. zoomEnabled: 是否开启手势缩放,默认开始。

      如果要关闭手势,在IOS上必须设置javascriptMode = JavascriptMode.unrestricted才会生效。

    15. initialMediaPlaybackPolicy: 媒体播放设置,默认为AutoMediaPlaybackPolicy.require_user_action_for_all_media_types

      • AutoMediaPlaybackPolicy.require_user_action_for_all_media_types:需要用户同意后才可以使用媒体播放器。
      • AutoMediaPlaybackPolicy.always_allow:总是允许播放媒体。
    16. allowsInlineMediaPlayback: 控制IOS是否允许在HTML5上嵌入媒体播放器,在Android上默认允许。

    2. WebViewController

    下面看一下WebViewController的伪代码:

    class WebViewController {
    
          Future<void> loadUrl(String url, {Map<String, String>? headers})
    
          Future<String?> currentUrl()
    
          Future<bool> canGoBack()
    
          Future<bool> canGoForward() 
    
          Future<void> goBack()
    
          Future<void> goForward()
    
          Future<void> reload()
    
          Future<void> clearCache()
    
          @Deprecated('Use [runJavascript] or [runJavascriptReturningResult]')
          Future<String> evaluateJavascript(String javascriptString)
    
          Future<void> runJavascript(String javaScriptString)
    
          Future<String> runJavascriptReturningResult(String javaScriptString)
    
          Future<String?> getTitle()
    
          Future<void> scrollTo(int x, int y)
    
          Future<void> scrollBy(int x, int y)
    
          Future<int> getScrollX()
    
          Future<int> getScrollY()
    }
    
    1. loadUrl : 加载新页面

    2. currentUrl : 获取当前URL

    3. canGoBack : 是否可以回退

    4. canGoForward : 是否可以前进

    5. goBack : 回退(如果不可回退,就不执行任何操作)

    6. goForward : 前进(如果不可前进,就不执行任何操作)

    7. reload : 重新加载/刷新

    8. clearCache : 清除缓存

    9. evaluateJavascript : 调用JavaScript方法,已过时,使用runJavascript/runJavascriptReturningResult代替

    10. runJavascript : 无返回值的调用JavaScript方法

    11. runJavascriptReturningResult : 有返回值的调用JavaScript方法

    12. getTitle : 获取HTML标题

    13. scrollTo : 滑动到X、Y位置

    14. scrollBy : 在当前位置上滑动X、Y长度

    15. getScrollX : 获取X轴滑动长度,单位:pixels

    16. getScrollY : 获取Y轴滑动长度,单位:pixels

    3. Cookie

    Cookie目前只支持删除,方法有以下两个:

    1. WebView.platform.clearCookies();

    2. CookieManager().clearCookies();

    四. WebView与JS交互

    1. Flutter调用JS方法

    JS代码如下,分别有一个无返回值和一个有返回值的方法。

    <script>
      function flutterCallJsMethod(message){
        alert(message);
        return "我是JS返回的Result";
      }
    
      function flutterCallJsMethodNoResult(message){
        alert(message);
      }
    </script>
    

    Flutter端调用代码如下:

    ///调用有返回值JS方法,并打印结果
    _controller
        .runJavascriptReturningResult(
            "flutterCallJsMethod('Flutter调用了JS')")
        .then((value) {
      Fluttertoast.showToast(msg: value.toString());
    });
    
    ///调用无返回值JS方法
    _controller
         .runJavascript("flutterCallJsMethodNoResult('Flutter调用了JS')");
    

    evaluateJavascript:方法已经弃用。

    2. JS调用Flutter方法

    Flutter提供了简单的支持JS调用的方法和参数,也可以通过修改Plugin实现自定义方法和参数。

    2.1 默认方法

    Flutter端代码如下

    WebView(
    ...
    javascriptMode: JavascriptMode.unrestricted,
    javascriptChannels: {
        JavascriptChannel(
            name: "toast",
            onMessageReceived: (message) {
            String result = message.message;
            ...
            }),
    },
    ...
    )
    

    javascriptChannels:表示JS可以调用Flutter的对象集合
    name:表示映射的对象名
    onMessageReceived:为JS传过来的参数

    以上代码在Android端的实现为

    webView.addJavascriptInterface(new JsInterface(), "toast");
    ...
    public class JsInterface {
        @JavascriptInterface
        public void postMessage(String message) {
           ...
        }
    }
    

    JavaScript调用代码如下

    <script>
        function jsCallFlutter(){
          toast.postMessage("JS调用Flutter postMessage");
        }
    </script>
    

    name:toast : 这个值是三端公共定义的。

    postMessage : 这个方法是Flutter Plugin内部默认定义好的一个方法。之所以叫这个名字是为了更好的兼容IOS

    IOS WebKit提供了一个默认的name:postMessage参考文档

    The user content controller uses this parameter to define a JavaScript function for your message handler in all frames in the specified content world. 
    The name of this function is window.webkit.messageHandlers.<name>.postMessage(<messageBody>), where <name> corresponds to the value of this parameter. 
    For example, if you specify the string MyFunction, the user content controller defines the window.webkit.messageHandlers.MyFunction.postMessage() function in JavaScript.
    

    2.2 自定义方法和参数

    自定义方法名和参数,需要修改Plugin代码。

    JS端代码如下

    <script>
        function jsCallFlutter2(){
          jscomm.toLocalEvent("JS调用Flutter", "callNative");
        }
    </script>
    

    Flutter端代码如下

    javascriptMode: JavascriptMode.unrestricted,
    javascriptChannels: {
      ...
      JavascriptChannel(
          name: "jscomm",
          onMessageReceived: (message) {
            dynamic result = json.decode(message.message);
            String event = result["event"];
            String data = result["data"];
          }),
    },
    

    以上代码在Android端的实现为

    webView.addJavascriptInterface(new JsInterface(), "jscomm");
    ...
    public class JsInterface {
        @JavascriptInterface
        public void toLocalEvent(String event,String data) {
    
        }
    }
    

    修改Flutter Plugin代码:JavaScriptChannel.java

    //默认实现的方法
    @JavascriptInterface
    public void postMessage(final String message) {
        Runnable postMessageRunnable =
                new Runnable() {
                    @Override
                    public void run() {
                        HashMap<String, String> arguments = new HashMap<>();
                        arguments.put("channel", javaScriptChannelName);
                        arguments.put("message", message);
                        methodChannel.invokeMethod("javascriptChannelMessage", arguments);
                    }
                };
        if (platformThreadHandler.getLooper() == Looper.myLooper()) {
            postMessageRunnable.run();
        } else {
            platformThreadHandler.post(postMessageRunnable);
        }
    }
    
    //新增加的方法
    @JavascriptInterface
    public void toLocalEvent(final String event, final String data) {
        Runnable postMessageRunnable =
                new Runnable() {
                    @Override
                    public void run() {
                        JSONObject jsonObject = new JSONObject();
                        try {
                            jsonObject.put("event", event);
                            jsonObject.put("data", data);
                        } catch (JSONException e) {
                        }
                        HashMap<String, String> arguments = new HashMap<>();
                        arguments.put("channel", javaScriptChannelName);
                        arguments.put("message", jsonObject.toString());
                        methodChannel.invokeMethod("javascriptChannelMessage", arguments);
                    }
                };
        if (platformThreadHandler.getLooper() == Looper.myLooper()) {
            postMessageRunnable.run();
        } else {
            platformThreadHandler.post(postMessageRunnable);
        }
    }
    

    注意:这个新增的toLocalEvent方法,修改的是本地缓存代码,不能提交到Git上,也就是说只有修改的那个人运行的代码有效!

    以上就是这次分享的全部了,切记修改的Plugin代码不会被提交,如果示例代码无法运行,仔细看文档。

    index.html完整源码见GitHub

    Flutter完整源码见GitHub

    相关文章

      网友评论

          本文标题:Flutter:加载本地Html、WebView与JS交互

          本文链接:https://www.haomeiwen.com/subject/ihhmtrtx.html