进阶实战 Flutter for OpenHarmony:webview_flutter 第三方库实战 - 智能内嵌浏览器系统

进阶实战 Flutter for OpenHarmony:webview_flutter 第三方库实战 - 智能内嵌浏览器系统
在这里插入图片描述
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net

🔍 一、第三方库概述与应用场景

📱 1.1 为什么需要 WebView?

在移动应用开发中,WebView 是连接原生应用与 Web 技术的重要桥梁。很多场景下,我们需要在应用中嵌入网页内容,比如展示用户协议、加载 H5 活动、实现混合开发等。使用 WebView 可以让开发者充分利用 Web 技术的灵活性,同时保持原生应用的体验。

想象一下这样的场景:用户打开一个电商应用,首页是原生开发的,但当用户点击活动页面时,应用加载了一个精美的 H5 活动页面。用户可以正常浏览、点击、滚动,体验几乎与原生页面无异。这就是 WebView 的魅力所在。

📋 1.2 webview_flutter 是什么?

webview_flutter 是 Flutter 官方维护的 WebView 插件,提供了在 Flutter 应用中嵌入网页浏览功能的能力。它支持加载 URL、加载 HTML 内容、JavaScript 双向交互、页面导航控制等功能,是 Flutter 混合开发的核心组件。

🎯 1.3 核心功能特性

功能特性详细说明OpenHarmony 支持
加载 URL加载网络网页链接✅ 完全支持
加载 HTML加载本地 HTML 内容✅ 完全支持
页面导航前进、后退、刷新✅ 完全支持
JavaScript 交互Flutter 与 JS 双向通信✅ 完全支持
Cookie 管理设置和获取 Cookie✅ 完全支持
UserAgent 设置自定义 UserAgent✅ 完全支持

💡 1.4 典型应用场景

混合开发:在原生应用中嵌入 H5 页面,实现灵活的内容更新。

协议展示:展示用户协议、隐私政策等网页内容。

第三方登录:实现 OAuth 授权登录流程。

支付页面:加载第三方支付页面。


🏗️ 二、系统架构设计

📐 2.1 整体架构

┌─────────────────────────────────────────────────────────┐ │ UI 层 (展示层) │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 地址栏 │ │ 进度条 │ │ 导航按钮 │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ ├─────────────────────────────────────────────────────────┤ │ 服务层 (业务逻辑) │ │ ┌─────────────────────────────────────────────────┐ │ │ │ WebViewService │ │ │ │ • 页面加载管理 │ │ │ │ • 导航状态管理 │ │ │ │ • JS 交互处理 │ │ │ │ • URL 拦截处理 │ │ │ └─────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────┤ │ 基础设施层 (底层实现) │ │ ┌─────────────────────────────────────────────────┐ │ │ │ webview_flutter 库 │ │ │ │ • WebViewController - 控制器 │ │ │ │ • WebViewWidget - 视图组件 │ │ │ │ • NavigationDelegate - 导航代理 │ │ │ │ • JavaScriptChannel - JS 通道 │ │ │ └─────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ 

📊 2.2 数据模型设计

/// 浏览器配置模型classWebViewConfig{/// 是否启用 JavaScriptfinal bool javaScriptEnabled;/// 是否启用缩放final bool zoomEnabled;/// 自定义 UserAgentfinalString? userAgent;/// 是否显示进度条final bool showProgressBar;constWebViewConfig({this.javaScriptEnabled =true,this.zoomEnabled =true,this.userAgent,this.showProgressBar =true,});}/// 导航状态模型classNavigationState{/// 是否可以后退final bool canGoBack;/// 是否可以前进final bool canGoForward;/// 当前 URLfinalString currentUrl;/// 加载进度final int progress;constNavigationState({this.canGoBack =false,this.canGoForward =false,this.currentUrl ='',this.progress =0,});}

📦 三、项目配置与依赖安装

📥 3.1 添加依赖

打开项目根目录下的 pubspec.yaml 文件,添加以下配置:

