在 Flutter 中使用 WebView 创建一个功能齐全的浏览器

在本文中,我将展示如何使用插件提供的功能创建功能齐全的移动浏览器应用程序,例如 Google Chrome 移动浏览器。flutter_inappwebview

查看之前介绍 flutter_inappwebview 插件的文章: InAppWebView:Flutter 中 WebView 的真正力量

这就是我们要实现的:

  • WebView 选项卡,自定义长按链接/图像预览,以及如何在不丢失 WebView 状态的情况下从一个选项卡移动到另一个选项卡;
  • 带有当前 URL 和所有弹出菜单操作的浏览器应用程序栏,例如打开 选项卡、新的隐身选项卡 、将当前 URL 保存到收藏夹列表 、保存页面以供离线使用 、查看网站使用的 SSL 证书 、启用桌面模式 等(功能类似于 Google Chrome 应用程序);
  • 开发者控制台 ,您可以在其中执行 JavaScript 代码 、查看一些网络信息 、管理浏览器存储(如 cookie ****等);window.localStorage
  • 设置页面,您可以在其中更新浏览器常规设置启用/禁用每个 WebView 选项卡提供的所有功能,例如启用/禁用 JavaScript、缓存、滚动条、设置自定义用户代理等,以及所有特定于 Android 和 iOS 的功能;flutter_inappwebview
  • 保存还原当前浏览器状态。

相反,关于应用程序状态管理,我们将使用包。provider

用于实现此浏览器应用程序的两个主要模型是 和 ,其中包含浏览器的信息和数据,例如所有 WebView 选项卡的列表和显示的当前 WebView 选项卡。它们还将用于保存和恢复浏览器状态。为了保存和恢复这些模型,我们将使用该包。BrowserModel``WebViewModel``shared_preferences

由于代码可能很长,因此我将只显示代码的一小部分或仅显示伪代码。但是,完整的代码 可在 Github 上获得****,网址为 github.com/pichillilor...

此外,此应用程序可在 Google Play 商店获得 ,网址为 play.google.com/store/apps/...

这是我们将得到的最终结果

flutter_browser_inappwebview.mp4

Flutter 浏览器应用最终结果

drive.google.com

"WebView"选项卡

我们要实现的第一件事是 WebView 选项卡。 考虑到我们需要维护每个 WebView 选项卡的状态以及从一个选项卡移动到另一个选项卡的能力,我们可以使用小部件,其中属性是我们要显示的当前 WebView 选项卡,并且是浏览器中打开的所有 WebView 选项卡的列表。IndexedStack``index``children

我们还想显示一个进度条,指示 WebView 的当前加载进度,因此我们使用 widget,其中第一个子项是之前创建的小部件,第二个子项是 .我们可以将其想象成这样:Stack``IndexedStack``LinearProgressIndicator

php 复制代码
var stackChildren = <Widget>[
  IndexedStack(
    index: browserModel.getCurrentTabIndex(),
    children: browserModel.webViewTabs,
  ),
  LinearProgressIndicator()
];

return Stack(
  children: stackChildren,
);

每个 WebView 选项卡都将是我们自定义小部件的一个实例,该小部件包含实例(真正的 WeView)和控制它。每个 WebView 选项卡都将具有可与其他浏览器选项卡区分开来的实例,以及包含必要数据(例如当前 URL 及其 WebView 设置)的实例,以便在用户完全关闭和重新打开应用程序时能够恢复它们。WebViewTab``StatefulWidget``InAppWebView``InAppWebViewController``GlobalKey``WebViewModel

对于每个 WebView,我们想要保存的最重要的数据是当前 URL(也包括更新时!)、页面标题(是否安全)、网站图标、选项卡索引和设置。

