[Flutter翻译]InAppWebView 5有什么新功能?空安全,新功能,错误修复。

2,149 阅读5分钟

原文地址:medium.com/flutter-com…

原文作者:medium.com/@pichillilo…

发布时间:2021年3月24日 - 8分钟阅读

image.png

Flutter InAppWebView。

终于,经过一番努力,flutter_inappwebview插件新的第5版出来了(写这篇文章的时候,最新的版本是5.2.0)!

那么,有什么新的内容?有什么变化?

嗯...很多

Null-safety支持

InAppWebView 5配备了Dart null-safety支持! 这意味着什么?我们知道,Dart的稳定版还没有发布null-safety功能(需要Dart SDK >=2.12.0-0 <3.0.0),所以,要使用这个插件,你还不能使用Flutter稳定版。 你需要升级Flutter,切换频道,比如,切换到dev频道:flutter频道dev,然后,flutter升级(见切换Flutter频道)。

支持安卓混合构图

使用新的WebView Android专用选项useHybridComposition: true来启用混合合成。 这将大大改善Android上的WebView的性能,并且,还将解决所有与键盘相关的问题。 需要注意的是,这个选项需要Flutter v1.20以上,并且只能用于Android 10以上的发布应用,因为动画在< Android 10上会掉帧(见Hybrid-Composition#performance)。

不再将URL作为一个字符串=更少的问题

所有将URL表示为字符串的类属性现在都已转换为Uri类型。为什么会有这样的变化?在某些情况下,由一个简单的String表示的URL可能会有问题,例如,当你在它里面有空格或特殊字符没有正确编码时。

使用Uri.parse()并将String转换为Uri,将为你解决很多URL的问题。

另外,还有一个新的类URLRequest,它代表了,嗯,一个使用Uri作为url属性类型的URL加载请求。

当你想使用loadUrl方法或在新的WebView属性initialUrlRequest中使用这个类(它取代了旧的initialUrl和initialHeaders属性)。一个简单的使用例子是

child: InAppWebView(
  initialUrlRequest: URLRequest(
      url: Uri.parse("https://flutter.dev/")
  ),
),

此外,使用URLRequest ,你可以进行一个初始的POST请求,比如。

child: InAppWebView(
  initialUrlRequest: URLRequest(
    url: Uri.parse("https://example.com"),
    method: 'POST',
    body: Uint8List.fromList(utf8.encode("name=FooBar")),
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  ),
),

不幸的是,在Android上,POST请求将忽略headers属性,因为没有任何本地的API来加载带有headers的POST请求,如loadUrl 。这是因为本地方法android.webkit.WebView.postUrl是唯一可以发送这种类型请求的方法。

iOS<11.0版本对Cookies的支持有限

由于WKHTTPCookieStore只在iOS≥11.0的情况下可用,所以通过JavaScript对iOS<11.0的情况下增加了有限的支持,所以,你无法获取、创建或更改HttpOnly cookies。另外,对于session-only cookie,你需要使用新的iosBelow11WebViewController参数(如果你想使用的那个Cookie Manager方法可用)。

用户脚本

什么是UserScript?我们可以说UserScript类相当于WKUserScript的iOS原生类。

当我可以使用evaluateJavascript方法来注入我的JavaScript代码时,我为什么要使用它呢?嗯,是的,但是不对!UserScript为你提供了注入JavaScript代码的可能性。UserScript为您提供了在加载其他资源之前注入JavaScript的可能性,例如,将injectionTime属性设置为UserScriptInjectionTime.at_DOCUMENT_START(相当于WKUserScriptInjectionTime.atDocumentStart iOS原生属性)。

不过,我在这里应该为Android做一个精确的规定。我说的在iOS上是可以保证的,但在Android上就不行了(你知道iOS !=Android吗?🤷♂️)!

这是因为Android端不存在相应的原生类/功能,所以InAppWebView会尝试尽快注入所有的用户脚本。你可以把它想象成这样。

要管理UserScripts,你可以使用相应的方法,比如addUserScript、removeUserScript等。

内容世界

从iOS 14.0+开始,WebKit引入了WKContentWorlds的概念,它是一个为JavaScript代码定义执行范围的对象,你用它来防止不同脚本之间的冲突。

这个概念也被引入到flutter_inappwebview插件中,新的,你猜对了,ContentWorld类。

但是,就像我之前说的,iOS !=Android,而且,在Android上,这个概念并不原生存在。所以,就用<iframe>HTML元素的用法来实现。

你可能会问,为什么我不使用类似LiquidCore库或者类似JavaScriptCore iOS框架的东西来实现它。

使用这些库/框架的问题是,您当然不能访问当前网页的窗口或文档 JavaScript 对象。

相反,使用 WKContentWorld,您可以访问这些对象,因此,您可以与网页本身进行交互。

在 Android 上使用 iframes,您可以创建一个新的 JavaScript 上下文,而不与网页的主 JavaScript 上下文冲突(例如,您可以拥有 2 个同名的变量,因为它们存在于 2 个不同的上下文/内容世界中),并实现这种内容世界,如在 iOS 上。所以,这个插件会创建一个id属性等于flutter_inappwebview_[Content World Name HERE]的<iframe>并附加到只包含内联脚本的网页内容上,以便为JavaScript代码定义一个新的执行范围。

显然,这有一些限制/缺点。

  • 对于任何ContentWorld,除了ContentWorld.PAGE(即网页本身),如果你需要访问window或document全局对象,你需要使用window.top和window.top.document,因为代码在iframe内运行。
  • 内联脚本的执行可能会被Content-Security-Policy头所阻止。

一个简单的例子。

child: InAppWebView(
  initialUrlRequest: URLRequest(
    url: Uri.parse("https://flutter.dev"),
  ),
  onLoadStop: (controller, url) async {
    await controller.evaluateJavascript(source: "var foo = 49;");
    await controller.evaluateJavascript(source: "var bar = 19;",
        contentWorld: ContentWorld.PAGE);
    print(await controller.evaluateJavascript(source: "foo + bar;"));

    print(await controller.evaluateJavascript(source: "bar;",
        contentWorld: ContentWorld.DEFAULT_CLIENT));
    await controller.evaluateJavascript(source: "var bar = 2;",
        contentWorld: ContentWorld.DEFAULT_CLIENT);
    print(await controller.evaluateJavascript(source: "bar;",
        contentWorld: ContentWorld.DEFAULT_CLIENT));

    if (Platform.isIOS) {
      await controller.evaluateJavascript(
          source: "document.body.innerHTML = 'LOL';",
          contentWorld: ContentWorld.world(name: "MyWorld"));
    } else {
      await controller.evaluateJavascript(
          source: "window.top.document.body.innerHTML = 'LOL';",
          contentWorld: ContentWorld.world(name: "MyWorld"));
    }
  },
  onConsoleMessage: (controller, consoleMessage) {
    print(consoleMessage);
  },
),

这个例子的证明就留给读者吧。

苹果支付API

iOS 13.0+新增了一个WebView选项,以启用JavaScript Apple Pay API:applePayAPIEnabled。正如Safari 13官方发布说明中写的那样,如果使用了任何脚本注入API(如evaluJavascript或UserScript),它将无法工作。

所以,当这个属性为真时,iOS上所有使用JavaScript实现的方法、选项和事件都不会被调用或不会做任何事情,结果永远是空的,但是,嘿,你会从你的用户那里得到报酬🤑(这很好,不是吗?

查看flutter_inappwebview官方文档,了解受此影响的API的完整列表!

评估Async JavaScript代码

从iOS 14.0+开始,WebKit增加了新的callAsyncJavaScript方法,该方法允许将源代码作为异步JavaScript函数执行(参见MDN - async函数)。这个方法已经在Android和iOS平台上实现。

在iOS上,你可以从iOS 10.3+开始使用这个函数,因为正如这里所说:async函数 浏览器兼容性,从该版本开始应该已经支持async函数。因此,在iOS版本的[10.3, 14.0)范围内,使用evaluateJavascript方法,并使用包含标识符和回调的Map来实现,回调将在异步函数执行结束时调用返回的结果。在Android上,它也是以同样的方式实现的!

返回类型与evaluateJavascript方法不同,而是一个CallAsyncJavaScriptResult实例,其中value属性包含成功值(如果有的话),error属性包含一个代表失败值(如果有的话)的String。

下面是一个例子。

child: InAppWebView(
  initialUrlRequest: URLRequest(
    url: Uri.parse("https://flutter.dev"),
  ),
  onLoadStop: (controller, url) async {
    final String functionBody = """
        var p = new Promise(function (resolve, reject) {
           window.setTimeout(function() {
             if (x >= 0) {
               resolve(x);
             } else {
               reject(y);
             }
           }, 1000);
        });
        await p;
        return p;
      """;

    var result = await controller.callAsyncJavaScript(
        functionBody: functionBody,
        arguments: {'x': 49, 'y': 'error message'});
    print(result);

    result = await controller.callAsyncJavaScript(
        functionBody: functionBody,
        arguments: {'x': -49, 'y': 'error message'});
    print(result);
  },
),

这将打印{value.49, error: null}和{value: null, error: error: null}。49, error: null}和{value: null, error: "错误信息"}。

服务工作者API

在Android上,AndroidServiceWorkerController和AndroidServiceWorkerClient类可以用来拦截请求。在使用这些类或它们的方法之前,你应该检查你要使用的服务工作者功能是否被支持,例如。

Future main() async {
  WidgetsFlutterBinding.ensureInitialized();
  if (Platform.isAndroid) {
    await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true);
    var swAvailable = await AndroidWebViewFeature.isFeatureSupported(AndroidWebViewFeature.SERVICE_WORKER_BASIC_USAGE);
    var swInterceptAvailable = await AndroidWebViewFeature.isFeatureSupported(AndroidWebViewFeature.SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST);
    if (swAvailable && swInterceptAvailable) {
      AndroidServiceWorkerController serviceWorkerController = AndroidServiceWorkerController.instance();
      serviceWorkerController.serviceWorkerClient = AndroidServiceWorkerClient(
        shouldInterceptRequest: (request) async {
          print(request);
          return null;
        },
      );
    }
  }
  runApp(MyApp());
}

