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

本次教程使用的是Flutter官方提供的WebView组件webview_flutter 2.3.1,flutter_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*)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*)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?,
   navigationDelegate : NavigationDelegate?,
   gestureRecognizers : Set>?,
   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 loadUrl(String url, {Map? headers})

      Future currentUrl()

      Future canGoBack()

      Future canGoForward() 

      Future goBack()

      Future goForward()

      Future reload()

      Future clearCache()

      @Deprecated('Use [runJavascript] or [runJavascriptReturningResult]')
      Future evaluateJavascript(String javascriptString)

      Future runJavascript(String javaScriptString)

      Future runJavascriptReturningResult(String javaScriptString)

      Future getTitle()

      Future scrollTo(int x, int y)

      Future scrollBy(int x, int y)

      Future getScrollX()

      Future 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代码如下,分别有一个无返回值和一个有返回值的方法。


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调用代码如下


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..postMessage(), where  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端代码如下


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 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 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

你可能感兴趣的:(android,屏幕适配,flutter)