为了跟踪 URL 更改,我们使用 和 事件。为了监听网站进度变化,我们使用事件。当网站被加载时,我们用来获取网站标题、它的图标,并根据HTTPS协议 、SSL证书(X509Certificate )的存在以及任何SSL错误 来检查它是否安全****。onLoadStart``onLoadStop``onUpdateVisitedHistory``onProgressChanged``onLoadStop

此外,我们还想实现我们的自定义链接/图像预览。为此,我们使用事件,该事件检测 WebView 中用户的长按事件。它返回一个包含有关用户单击的内容的有用信息,例如,它是图像还是链接。我们检查并显示我们的自定义:onLongPressHitTestResult``hitTestResult``AlertDialog

在左侧,我们有一个长按链接预览。相反,在右侧,我们检测到它是一个图像,因此我们显示了相应的警报对话框。

此外,我们希望在应用程序暂停/恢复时暂停/恢复 WebView,并在从一个 WebView 选项卡移动到另一个 WebView 选项卡时暂停/恢复 JavaScript 执行。

要在 Android 上暂停/恢复 WebView,我们可以使用 和 方法(在 iOS 上,它由它自己自动管理)。相反,要暂停/恢复 JavaScript 执行,我们可以使用 和 方法。InAppWebViewController.android.pause()``InAppWebViewController.android.resume()``WKWebView``InAppWebViewController.pauseTimers()``InAppWebViewController.resumeTimers()

注意:在 Android 上,调用暂停/恢复计时器方法将暂停/恢复所有 WebView 的 JavaScript 执行,相反,在 iOS 上,它将仅针对特定的 WebView 暂停/恢复。

当 App 生命周期状态发生变化时,也就是 Flutter 触发事件时,会调用这些方法:didChangeAppLifecycleState

scss 复制代码
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  if (_webViewController != null) {
    if (state == AppLifecycleState.paused) {      if (Platform.isAndroid) {
        _webViewController?.android?.pause();
      }
      _webViewController?.pauseTimers();    } else {      if (Platform.isAndroid) {
        if (Platform.isAndroid) {
          _webViewController?.android?.resume();
        }
        _webViewController?.resumeTimers();
      } else if (Platform.isIOS) {
        var currentWebViewModel =
          Provider.of<WebViewModel>(context, listen: false);
        if (widget.webViewModel.tabIndex ==
          currentWebViewModel.tabIndex) {
          _webViewController?.resumeTimers();
        } else {
          _webViewController?.pauseTimers();
        }
      }    }
  }
}

此外,我们希望在出现加载错误时显示自定义错误页面。对于这种情况,我们可以监听事件,例如,将自定义 HTML 加载到 WebView(或任何您想要的内容)中:onLoadError

xml 复制代码
onLoadError: (controller, url, code, message) async {
  if (Platform.isIOS && code == -999) {
    // NSURLErrorDomain
    return;
  }

  url = url ?? 'about:blank';

  _webViewController?.loadData(data: """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <style>
    ${await _webViewController?.getTRexRunnerCss()}
    </style>
    <style>
    .interstitial-wrapper {
        box-sizing: border-box;
        font-size: 1em;
        line-height: 1.6em;
        margin: 0 auto 0;
        max-width: 600px;
        width: 100%;
    }
    </style>
</head>
<body>
    ${await _webViewController?.getTRexRunnerHtml()}
    <div class="interstitial-wrapper">
      <h1>Website not available</h1>
      <p>Could not load web pages at <strong>$url</strong> because:</p>
      <p>$message</p>
    </div>
</body>
    """, baseUrl: url, androidHistoryUrl: url);
},

在Android上,我们还可以使用以下方法禁用默认错误页面 Android特定选项 .结果如下:disableDefaultErrorPage: true

T-Rex Runner 游戏的自定义错误页面。

是的!你是对的!这是霸王龙奔跑者游戏!🦖🦖🦖

浏览器应用栏

浏览器应用栏及其操作基于 Google Chrome 移动应用。如您所见,它由一个搜索栏、一个显示选项卡数量的矩形和一个下拉菜单组成。