dependencies:flutter:sdk: flutter # webview_flutter - 内嵌浏览器插件webview_flutter:git:url:"https://atomgit.com/openharmony-tpc/flutter_packages.git"path:"packages/webview_flutter/webview_flutter"

配置说明

  • 使用 git 方式引用开源鸿蒙适配的 flutter_packages 仓库
  • url:指定 AtomGit 托管的仓库地址
  • path:指定 webview_flutter 包的具体路径
  • 本项目基于 [email protected] 开发,适配 Flutter 3.27.5-ohos-1.0.4
⚠️ 重要:对于 OpenHarmony 平台,必须使用 git 方式引用适配版本,不能直接使用 pub.dev 的版本号。

🔧 3.2 下载依赖

配置完成后,需要在项目根目录执行以下命令下载依赖:

flutter pub get 

🔐 3.3 权限配置

WebView 需要网络权限,在 ohos/entry/src/main/module.json5 中添加:

{"module":{"requestPermissions":[{"name":"ohos.permission.INTERNET","reason":"$string:network_reason","usedScene":{"abilities":["EntryAbility"],"when":"inuse"}}]}}

ohos/entry/src/main/resources/base/element/string.json 中添加:

{"string":[{"name":"network_reason","value":"使用网络加载网页内容"}]}

🛠️ 四、核心组件详解

🎬 4.1 WebViewController - 控制器

WebViewController 是 WebView 的核心控制器,提供所有控制方法。

finalWebViewController controller =WebViewController()// 启用 JavaScript..setJavaScriptMode(JavaScriptMode.unrestricted)// 设置导航代理..setNavigationDelegate(NavigationDelegate( onProgress:(int progress){// 页面加载进度}, onPageStarted:(String url){// 页面开始加载}, onPageFinished:(String url){// 页面加载完成}, onWebResourceError:(WebResourceError error){// 加载错误},))// 加载 URL..loadRequest(Uri.parse('https://www.example.com'));

📋 4.2 WebViewWidget - 视图组件

WebViewWidget 是显示 WebView 的组件。

WebViewWidget(controller: _controller);

🔄 4.3 JavaScript 双向交互

Flutter 调用 JavaScript:

// 执行 JavaScript 并获取返回值final result =await _controller.runJavaScript('document.title');print('页面标题: $result');

JavaScript 调用 Flutter:

// 注册 JavaScript 通道 _controller.addJavaScriptChannel('FlutterChannel', onMessageReceived:(JavaScriptMessage message){print('收到 JS 消息: ${message.message}');},);// JavaScript 端调用// FlutterChannel.postMessage('Hello from JavaScript');

🚦 4.4 URL 拦截

