lzyprime 部落格 (github)
建立時間:2020.03.06
qq及郵箱:2383518170
λ:
-
git clone -b flutter_webview_demo https://github.com/lzyprime/flutter_demos.git 複製程式碼
-
外掛: webview_flutter , 目前版本:^0.3.19+9
-
由於要兼顧 Android 和 Ios 兩個平臺的webView, js 呼叫native時無法直接返回資料,所以通過回撥的形式曲線救國: 接到js請求後,處理資料,然後主動呼叫js相關函式。
-
flutter 呼叫js 可以監聽返回值
主要引數和方法
官方的example或者跳到原始碼一看便知
// 建構函式
const WebView({
Key key,
this.onWebViewCreated, //webView建立完成後的回撥函式, WebViewCreatedCallback(WebViewController controller),會返回 webViewController
this.initialUrl, //要載入的url地址
this.javascriptMode = JavascriptMode.disabled, //js是否執行,預設值為不執行, JavascriptMode.unrestricted執行。不能為空
this.javascriptChannels, // js 呼叫 flutter 時的處理者們,set<JavascriptChannel>。 所有JavascriptChannel的name不允許重複
this.navigationDelegate, // 攔截請求並處理,詳情請檢視原始碼
this.gestureRecognizers, // 手勢監聽與處理,詳情請檢視原始碼
this.onPageStarted, // 開始載入時的回撥,PageStartedCallback(String url)
this.onPageFinished, // 載入結束的回撥,PageFinishedCallback(String url)
/// 剩餘引數英文直譯即可,詳情請檢視原始碼
this.debuggingEnabled = false,
this.gestureNavigationEnabled = false,
this.userAgent,
this.initialMediaPlaybackPolicy =
AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
}) : assert(javascriptMode != null),
assert(initialMediaPlaybackPolicy != null),
super(key: key);
複製程式碼
webViewController:
onWebViewCreated
會返回 當前webView的controller, 官方給的example裡的做法是定義一個Completer<WebViewController>
實現延遲初始化:
class _WebViewPageState extends State<WebViewPage> {
final _controller = Completer<WebViewController>();
....
WebView(
....
onWebViewCreated: (controller) {
_controller.complete(controller);
},
)
....
}
複製程式碼
之後對WebViewController
的呼叫通過 _controller.future
來實現,型別是Future<WebViewController>
, 所以全都是非同步呼叫:
_controller.future.then((controller){
...
})
// 或
FutureBuilder(
future: _controller.future,
builder: (BuildContext context,AsyncSnapshot<WebViewController> controller){
return (Widget)
}
)
複製程式碼
主要函式:
// loadUrl,currentUrl,canGoBack ... 等函式
// 檢視原始碼,看函式名和註釋便知功能
///其中,js互操作常用:
Future<String> evaluateJavascript("js 程式碼") // 執行js, 並且可以接收 js 執行的返回值
複製程式碼
JavascriptChannel
JavascriptChannel({
@required this.name, // js 呼叫時的變數名,
// 如name="Print", js可以通過 Print.postMessage(msg) 呼叫flutter
// 請求會在 onMessageReceived 函式中處理
@required this.onMessageReceived, // 處理js 請求
// typedef void JavascriptMessageHandler(JavascriptMessage message);
// message.message 即 js 呼叫時傳遞的msg
// 函式沒有返回值
}) : assert(name != null),
assert(onMessageReceived != null),
assert(_validChannelNames.hasMatch(name));
複製程式碼
封裝互操作
Flutter 呼叫 js 是可以接收返回值的, js 寫好函式,flutter 調就是了。
js 呼叫 flutter 無返回值, 所以要做一點簡單封裝
js 請求包與相應包格式:
// js 請求:
{
guid: String, // 用於校驗請求一致性,flutter 原封不動傳回
api: String, // 要請求的介面名, flutter 原封不動傳回
data: Object,
...
...
}
// flutter 回包:
{
guid: String, // 用於校驗請求一致性,由js傳入, flutter 原封不動傳回
api: String, // 要請求的介面名, 由js傳入, flutter 原封不動傳回
data: Object,
...
...
}
複製程式碼
flutter
// JavascriptChannel 以介面的形式實現,目的是將一組api封裝在一起
// 當然也可以直接構造
class NativeBridge implements JavascriptChannel {
BuildContext context; //來源於當前widget, 便於操作UI
Future<WebViewController> _controller; //當前webView 的 controller
NativeBridge(this.context, this._controller);
// api 與具體函式的對映表,可通過 _functions[key](data) 呼叫函式
// 如 _functions["getValue"](null)
get _functions => <String, Function>{
"getValue": _getValue,
"inputText": _inputText,
"showSnackBar": _showSnackBar,
"newWebView": _newWebView,
};
@override
String get name => "nativeBridge"; // js 通過 nativeBridge.postMessage(msg); 呼叫flutter
// 處理js請求
@override
get onMessageReceived => (msg) async {
// 將收到的string資料轉為json
Map<String, dynamic> message = json.decode(msg.message);
// 非同步是因為有些api函式實現可能為非同步,如inputText,等待UI相應
// 根據 api 欄位,呼叫具體函式
final data = await _functions[message["api"]](message["data"]);
// 組織回包
final res = <String, dynamic>{
"guid": message["guid"],
"api": message["api"],
"data": data
}
// 執行js函式,window.jsBridge.receiveMessage
// 將資料轉為字串傳遞
_controller.then((v) => v.evaluateJavascript(
"window.jsBridge.receiveMessage(${json.encode(res)})"));
};
Map<String, dynamic> _getValue(data) => {"value": 1};
Future<Map<String, dynamic>> _inputText(data) async {
String text = await showDialog(
context: context,
builder: (_) {
final textController = TextEditingController();
return AlertDialog(
content: TextField(controller: textController),
actions: <Widget>[
FlatButton(
onPressed: () => Navigator.pop(context, textController.text),
child: Icon(Icons.done)),
],
);
});
return {"text": text ?? ""};
}
Map<String, dynamic> _showSnackBar(data) {
Scaffold.of(context)
.showSnackBar(SnackBar(content: Text(data["text"] ?? "")));
return null;
}
Map<String, dynamic> _newWebView(data) {
Navigator.of(context)
.push(MaterialPageRoute(builder: (_) => WebViewPage(url: data["url"])));
return null;
}
}
複製程式碼
加入webView中:
class _WebViewPageState extends State<WebViewPage> {
final _controller = Completer<WebViewController>();
...
@override
Widget build(BuildContext context) {
...
WebView(
...
javascriptChannels: [NativeBridge(context, _controller.future)].toSet(),
)
}
}
複製程式碼
js
我不會寫js, 也省略了guid生成的部分。大概原理如下:
如果 Js 呼叫 flutter 需要返回值,則準備一個處理資料的回撥函式,flutter 處理完資料後,主動呼叫回撥函式。
為了方便flutter 呼叫,封裝了 window.jsBridge.receiveMessage(msg), 該函式根據 guid和api欄位分發到具體的回撥函式。
<!DOCTYPE html>
<html>
<body>
<p id="getValue"></p>
<hr />
<p id="inputText"></p>
<script>
var callbacks = {};
window.jsBridge = {
// js 呼叫 flutter, 將回撥函式儲存到callbacks裡
invoke: function (api, data, callback) {
callbacks[api] = callback;
nativeBridge.postMessage(JSON.stringify({
api: api,
data: data || {},
}));
},
// flutter 處理完資料後呼叫, 根據guid和api,在callbacks裡取出回撥繼續執行
receiveMessage: function (msg) {
if (callbacks[msg.api]) {
callbacks[msg.api](msg); // 執行呼叫
}
}
};
window.jsBridge.invoke("getValue", {}, function (data) {
document.getElementById("getValue").innerHTML = JSON.stringify(data);
});
window.jsBridge.invoke("inputText", {}, function (data) {
document.getElementById("inputText").innerHTML = JSON.stringify(data);
});
window.jsBridge.invoke("showSnackBar", { text: "snackBar should show" }, null);
window.jsBridge.invoke("newWebView", { url: "https://lzyprime.github.io" }, null);
</script>
</body>
</html>
複製程式碼
~λ:
-
再次強調,我不會js,而且這只是簡單的demo, 主要為了說明原理, 實際應用時肯定會更嚴格。但是方式還是類似:js 呼叫 flutter 並儲存回撥, flutter 處理完成後主動呼叫回撥, 從而實現返回值
-
完整程式碼在倉庫裡,為了方便只有一個檔案