它是其中的一个实例:AppBar

  • 如果启用了"主页"选项,则前导项是IconButton ;
  • 标题项是一个小部件,由监听事件的简单部件和指示当前网站是否安全或是否为离线网站(网络存档)的小部件组成;Stack``TextField``onSubmitted``IconButton
  • 操作项是一个小部件,它包含当前选项卡的数量,并提供移动到另一个选项卡或关闭它们的访问权限,以及一个小部件,其中包含所有可用操作的列表,例如"新选项卡"、"新隐身选项卡"、"设置"页面、"开发人员"页面等。InkWell``PopupMenuButton

浏览器应用栏。

如您所见,下拉操作类似于 Google Chrome 移动应用程序:我们可以浏览 WebView 历史记录、将当前页面保存为 Web 存档、将网站保存到收藏夹列表、启用桌面模式、重新加载页面、截屏等。

包含收藏夹列表,这是一个简单的实例列表,其中包含保存为收藏夹的每个网页的网站图标、URL 和标题。还包含一个,表示使用该方法保存的 Web 存档(目前,此方法仅在 Android 上可用)以下载网页以便离线使用它们。BrowserModel``FavoriteModel``BrowserModel``Map<String, WebArchiveModel>``InAppWebViewController.android.saveWebArchive

相反,WebView 选项卡历史记录由该方法提供,该方法返回当前会话期间访问的 URL 和标题的列表。InAppWebViewController.getCopyBackForwardList

如果我们单击"在页面上查找"操作,浏览器应用栏将显示相应的小部件,该小部件通过小部件和 3 秒作为操作管理"在页面上查找"WebView 功能,允许我们使用该方法搜索单词,使用该方法从一个结果移动到另一个结果,并清除使用该方法找到的匹配项。AppBar``TextField``IconButton``InAppWebViewController.findAllAsync(find: "")``InAppWebViewController.findNext(forward: )``InAppWebViewController.clearMatches

在"应用栏"页上查找。

因此,浏览器应用栏将是一个小部件,它实现:PreferredSizeWidet

scala 复制代码
class BrowserAppBar extends StatefulWidget
    implements PreferredSizeWidget {
  BrowserAppBar({Key key})
      : preferredSize = Size.fromHeight(kToolbarHeight),
        super(key: key);

  @override
  _BrowserAppBarState createState() => _BrowserAppBarState();

  @override
  final Size preferredSize;
}

class _BrowserAppBarState extends State<BrowserAppBar> {
  bool _isFindingOnPage = false;

  @override
  Widget build(BuildContext context) {
    return _isFindingOnPage
        ? FindOnPageAppBar(
            hideFindOnPage: () {
              setState(() {
                _isFindingOnPage = false;
              });
            },
          )
        : WebViewTabAppBar(
            showFindOnPage: () {
              setState(() {
                _isFindingOnPage = true;
              });
            },
          );
  }
}

where 和 are 实例。FindOnPageAppBar``WebViewTabAppBar``AppBar

SSL证书查看器

如您所见,如果当前网站使用有效的SSL证书(我们通过WebView事件进行检查),或者是本地内容,则在网站URL的左侧将有一个绿色的锁定图标,否则,将显示灰色轮廓信息图标。onReceivedServerTrustAuthRequest

如果您单击它,该应用程序会显示一个自定义弹出对话框,该对话框使用自定义类实现,以便我们创建具有透明背景的新页面路由。PageRoute

如果单击"详细信息",然后单击"证书信息",则可以看到包含所有 X509 证书详细信息的小部件。AlertDialog

从一个 WebView 选项卡移动到另一个 WebView 选项卡

因为我们保存了所有 WebView 选项卡的列表,并且每个 WebView 选项卡都有其 ,我们可以更改显示的 WebView,将当前的 WebView 选项卡索引更新为我们想要显示的索引。BrowserModel``GlobalKey

