突然插播:Flutter與JS互調

語言: CN / TW / HK

這是我參與更文挑戰的第9天,活動詳情檢視: 更文挑戰

所以要一鍋端了?

很順利的解決了iOS端,Swift與H5的互動問題,公司覺得你這光解決iOS的不行啊,最好是寫一個通用的WebView殼子,能滿足日常的iOS與Android雙端WebView載入H5除錯。

這不是要一鍋端的節奏?

既然是雙端除錯,那麼必然會想到使用跨平臺方案來寫這個殼子,基於技術背景,Flutter當仁不讓的成為了第一選擇,而選擇使用的Flutter中的WebView外掛就是webview_flutter!

別說,我為了這個突然的需求,也反反覆覆看了webview_flutter的官方文件和程式碼,自己寫了Demo來驗證。

簡單介紹一下webview_flutter外掛


A Flutter plugin that provides a WebView widget.

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

一個為Flutter提供WebView元件的外掛。

在iOS端是基於原生的WKWebView,而在Android則是基於系統的WebView。


從這段簡介可以看出,雖然這是一個Flutter外掛,但是實際上橋接的都是基於iOS與Android平臺系統層面的WebView元件,這樣做的好處就是在每一端都做到使用原生,只要中間通訊層邏輯一致並完好,那麼就可以展平了雙端WebView中H5與Flutter通訊的能力。

Flutter與JS互調使用與講解

在這裡我們先精簡一下官方給出的example例子。

``` import 'dart:async'; import 'dart:convert'; import 'dart:io';

import 'package:flutter/material.dart';

import 'package:webview_flutter/webview_flutter.dart';

class WebViewExample extends StatefulWidget { @override _WebViewExampleState createState() => _WebViewExampleState(); }

class _WebViewExampleState extends State { final Completer _controller = Completer();

@override void initState() { super.initState(); if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView(); }

@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Flutter WebView example'), ), // We're using a Builder here so we have a context that is below the Scaffold // to allow calling Scaffold.of(context) so we can show a snackbar. body: Builder(builder: (BuildContext context) { return WebView( initialUrl: 'http://www.baidu.com', javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController webViewController) { _controller.complete(webViewController); }, onProgress: (int progress) { print("WebView is loading (progress : $progress%)"); }, javascriptChannels: { _toasterJavascriptChannel(context), }, navigationDelegate: (NavigationRequest request) { if (request.url.startsWith('http://www.youtube.com/')) { print('blocking navigation to $request}'); return NavigationDecision.prevent; } print('allowing navigation to $request'); return NavigationDecision.navigate; }, onPageStarted: (String url) { print('Page started loading: $url'); }, onPageFinished: (String url) async { final webViewController = await _controller.future; final callback = await webViewController.evaluateJavascript("alert('Hello world');"); print(callback); }, gestureNavigationEnabled: true, ); }), ); }

JavascriptChannel _toasterJavascriptChannel(BuildContext context) { return JavascriptChannel( name: 'SeasonCallback', onMessageReceived: (JavascriptMessage message) { // ignore: deprecated_member_use Scaffold.of(context).showSnackBar( SnackBar(content: Text(message.message)), ); }); } } ```

這段程式碼最為核心的部分就是以下這段,請詳細看看我寫的註釋,詳細又重要!!!

如果在網頁裡面看不懂的,可以考慮下載webview_flutter的程式碼,一點點對照的看,並與前兩篇我文章。