相反,在iOS上,从iOS 14.0+开始,JavaScript Service Worker API是可用的。

要在iOS上启用这个JavaScript API,只有2种方法。

  • 使用 "App-Bound Domains"
  • 您的应用程序建议自己作为一个可能的 "默认浏览器",如iOS Safari或Google Chrome。

App-Bound Domains:请阅读 WebKit - App-Bound Domains 一文了解详情。你可以使用新的Info.plist键WKAppBoundDomains来指定最多10个 "应用绑定 "域,例如。

<dict>
<key>WKAppBoundDomains</key>
<array>
    <string>flutter.dev</string>
    <string>github.com</string>
</array>
</dict>

之后,你需要将limitsNavigationsToAppBoundDomains iOS特定的WebView选项设置为true,例如。

InAppWebViewGroupOptions(
  ios: IOSInAppWebViewOptions(
    limitsNavigationsToAppBoundDomains: true
  )
)

iOS默认浏览器:请阅读准备你的应用程序成为默认浏览器或电子邮件客户端的文章,了解详情。

网络消息通道和网络消息监听器

除了JavaScript处理程序之外,还有两种新的方式与JavaScript进行通信。

  • Web消息通道:它们是HTML5消息通道的代表。更多细节请参见 Channel Messaging API。要创建Web消息通道,你需要使用InAppWebViewController.createWebMessageChannel方法。该方法应在页面加载时调用,例如,当WebView.onLoadStop事件被触发时。

  • Web消息监听器。它允许在WebMessageListener监听的每个框架中注入一个JavaScript对象。要添加一个Web消息监听器,你需要使用InAppWebViewController.addWebMessageListener方法。这个方法应该在使用它的网页被加载之前被调用,例如当WebView.onWebViewCreated事件被触发时。

官方网站

这个插件的官方网站已经发布在inappwebview.dev上了!

所有在repository的README.md上写的初始设置和配置都将被移到那里。我将添加一个入门部分和其他文档来帮助你开始使用它!

另外,它还包含一个展示部分,有一个用Flutter和Flutter InAppWebView构建的应用程序的开放列表。目前,因为网站是新的,所以只有一个App,那就是Flutter浏览器App

你是否在使用这个插件?通过提交App页面提交你的App,并按照说明操作吧!

结束语

查看CHANGELOG.md文件,查看更改和修复的完整列表!

今天就到这里吧!

我要感谢所有以任何方式支持这个项目的人! 非常感谢大家的支持! 💙


通过www.DeepL.com/Translator(免费版)翻译