前言
開發過程中我們會遇到App巢狀網頁的使用場景,我們在App中跳轉到H5網頁有時候網頁中會有跳轉到App的需求,也就是App與網頁的相互通訊。下面我們來慢慢的實現這個功能。
準備工作
安裝Flutter外掛webview_flutter
pubspec.yaml新增依賴
開發使用的Flutter版本是2.2.3,dart版本是2.13.x,安裝
webview_flutter: ^2.0.10
最低dart版本>=2.12.x
,建議使用新的版本,之前有遇到安卓手機不能彈起輸入鍵盤、記憶體洩漏、文字不能複製等一系列問題,2.0.10
這個版本沒有遇見類似問題。
dependencies:
...
webview_flutter: ^2.0.10
..
複製程式碼
使用webview_flutter
外掛
import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_nxj_c/helpers/colors.dart';
import 'package:flutter_nxj_c/helpers/config.dart';
import 'package:flutter_nxj_c/stores/hybrid_h5.dart';
import 'package:webview_flutter/webview_flutter.dart';
class HybridH5 extends StatefulWidget {
static const String routeName = '/hybrid_h5';
const HybridH5();
@override
_HybridH5State createState() => _HybridH5State();
}
class _HybridH5State extends State<HybridH5> {
final Completer<WebViewController> _controller = Completer<WebViewController>();
final _hybridH5Store = HybridH5Store();
late WebViewController _webViewController;
String title = '載入中...';
@override
void initState() {
super.initState();
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
Future(() async {
// 請求引數引數
var params = ModalRoute.of(context)!.settings.arguments;
debugPrint('WebView引數-----$params');
// Modal.loading(duration: Duration.zero);
await _initController();
});
}
Future<void> _initController() async {
await _controller.future.then((controller) {
_webViewController = controller;
_webViewController.loadUrl('https://juejin.cn/user/1046390798028072');
});
}
@override
Widget build(BuildContext context) => WillPopScope(
onWillPop: () async {
var readyController = await _controller.future;
if (await readyController.canGoBack()) {
await readyController.goBack();
return Future.value(false);
}
return Future.value(true);
},
child: CupertinoPageScaffold(
backgroundColor: ColorTheme.of(context).colorF3F3F6,
navigationBar: CupertinoNavigationBar(
backgroundColor: ColorTheme.of(context).colorFFFFFF,
padding: EdgeInsetsDirectional.zero,
border: Border.all(color: ColorTheme.of(context).borderColor),
transitionBetweenRoutes: Platform.isIOS,
middle: Text('$title'),
leading: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
var readyController = await _controller.future;
if (await readyController.canGoBack()) {
await readyController.goBack();
return;
}
Navigator.pop(context, '資料傳參');
},
child: Container(
width: 42.0,
padding: const EdgeInsets.only(left: 10.0, right: 20.0),
child: Image.asset(
'assets/icons/ic_arrow_left_gray.png',
color: ColorTheme.of(context).color202326,
),
),
),
),
child: WebView(
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (webViewController) {
_controller.complete(webViewController);
},
onProgress: (progress) {
print('WebView is loading (progress : $progress%)');
},
javascriptChannels: <JavascriptChannel>{
_toasterJavascriptChannel(context),
},
navigationDelegate: (request) => _hybridH5Store.listenNavigationDelegate(request),
onPageStarted: (url) {
print('Page started loading: $url');
},
onPageFinished: (url) async {
debugPrint('Page finished loading: $url');
await _webViewController.evaluateJavascript('document.title').then((result) {
debugPrint('標題--: $result');
if (result.replaceAll('"', '').isNotEmpty) {
setState(() => title = result.replaceAll('"', ''));
}
});
},
gestureNavigationEnabled: true,
),
),
);
JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
return JavascriptChannel(
name: 'Toaster',
onMessageReceived: (message) {
// ignore: deprecated_member_use
Scaffold.of(context).showSnackBar(
SnackBar(content: Text(message.message)),
);
});
}
}
複製程式碼
初始化載入地址,設定headers
使用_webViewController.loadUrl
方法載入網址,這個地方我們可以headers
,有token需求的同學們就可以直接將App內的token放到瀏覽器中,我們也可以訪問cookie
Future<void> _initController() async {
await _controller.future.then((controller) {
_webViewController = controller;
_webViewController.loadUrl('https://juejin.cn/user/1046390798028072',headers:{});
});
}
複製程式碼
Webview
元件構建
當webview
生成成功以後呼叫_controller.complete(webViewController)
將我們需要訪問的網址放入,瀏覽器就會訪問指定的網頁
WebView(
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (webViewController) {
_controller.complete(webViewController);
},
onProgress: (progress) {
print('WebView is loading (progress : $progress%)');
},
javascriptChannels: <JavascriptChannel>{
_toasterJavascriptChannel(context),
},
navigationDelegate: (request) => _hybridH5Store.listenNavigationDelegate(request),
onPageStarted: (url) {
print('Page started loading: $url');
},
onPageFinished: (url) async {
debugPrint('Page finished loading: $url');
await _webViewController.evaluateJavascript('document.title').then((result) {
debugPrint('標題--: $result');
if (result.replaceAll('"', '').isNotEmpty) {
setState(() => title = result.replaceAll('"', ''));
}
});
},
gestureNavigationEnabled: true,
)
複製程式碼
獲取瀏覽器訪問的頁面title
在瀏覽器訪問網頁的過程中我們會修改標題,達到不同頁面顯示不同標題的功能。
呼叫_webViewController.evaluateJavascript('document.title')
方法獲取網頁標題。
注意:這是一個非同步方法。
onPageFinished: (url) async {
debugPrint('Page finished loading: $url');
await _webViewController.evaluateJavascript('document.title').then((result) {
debugPrint('標題--: $result');
if (result.replaceAll('"', '').isNotEmpty) {
setState(() => title = result.replaceAll('"', ''));
}
});
}
複製程式碼
瀏覽器網頁與App的相互通訊
App如何接收到網頁的方法
WebView
增加javascriptChannels
,javascriptChannels
是javaScript
的管道可以包含很多個自己定義的方法。
javascriptChannels: <JavascriptChannel>{
// 彈出App內的提示框
_toasterJavascriptChannel(context),
// 儲存圖片到相簿
_fileDownLoaderChannel(),
// 新增自定義的方法處理網頁
...
},
複製程式碼
_toasterJavascriptChannel
JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
return JavascriptChannel(
name: 'Toaster',
onMessageReceived: (message) {
// ignore: deprecated_member_use
Scaffold.of(context).showSnackBar(
SnackBar(content: Text(message.message)),
);
});
}
複製程式碼
_fileDownLoaderChannel
JavascriptChannel _fileDownLoaderChannel() {
return JavascriptChannel(
name: 'fileDownLoader',
onMessageReceived: (message) async {
// 跳轉到指定頁面
try {
final data = json.decode(message.message) as Map<String, dynamic>;
var response = await api.Interceptor.dio
.get<Options>("${data['url']}", options: Options(responseType: ResponseType.bytes));
await ImageGallerySaver.saveImage(Uint8List.fromList(response.data as List<int>));
Toast.info(msg: '儲存圖片成功', showTime: 5000);
} catch (err) {
Toast.info(msg: '儲存圖片失敗', showTime: 5000);
}
});
}
複製程式碼
網頁通知App彈出提示框、儲存圖片到相簿
// 儲存圖片到App
window.fileDownLoader.postMessage(JSON.stringify({ url: detailData.codeUrl }));
// 使用App的提示
window.Toaster.postMessage(JSON.stringify({ message: '提示資訊' }));
複製程式碼
JavascriptChannel
的name
就是window
方法需要通訊的方法名,onMessageReceived
方法的引數就是postMessage
傳送的引數。
網頁不能開啟三方應用問題
網頁中會有開啟第三方應用、撥打電話的功能,在我們不對WebView
增加方法的時候點選就會無效。話不多說現在開始解決這個問題。
WebView
中增加navigationDelegate
這個方法可以監聽到導航地址的變化。
navigationDelegate: (NavigationRequest request) {
debugPrint('request.url ${request.url}');
// 檢查支付寶
if (request.url.contains('alipays://')) {
_openUrl(request.url);
return NavigationDecision.prevent;
}
// 路由攔截-單頁應用路由檢測有問題
if (request.url.contains('https://3gimg.qq.com/') ||
request.url.contains('https://apis.map.qq.com')) {
debugPrint('跳轉地圖-路由被攔截');
return NavigationDecision.prevent;
}
if (request.url.contains('tel:')) {
// 撥打電話
_openUrl(request.url);
return NavigationDecision.prevent;
}
debugPrint('allowing navigation to ${request.url}');
return NavigationDecision.navigate;
}
複製程式碼
_openUrl方法
使用url_launcher
外掛的launch
方法可以開啟App或者瀏覽器,開啟App使用的是schema
。
Future<void> _openUrl(String linkUrl) async {
await launch(linkUrl);
}
複製程式碼
結語
目前就是我使用Webview
遇到的問題,大家遇到有其它問題歡迎留言討論。