我们可以通过多种方式实现这一点,例如,使用一个简单的方法,其中每个子项显示 WebView 选项卡标题、网站图标和 URL,并且,当用户单击它时,我们使用单击的子项的索引来更新实例的当前 WebView 选项卡索引。ListView``BrowserModel

在此示例应用程序中,我使用一个名为的自定义小部件来实现它,该小部件使用小部件来侦听垂直拖动事件,以模仿 Google Chrome 移动应用程序的行为。使用类和小部件,我获得了以下效果:TabViewer``GestureDetector``Timer``Transform

WebView 选项卡查看器。

开发者控制台

Developer Console 页面包含一个包含 3 个选项卡的选项卡:TabBarView

  • JavaScript 控制台:您可以在其中查看控制台日志并执行 JavaScript 代码,就像您在 Google Chrome 桌面版的 JavaScript 控制台上一样;
  • 网络信息:在这里您可以看到为主机加载的所有资源,例如图像、XMLHttpRequests 等;
  • 存储管理器:您可以在其中管理 cookie、Web 存储和 HTTP 身份验证凭据。

为了侦听 JavaScript 控制台日志,我们使用 WebView 选项卡上的 WebView 事件,而不是执行 JavaScript 代码,我们使用 该方法。onConsoleMessage``InAppWebViewController.evaluateJavascript(source: "")

开发者控制台页面。

每个控制台日志消息和 JavaScript 结果都将添加到"JavaScript 控制台"选项卡中显示的列表中。此外,我们可以浏览我们的 JavaScript 代码历史记录并清除当前列表。

为了侦听资源加载,我们使用 WebView 选项卡上的 WebView 事件。每个资源都将添加到"网络信息"选项卡中显示的列表中。onLoadResource

相反,存储管理器选项卡将使用该类来管理 cookie、管理本地和会话存储、管理 HTTP 身份验证凭据以及管理一般的 Web 存储,例如应用程序缓存 API、Web SQL 数据库 API 和 HTML5 Web 存储 API(在 Android 上,它是使用 WebStorage 实现的,而在 iOS 上,它是使用 WKWebsiteDataStore.default()) 实现的)。CookieManager``InAppWebViewController.webStorage``HttpAuthCredentialDatabase``WebStorageManager

"设置"页

此页面包含所有浏览器设置以及当前的 WebView 选项卡设置。它还包含一个带有 3 个选项卡:TabBarView

  • 跨平台选项,例如User-Agent,启用/禁用JavaScript,启用/禁用缩放支持等;
  • 特定于 Android 的选项,例如启用/禁用 DOM 存储 API 和数据库存储 API、缓存模式等;
  • 特定于 iOS 的选项,例如启用/禁用滚动、原生链接预览等。

每个选项卡都包含一个小部件,其中每个子项可以是一个小部件、一个小部件或带有一个小部件的小部件,该小部件表示带有少量描述的 WebView 选项。ListView``ListTile``SwitchListTile``Container``DropdownButton

保存和恢复浏览器状态

如前所述,要保存和恢复浏览器状态,例如WebView选项卡的当前列表,我们将使用该包。shared_preferences

每次 App 使用类提供的方法识别出当前 和 当前(例如当前 URL 或标题)中的某些内容发生更改时,我们都会将实例编码为 JSON 字符串,并使用包保存它。BrowserModel``WebViewModel``addListener``ChangeNotifier``BrowserModel``shared_preferences

为了避免在快速的顺序更改中保存,我们不会立即保存它,而是等待检查在特定时间范围内是否发出了另一个"保存"请求。然后,我们保存它。

当我们在应用程序重启期间恢复浏览器状态时,我们会解码 JSON 字符串,并将所有必要的数据添加/覆盖到当前实例中。BrowserModel

我们可以使用类中定义的以下 3 种方法(保存、刷新和恢复)来实现这一点:BrowserModel