return WebView( /// 需要載入的網頁URL initialUrl: 'http://www.baidu.com', /// JS的模式 javascriptMode: JavascriptMode.unrestricted, /// WebView生成成功時,會回撥一個webViewController,它非常的重要,後面會繼續說 onWebViewCreated: (WebViewController webViewController) { /// 接受該控制器 _controller.complete(webViewController); }, /// webView載入的回撥進度,相當於Swift中對WKWebView中的estimatedProgress的KVO onProgress: (int progress) { print("WebView is loading (progress : $progress%)"); }, /// JS通道,這裡相當於Flutter側監聽JS方法的集合,和Swift中通過userContentController註冊監聽JS控制代碼一致 javascriptChannels: <JavascriptChannel>{ /// 這個函式後面單獨說明 _toasterJavascriptChannel(context), }, /// 這裡相當於Swift中WKNavigationDelegate代理方法中的 /// func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)方法, /// 就是用於攔截跳轉的,和攔截微信和支付寶跳轉類似 navigationDelegate: (NavigationRequest request) { /// 這裡做了跳轉攔截,如果跳轉的地址是油管,那麼將阻止載入 if (request.url.startsWith('http://www.youtube.com/')) { print('blocking navigation to $request}'); return NavigationDecision.prevent; } print('allowing navigation to $request'); /// 其他的跳轉允許 return NavigationDecision.navigate; }, /// 監聽網頁開始載入,相當於Swift中WKNavigationDelegate代理方法中的 /// func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!)方法 onPageStarted: (String url) { print('Page started loading: $url'); }, /// 監聽網頁載入完畢,相當於Swift中WKNavigationDelegate代理方法中的 /// func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)方法 onPageFinished: (String url) async { /// 通過Completer拿webView控制器 final webViewController = await _controller.future; /// 通過webViewController執行JS方法,這裡是呼叫一個JS的彈窗,並且非同步獲取JS的回撥,這個回撥可能為空 final callback = await webViewController.evaluateJavascript("alert('Hello world');"); print(callback); }, /// 是否支援手勢側滑 gestureNavigationEnabled: true, );

可以看到,如果理解了Swift中的WKWebView的WKNavigationDelegate代理方法含義,Flutter中的WebView無非是換了些名稱,將delegate風格換成了callback風格而已的初始化函式。

_toasterJavascriptChannel就是具體化的如果註冊監聽JS方法控制代碼:

JavascriptChannel _toasterJavascriptChannel(BuildContext context) { return JavascriptChannel( /// 註冊一個監聽控制代碼為SeasonCallback /// 和Swift中func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String)方法一致 name: 'SeasonCallback', /// 監聽的SeasonCallback回撥,獲取JS傳到Flutter側的引數,和Swift中的WKScriptMessageHandler代理回撥一致, /// func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) /// 然後JS就可以呼叫Flutter側的函數了 onMessageReceived: (JavascriptMessage message) { /// Flutter接受到JS的message,並調起Flutter函式 Scaffold.of(context).showSnackBar( SnackBar(content: Text(message.message)), ); }); } 回憶一下上一篇文章中的3個需求,如果在Flutter需要實現,應該怎麼寫呢?理解了上一篇文章並看懂了這篇文章的註釋,應該不在話下。

總結

  • Flutter中的webview_flutter本質上還是呼叫了雙端的原生WebView元件,所以對應的一致性較好,如果瞭解原生與JS的互調,那麼Flutter與JS互調依葫蘆畫瓢即可。

  • Swift中的多個代理回撥在Flutter中都通過Callback完成,這樣看起來緊湊,雖然可能在編碼中不易於看清楚,但是比較符合Flutter的宣告式程式設計風格,需要努力適應。

webview_flutter的使用注意事項與坑點:

注意事項

iOS端需要在info.plist檔案中新增配置與許可權: <key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict> <key>io.flutter.embedded_views_preview</key> <true/>

Android端:

  • 1.在路徑android/app/build.gradle新增最小SDK支援: android { defaultConfig { minSdkVersion 19 } }

  • 2.安卓端開啟混合檢視 ``` import 'dart:io';

import 'package:webview_flutter/webview_flutter.dart';

class WebViewExample extends StatefulWidget { @override WebViewExampleState createState() => WebViewExampleState(); }

class WebViewExampleState extends State { @override void initState() { super.initState(); /// 就是下面這段程式碼 // Enable hybrid composition. if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView(); }

@override Widget build(BuildContext context) { return WebView( initialUrl: 'http://flutter.cn', ); } } ```

  • 3.開啟http選項和隱私許可權,注意是在專案/android/app/src/main/AndroidManifest.xml檔案中進行新增 ```


```

坑點

如果你使用webview_flutter在Android端載入H5,而H5中正好需要呼叫系統相機或者是相簿的話,這個JS是無法得到相應的,而iOS端卻可以。

android webview 裡點選<input type=’file’>沒有反應。

出現這個問題的原因是,官方的webview_flutter外掛沒有針對這一功能做適配。

詳細可以看flutter webview android h5 上傳檔案失敗解決辦法

如果需要Android端有這個能力,需要在新增許可權的同時,改寫外掛在Android側的實現。

當然有另外一個外掛flutter_webview_plugin可以實現Android上傳這個功能,但是這個flutter_webview_plugin不管從易用性還是功能性,都不及webview_flutter。

我好希望有個一個大佬給教我改寫webview_flutter外掛,支援Android端的上傳功能,我按照網上的方法改寫了,然後崩潰了。。。

明天繼續

明天會講解Swift中,準確是OC中WebViewJavaScriptBridge的使用與注意實現,大家加油!