_controller.setNavigationDelegate(NavigationDelegate( onNavigationRequest:(NavigationRequest request){// 拦截特定 URLif(request.url.contains('tel:')){// 处理电话链接returnNavigationDecision.prevent;}returnNavigationDecision.navigate;},));

📝 五、完整示例代码

下面是一个完整的智能内嵌浏览器系统示例:

import'package:flutter/material.dart';import'package:webview_flutter/webview_flutter.dart';voidmain(){runApp(constWebViewApp());}classWebViewAppextendsStatelessWidget{constWebViewApp({super.key});@overrideWidgetbuild(BuildContext context){returnMaterialApp( title:'智能浏览器', debugShowCheckedModeBanner:false, theme:ThemeData( colorScheme:ColorScheme.fromSeed(seedColor:Colors.indigo), useMaterial3:true,), home:constMainPage(),);}}classMainPageextendsStatefulWidget{constMainPage({super.key});@overrideState<MainPage>createState()=>_MainPageState();}class _MainPageState extendsState<MainPage>{ int _currentIndex =0;finalList<Widget> _pages =[constBrowserPage(),constJsInteractionPage(),constBookmarkPage(),];@overrideWidgetbuild(BuildContext context){returnScaffold( body: _pages[_currentIndex], bottomNavigationBar:NavigationBar( selectedIndex: _currentIndex, onDestinationSelected:(index){setState(()=> _currentIndex = index);}, destinations:const[NavigationDestination(icon:Icon(Icons.language), label:'浏览器'),NavigationDestination(icon:Icon(Icons.code), label:'JS交互'),NavigationDestination(icon:Icon(Icons.bookmark), label:'书签'),],),);}}// ============ 浏览器页面 ============classBrowserPageextendsStatefulWidget{constBrowserPage({super.key});@overrideState<BrowserPage>createState()=>_BrowserPageState();}class _BrowserPageState extendsState<BrowserPage>{ late finalWebViewController _controller;finalTextEditingController _urlController =TextEditingController(); int _loadingProgress =0; bool _isLoading =false; bool _canGoBack =false; bool _canGoForward =false;String _currentTitle ='';@overridevoidinitState(){super.initState(); _controller =WebViewController()..setJavaScriptMode(JavaScriptMode.unrestricted)..setNavigationDelegate(NavigationDelegate( onProgress:(int progress){setState(()=> _loadingProgress = progress);}, onPageStarted:(String url){setState((){ _isLoading =true; _urlController.text = url;});}, onPageFinished:(String url)async{setState(()=> _isLoading =false);await_updateNavigationState();final title =await _controller.getTitle();setState(()=> _currentTitle = title ??'');}, onWebResourceError:(WebResourceError error){ScaffoldMessenger.of(context).showSnackBar(SnackBar(content:Text('加载失败: ${error.description}')),);},))..loadRequest(Uri.parse('https://www.baidu.com'));}Future<void>_updateNavigationState()async{final canGoBack =await _controller.canGoBack();final canGoForward =await _controller.canGoForward();setState((){ _canGoBack = canGoBack; _canGoForward = canGoForward;});}void_loadUrl(){String url = _urlController.text.trim();if(!url.startsWith('http://')&&!url.startsWith('https://')){ url ='https://$url';} _controller.loadRequest(Uri.parse(url));}@overrideWidgetbuild(BuildContext context){returnScaffold( body:SafeArea( child:Column( children:[_buildUrlBar(),if(_isLoading)LinearProgressIndicator( value: _loadingProgress /100.0, backgroundColor:Colors.grey.shade200, valueColor:AlwaysStoppedAnimation<Color>(Colors.indigo.shade400),),Expanded(child:WebViewWidget(controller: _controller)),_buildNavigationBar(),],),),);}Widget_buildUrlBar(){returnContainer( padding:constEdgeInsets.all(8), decoration:BoxDecoration( color:Colors.grey.shade100, boxShadow:[BoxShadow( color:Colors.black.withOpacity(0.05), blurRadius:4, offset:constOffset(0,2),),],), child:Row( children:[Expanded( child:TextField( controller: _urlController, decoration:InputDecoration( hintText:'输入网址', filled:true, fillColor:Colors.white, contentPadding:constEdgeInsets.symmetric(horizontal:16, vertical:10), border:OutlineInputBorder( borderRadius:BorderRadius.circular(24), borderSide:BorderSide.none,), suffixIcon:IconButton( icon:constIcon(Icons.clear, size:18), onPressed:()=> _urlController.clear(),),), onSubmitted:(_)=>_loadUrl(),),),constSizedBox(width:8),IconButton( icon:constIcon(Icons.refresh), onPressed:()=> _controller.reload(),),],),);}Widget_buildNavigationBar(){returnContainer( padding:constEdgeInsets.symmetric(vertical:8), decoration:BoxDecoration( color:Colors.white, boxShadow:[BoxShadow( color:Colors.black.withOpacity(0.05), blurRadius:4, offset:constOffset(0,-2),),],), child:Row( mainAxisAlignment:MainAxisAlignment.spaceEvenly, children:[IconButton( icon:constIcon(Icons.arrow_back), onPressed: _canGoBack ?()=> _controller.goBack():null,),IconButton( icon:constIcon(Icons.arrow_forward), onPressed: _canGoForward ?()=> _controller.goForward():null,),IconButton( icon:constIcon(Icons.home), onPressed:()=> _controller.loadRequest(Uri.parse('https://www.baidu.com')),),IconButton( icon:constIcon(Icons.share), onPressed:(){ScaffoldMessenger.of(context).showSnackBar(SnackBar(content:Text('分享: $_currentTitle')),);},),],),);}}// ============ JS 交互页面 ============classJsInteractionPageextendsStatefulWidget{constJsInteractionPage({super.key});@overrideState<JsInteractionPage>createState()=>_JsInteractionPageState();}class _JsInteractionPageState extendsState<JsInteractionPage>{ late finalWebViewController _controller;finalTextEditingController _messageController =TextEditingController();String _receivedMessage ='';finalList<String> _messageHistory =[];@overridevoidinitState(){super.initState(); _controller =WebViewController()..setJavaScriptMode(JavaScriptMode.unrestricted)..addJavaScriptChannel('FlutterChannel', onMessageReceived:(JavaScriptMessage message){setState((){ _receivedMessage = message.message; _messageHistory.insert(0,'JS: ${message.message}');});},)..loadHtmlString(''' <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; } .container { background: white; border-radius: 16px; padding: 24px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); } h1 { color: #333; margin-bottom: 20px; font-size: 24px; } .input-group { margin-bottom: 16px; } input { width: 100%; padding: 12px 16px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 16px; transition: border-color 0.3s; } input:focus { outline: none; border-color: #667eea; } button { width: 100%; padding: 14px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: bold; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; } button:active { transform: scale(0.98); } #result { margin-top: 20px; padding: 16px; background: #f5f5f5; border-radius: 8px; min-height: 60px; } </style> </head> <body> <div> <h1>📱 Flutter 与 JS 交互</h1> <div> <input type="text" placeholder="输入消息发送给 Flutter"> </div> <button onclick="sendToFlutter()">发送给 Flutter</button> <div>等待 Flutter 消息...</div> </div> <script> function sendToFlutter() { var message = document.getElementById('messageInput').value; if (message) { FlutterChannel.postMessage(message); document.getElementById('messageInput').value = ''; } } function receiveFromFlutter(message) { document.getElementById('result').innerHTML = '<strong>Flutter 说:</strong> ' + message; } </script> </body> </html> ''');}void_sendToJs(){final message = _messageController.text.trim();if(message.isNotEmpty){ _controller.runJavaScript("receiveFromFlutter('$message')");setState((){ _messageHistory.insert(0,'Flutter: $message');}); _messageController.clear();}}@overrideWidgetbuild(BuildContext context){returnScaffold( appBar:AppBar( title:constText('JavaScript 交互'), centerTitle:true,), body:Column( children:[Expanded( child:WebViewWidget(controller: _controller),),Container( padding:constEdgeInsets.all(16), decoration:BoxDecoration( color:Colors.white, boxShadow:[BoxShadow( color:Colors.black.withOpacity(0.1), blurRadius:8, offset:constOffset(0,-2),),],), child:Column( crossAxisAlignment:CrossAxisAlignment.start, children:[if(_messageHistory.isNotEmpty)...[Text('消息历史', style:TextStyle( color:Colors.grey.shade600, fontSize:12, fontWeight:FontWeight.bold,),),constSizedBox(height:8),Container( height:60, child:ListView.builder( scrollDirection:Axis.horizontal, itemCount: _messageHistory.length, itemBuilder:(context, index){returnContainer( margin:constEdgeInsets.only(right:8), padding:constEdgeInsets.symmetric(horizontal:12, vertical:8), decoration:BoxDecoration( color: _messageHistory[index].startsWith('Flutter')?Colors.indigo.shade50 :Colors.green.shade50, borderRadius:BorderRadius.circular(8),), child:Center( child:Text( _messageHistory[index], style:constTextStyle(fontSize:12),),),);},),),constSizedBox(height:12),],Row( children:[Expanded( child:TextField( controller: _messageController, decoration:InputDecoration( hintText:'输入消息发送给 JS', filled:true, fillColor:Colors.grey.shade100, border:OutlineInputBorder( borderRadius:BorderRadius.circular(12), borderSide:BorderSide.none,), contentPadding:constEdgeInsets.symmetric(horizontal:16),), onSubmitted:(_)=>_sendToJs(),),),constSizedBox(width:12),FloatingActionButton( onPressed: _sendToJs, child:constIcon(Icons.send),),],),],),),],),);}}// ============ 书签页面 ============classBookmarkPageextendsStatefulWidget{constBookmarkPage({super.key});@overrideState<BookmarkPage>createState()=>_BookmarkPageState();}class _BookmarkPageState extendsState<BookmarkPage>{finalList<BookmarkItem> _bookmarks =[BookmarkItem(title:'百度', url:'https://www.baidu.com', icon:Icons.search),BookmarkItem(title:'开源鸿蒙社区', url:'https://openharmonycrossplatform.ZEEKLOG.net', icon:Icons.code),BookmarkItem(title:'Flutter 官网', url:'https://flutter.dev', icon:Icons.flutter_dash),BookmarkItem(title:'GitHub', url:'https://github.com', icon:Icons.code),];void_openBookmark(BookmarkItem bookmark){Navigator.push( context,MaterialPageRoute( builder:(context)=>BookmarkDetailPage(bookmark: bookmark),),);}@overrideWidgetbuild(BuildContext context){returnScaffold( appBar:AppBar( title:constText('书签管理'), centerTitle:true,), body:ListView.builder( padding:constEdgeInsets.all(16), itemCount: _bookmarks.length, itemBuilder:(context, index){final bookmark = _bookmarks[index];returnCard( margin:constEdgeInsets.only(bottom:12), child:ListTile( leading:Container( width:48, height:48, decoration:BoxDecoration( color:Colors.indigo.shade50, borderRadius:BorderRadius.circular(12),), child:Icon(bookmark.icon, color:Colors.indigo),), title:Text( bookmark.title, style:constTextStyle(fontWeight:FontWeight.w600),), subtitle:Text( bookmark.url, style:TextStyle(color:Colors.grey.shade500, fontSize:12), maxLines:1, overflow:TextOverflow.ellipsis,), trailing:constIcon(Icons.arrow_forward_ios, size:16), onTap:()=>_openBookmark(bookmark),),);},),);}}classBookmarkDetailPageextendsStatefulWidget{finalBookmarkItem bookmark;constBookmarkDetailPage({super.key, required this.bookmark});@overrideState<BookmarkDetailPage>createState()=>_BookmarkDetailPageState();}class _BookmarkDetailPageState extendsState<BookmarkDetailPage>{ late finalWebViewController _controller; int _loadingProgress =0;@overridevoidinitState(){super.initState(); _controller =WebViewController()..setJavaScriptMode(JavaScriptMode.unrestricted)..setNavigationDelegate(NavigationDelegate( onProgress:(int progress){setState(()=> _loadingProgress = progress);},))..loadRequest(Uri.parse(widget.bookmark.url));}@overrideWidgetbuild(BuildContext context){returnScaffold( appBar:AppBar( title:Text(widget.bookmark.title),), body:Column( children:[if(_loadingProgress <100)LinearProgressIndicator( value: _loadingProgress /100.0, backgroundColor:Colors.grey.shade200, valueColor:AlwaysStoppedAnimation<Color>(Colors.indigo.shade400),),Expanded(child:WebViewWidget(controller: _controller)),],),);}}// ============ 数据模型 ============classBookmarkItem{finalString title;finalString url;finalIconData icon;BookmarkItem({ required this.title, required this.url, required this.icon,});}

🏆 六、最佳实践与注意事项

⚠️ 6.1 性能优化

启用缓存:合理使用 WebView 缓存,减少重复加载。

预加载:对于常用页面,可以提前初始化 WebView。

内存管理:及时释放 WebView 资源,避免内存泄漏。

🔐 6.2 安全注意事项

HTTPS:优先使用 HTTPS 协议,确保数据安全。

JS 注入:避免直接拼接用户输入到 JavaScript 代码中。

URL 验证:对加载的 URL 进行验证,防止恶意链接。

📱 6.3 OpenHarmony 平台特殊说明

原生支持:webview_flutter 在 OpenHarmony 上完全支持。

权限配置:确保配置了网络权限。

版本兼容:使用 git 方式引用适配版本。


📌 七、总结

本文通过一个完整的智能内嵌浏览器系统案例,深入讲解了 webview_flutter 第三方库的使用方法与最佳实践:

页面加载:使用 WebViewController 加载 URL 和 HTML 内容。

导航控制:实现前进、后退、刷新等导航功能。

JS 交互:通过 JavaScriptChannel 和 runJavaScript 实现双向通信。

URL 拦截:使用 NavigationDelegate 拦截和处理特定 URL。

掌握这些技巧,你就能构建出专业级的内嵌浏览器功能,实现混合开发的需求。


参考资料

Read more

Cursor实战:Web版背单词应用开发演示

Cursor实战:Web版背单词应用开发演示

Cursor实战:Web版背单词应用开发演示 * 需求分析 * 自行编写需求文档 * 借助Cursor生成需求文档 * 前端UI设计 * 后端开发 * 项目结构 * 环境参数 * 数据库设计 * 安装Python依赖 * 运行应用 * 前端代码修改 * 测试前端界面 * 测试数据生成 * 功能测试 * Bug修复 * 总结 在上一篇《Cursor AI编程助手不完全指南》中,我们详细介绍了Cursor这款强大的AI编程工具。为了让大家能更直观地了解 Cursor 的实战应用价值,本文将通过一个实际项目来展示其开发流程。我们将使用 Cursor 开发一个 Web 版单词学习程序,通过这个案例,您将看到 AI 辅助开发的完整过程,体验从需求分析到代码实现的全过程。让我们开始这次实战之旅。 需求分析 在开始开发之前,明确的需求文档是项目成功的关键。一个好的需求文档不仅能指导开发方向,还能作为与 Cursor 进行高效对话的重要基础。我们有两种方式来准备需求文档:自行编写需求文档和借助 Cursor 生成需求文档

前端老铁别硬扛:手写防抖节流太累,2026年主流库实战避坑指南

前端老铁别硬扛:手写防抖节流太累,2026年主流库实战避坑指南

前端老铁别硬扛:手写防抖节流太累,2026年主流库实战避坑指南 * 前端老铁别硬扛:手写防抖节流太累,2026年主流库实战避坑指南 * 开头先唠两句 * 这俩兄弟到底是个啥鬼 * 现在的库都卷成啥样了 * Lodash:老大哥还是稳 * Underscore:廉颇老矣 * RxJS:函数式编程的"重炮" * 轻量级选手:just-debounce-it 和 throttle-debounce * WASM 狠人:rust-debounce 和 friends * 选错了真的会谢 * 坑一:定时器清理不干净,内存泄漏到怀疑人生 * 坑二:异步地狱,Promise 状态乱套 * 坑三:this 指向迷之丢失 * 坑四:时间参数的动态调整 * 真实项目里怎么骚操作 * 搜索框的终极方案:防抖 + 请求取消 + 竞态处理 * 无限滚动加载:节流的参数调优艺术 * 拖拽排序:防抖节流的组合拳 * 窗口

【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题

【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题

【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题 在开发 Web 应用时,尤其是集成了 Unity WebGL 内容的页面,遇到一个问题:当 Unity WebGL 渲染内容嵌入到一个 Tab 中时,切换 Tab 后画面会变黑,直到用户点击黑屏区域,才会恢复显示。 这个问题通常是因为 Unity 渲染在 Tab 切换时被暂停或未能获得焦点所致。 在本文中,我们将介绍如何在使用 Layui 框架时,通过监听 Tab 切换事件并强制 Unity WebGL 渲染恢复,来解决这一问题。 1. 问题描述 当 Unity WebGL 内容嵌入到页面中的多个