ini 复制代码
DateTime _lastTrySave = DateTime.now();
Timer _timerSave;
Future<void> save() async {
  _timerSave?.cancel();

  if (DateTime.now().difference(_lastTrySave) >= Duration(milliseconds: 400)) {
    _lastTrySave = DateTime.now();
    await flush();
  } else {
    _lastTrySave = DateTime.now();
    _timerSave = Timer(Duration(milliseconds: 500), () {
      save();
    });
  }
}

Future<void> flush() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  await prefs.setString("browser", json.encode(toJson()));
}

Future<void> restore() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  Map<String, dynamic> browserData;
  try {
    browserData = await json.decode(prefs.getString("browser"));
  } catch (e) {
    print(e);
    return;
  }

  this.clearFavorites();
  this.closeAllTabs();
  this.clearWebArchives();

  List<Map<String, dynamic>> favoritesList = browserData["favorites"]?.cast<Map<String, dynamic>>();
  List<FavoriteModel> favorites = favoritesList?.map((e) => FavoriteModel.fromMap(e))?.toList() ?? [];

  Map<String, dynamic> webArchivesMap = browserData["webArchives"]?.cast<String, dynamic>() ?? {};
  Map<String, WebArchiveModel> webArchives = webArchivesMap.map((key, value) =>
      MapEntry(key, WebArchiveModel.fromMap(value?.cast<String, dynamic>())));

  BrowserSettings settings = BrowserSettings.fromMap(browserData["settings"]?.cast<String, dynamic>()) ?? BrowserSettings();
  List<Map<String, dynamic>> webViewTabList = browserData["webViewTabs"]?.cast<Map<String, dynamic>>();
  List<WebViewTab> webViewTabs = webViewTabList
      ?.map((e) => WebViewTab(
        key: GlobalKey(),
        webViewModel: WebViewModel.fromMap(e),
      ))
      ?.toList() ?? [];
  webViewTabs.sort((a, b) => a.webViewModel.tabIndex.compareTo(b.webViewModel.tabIndex));


  this.addFavorites(favorites);
  this.addWebArchives(webArchives);
  this.updateSettings(settings);
  this.addTabs(webViewTabs);

  int currentTabIndex = browserData["currentTabIndex"] ?? this._currentTabIndex;
  currentTabIndex = min(currentTabIndex, this._webViewTabs.length - 1);

  if (currentTabIndex >= 0)
    this.showTab(currentTabIndex);
}

结论

在本文中,我们使用该插件创建了一个功能齐全的浏览器应用程序 。该插件正在持续开发中(在撰写本文时,最新版本是),我建议您查看 API 参考以了解所有功能和之前介绍 flutter_inappwebview 插件的文章:InAppWebView:Flutter 中 WebView 的真正力量。对于任何新功能请求/错误修复,您可以使用存储库的 issue 部分。flutter_inappwebview``4.0.0+4

如果你觉得这很有用,并且你喜欢这个插件和这个应用程序项目,请给一个星标:flutter_inappwebview

以上就是今天的全部内容,感谢您的关注!

相关推荐
程序员海军几秒前
2024 Nuxt3 年度生态总结
前端·nuxt.js
m0_7482567811 分钟前
SpringBoot 依赖之Spring Web
前端·spring boot·spring
web1350858863539 分钟前
前端node.js
前端·node.js·vim
m0_5127446441 分钟前
极客大挑战2024-web-wp(详细)
android·前端
若川1 小时前
Taro 源码揭秘:10. Taro 到底是怎样转换成小程序文件的?
前端·javascript·react.js
潜意识起点1 小时前
精通 CSS 阴影效果:从基础到高级应用
前端·css
奋斗吧程序媛1 小时前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode
IT女孩儿1 小时前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
m0_748256564 小时前
如何解决前端发送数据到后端为空的问题
前端
请叫我飞哥@4 小时前
HTML5适配手机
前端·html·html5