欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
本文基于flutter3.27.5开发

一、webview_flutter 库概述
WebView 是移动应用开发中展示网页内容的核心组件,无论是展示富文本内容、嵌入 H5 页面、还是实现混合开发,都需要用到 WebView 组件。在 Flutter for OpenHarmony 应用开发中,webview_flutter 是一个功能强大的 WebView 插件,提供了完整的网页渲染和交互能力。
webview_flutter 库特点
webview_flutter 库基于 Flutter 平台接口实现,提供了以下核心特性:
完整的网页渲染:支持加载 URL、HTML 字符串、本地文件、Flutter 资源等多种内容源,满足各种网页展示需求。
JavaScript 交互:支持 JavaScript 执行、JavaScript 通道通信、JavaScript 对话框处理,实现 Flutter 与网页的双向通信。
导航控制:提供完整的导航代理机制,支持拦截导航请求、控制页面跳转、处理加载进度等。
Cookie 管理:提供独立的 Cookie 管理器,支持 Cookie 的读取、设置、清除等操作。
权限处理:支持处理网页的权限请求,如摄像头、麦克风、地理位置等权限。
支持平台对比
| 平台 | 支持情况 | 底层实现 |
|---|---|---|
| Android | ✅ 完全支持 | WebView |
| iOS | ✅ 完全支持 | WKWebView |
| Web | ✅ 完全支持 | iframe |
| macOS | ✅ 完全支持 | WKWebView |
| Windows | ✅ 完全支持 | WebView2 |
| Linux | ✅ 完全支持 | WebKitGTK |
| OpenHarmony | ✅ 完全支持 | Web组件 |
功能支持对比
| 功能 | Android | iOS | OpenHarmony |
|---|---|---|---|
| 加载 URL | ✅ | ✅ | ✅ |
| 加载 HTML | ✅ | ✅ | ✅ |
| 加载本地文件 | ✅ | ✅ | ✅ |
| JavaScript 执行 | ✅ | ✅ | ✅ |
| JavaScript 通道 | ✅ | ✅ | ✅ |
| Cookie 管理 | ✅ | ✅ | ✅ |
| 导航代理 | ✅ | ✅ | ✅ |
| 权限请求 | ✅ | ✅ | ✅ |
| 自定义请求头 | ✅ | ✅ | ✅ |
使用场景:嵌入 H5 页面、展示富文本内容、混合开发、网页应用壳等。
二、安装与配置
2.1 添加依赖
在项目的 pubspec.yaml 文件中添加 webview_flutter 依赖:
yaml
dependencies:
webview_flutter:
git:
url: https://atomgit.com/openharmony-tpc/flutter_packages.git
path: packages/webview_flutter/webview_flutter
然后执行以下命令获取依赖:
bash
flutter pub get
2.2 兼容性信息
| 项目 | 版本要求 |
|---|---|
| Flutter SDK | 3.7.12-ohos-1.0.6 |
| OpenHarmony SDK | 5.0.0 (API 12) |
| DevEco Studio | 5.0.13.200 |
| ROM | 5.1.0.120 SP3 |
2.3 权限配置
webview_flutter 在 OpenHarmony 平台上需要配置网络权限:
在 entry 目录下的 module.json5 中添加权限
打开 ohos/entry/src/main/module.json5,在 requestPermissions 数组中添加:
json
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:network_reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
}
]
在 entry 目录下添加申请权限的原因
打开 ohos/entry/src/main/resources/base/element/string.json,在 string 数组中添加:
json
{
"string": [
{
"name": "network_reason",
"value": "使用网络访问文件资源"
}
]
}
三、核心 API 详解
3.1 WebViewController 类
WebViewController 是 WebView 的核心控制器,负责管理 WebView 的所有操作。
创建控制器:
dart
final WebViewController controller = WebViewController();
设置 JavaScript 模式:
dart
controller.setJavaScriptMode(JavaScriptMode.unrestricted);
JavaScriptMode.unrestricted 表示启用 JavaScript,JavaScriptMode.disabled 表示禁用 JavaScript。
加载 URL:
dart
controller.loadRequest(Uri.parse('https://flutter.dev'));
加载 HTML 字符串:
dart
controller.loadHtmlString('<html><body><h1>Hello WebView</h1></body></html>');
加载本地文件:
dart
controller.loadFile('/path/to/file.html');
加载 Flutter 资源:
dart
controller.loadFlutterAsset('assets/www/index.html');
导航操作:
dart
bool canGoBack = await controller.canGoBack();
bool canGoForward = await controller.canGoForward();
await controller.goBack();
await controller.goForward();
await controller.reload();
获取当前 URL:
dart
String? currentUrl = await controller.currentUrl();
获取页面标题:
dart
String? title = await controller.getTitle();
执行 JavaScript:
dart
await controller.runJavaScript('alert("Hello from Flutter")');
Object result = await controller.runJavaScriptReturningResult('document.title');
3.2 WebViewWidget 类
WebViewWidget 是用于显示 WebView 的 Widget,需要传入 WebViewController。
dart
WebViewWidget(controller: controller)
完整示例:
dart
class WebViewPage extends StatefulWidget {
const WebViewPage({super.key});
@override
State<WebViewPage> createState() => _WebViewPageState();
}
class _WebViewPageState extends State<WebViewPage> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse('https://flutter.dev'));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('WebView')),
body: WebViewWidget(controller: _controller),
);
}
}
3.3 NavigationDelegate 类
NavigationDelegate 用于处理导航事件,包括页面开始加载、加载完成、加载错误、导航请求等。
dart
final NavigationDelegate delegate = NavigationDelegate(
onNavigationRequest: (NavigationRequest request) {
if (request.url.contains('blocked-site.com')) {
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
onPageStarted: (String url) {
print('页面开始加载: $url');
},
onPageFinished: (String url) {
print('页面加载完成: $url');
},
onProgress: (int progress) {
print('加载进度: $progress%');
},
onWebResourceError: (WebResourceError error) {
print('加载错误: ${error.description}');
},
);
controller.setNavigationDelegate(delegate);
NavigationDecision 枚举:
NavigationDecision.navigate 表示允许导航。
NavigationDecision.prevent 表示阻止导航。
3.4 JavaScriptChannel 类
JavaScriptChannel 用于建立 Flutter 与网页之间的通信通道。
dart
controller.addJavaScriptChannel(
'Toaster',
onMessageReceived: (JavaScriptMessage message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message.message)),
);
},
);
在网页中调用:
javascript
Toaster.postMessage('Hello from WebView');
3.5 WebViewCookieManager 类
WebViewCookieManager 用于管理 WebView 的 Cookie。
dart
final WebViewCookieManager cookieManager = WebViewCookieManager();
await cookieManager.setCookie(
WebViewCookie(
name: 'session',
value: 'abc123',
domain: 'example.com',
path: '/',
),
);
List<WebViewCookie> cookies = await cookieManager.getCookies('https://example.com');
await cookieManager.clearCookies();
四、实战案例
4.1 基础 WebView 页面
dart
class BasicWebViewPage extends StatefulWidget {
const BasicWebViewPage({super.key});
@override
State<BasicWebViewPage> createState() => _BasicWebViewPageState();
}
class _BasicWebViewPageState extends State<BasicWebViewPage> {
late final WebViewController _controller;
bool _isLoading = true;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageStarted: (_) => setState(() => _isLoading = true),
onPageFinished: (_) => setState(() => _isLoading = false),
))
..loadRequest(Uri.parse('https://flutter.dev'));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('基础 WebView')),
body: Stack(
children: [
WebViewWidget(controller: _controller),
if (_isLoading)
const Center(child: CircularProgressIndicator()),
],
),
);
}
}
4.2 带导航控制的 WebView
dart
class NavigationWebViewPage extends StatefulWidget {
const NavigationWebViewPage({super.key});
@override
State<NavigationWebViewPage> createState() => _NavigationWebViewPageState();
}
class _NavigationWebViewPageState extends State<NavigationWebViewPage> {
late final WebViewController _controller;
bool _canGoBack = false;
bool _canGoForward = false;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageFinished: (_) async {
setState(() {
_canGoBack = _controller.canGoBack() as bool;
_canGoForward = _controller.canGoForward() as bool;
});
},
))
..loadRequest(Uri.parse('https://flutter.dev'));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('导航控制'),
actions: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: _canGoBack ? () => _controller.goBack() : null,
),
IconButton(
icon: const Icon(Icons.arrow_forward),
onPressed: _canGoForward ? () => _controller.goForward() : null,
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => _controller.reload(),
),
],
),
body: WebViewWidget(controller: _controller),
);
}
}
4.3 Flutter 与 WebView 通信
dart
class CommunicationWebViewPage extends StatefulWidget {
const CommunicationWebViewPage({super.key});
@override
State<CommunicationWebViewPage> createState() => _CommunicationWebViewPageState();
}
class _CommunicationWebViewPageState extends State<CommunicationWebViewPage> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'FlutterChannel',
onMessageReceived: (JavaScriptMessage message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('收到消息: ${message.message}')),
);
},
)
..loadHtmlString('''
<html>
<body>
<h1>Flutter 与 WebView 通信</h1>
<button onclick="sendMessage()">发送消息到 Flutter</button>
<script>
function sendMessage() {
FlutterChannel.postMessage('Hello from WebView!');
}
</script>
</body>
</html>
''');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('通信示例')),
body: WebViewWidget(controller: _controller),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller.runJavaScript('document.body.style.backgroundColor = "lightblue"');
},
child: const Icon(Icons.color_lens),
),
);
}
}
4.4 拦截特定 URL
dart
class BlockedUrlWebViewPage extends StatefulWidget {
const BlockedUrlWebViewPage({super.key});
@override
State<BlockedUrlWebViewPage> createState() => _BlockedUrlWebViewPageState();
}
class _BlockedUrlWebViewPageState extends State<BlockedUrlWebViewPage> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onNavigationRequest: (NavigationRequest request) {
if (request.url.contains('youtube.com')) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已阻止访问 YouTube')),
);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
))
..loadRequest(Uri.parse('https://flutter.dev'));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('URL 拦截')),
body: WebViewWidget(controller: _controller),
);
}
}
五、最佳实践
5.1 处理加载状态
始终显示加载指示器,提升用户体验:
dart
class LoadingIndicatorWebView extends StatefulWidget {
const LoadingIndicatorWebView({super.key, required this.url});
final String url;
@override
State<LoadingIndicatorWebView> createState() => _LoadingIndicatorWebViewState();
}
class _LoadingIndicatorWebViewState extends State<LoadingIndicatorWebView> {
late final WebViewController _controller;
int _loadingProgress = 0;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onProgress: (int progress) {
setState(() => _loadingProgress = progress);
},
))
..loadRequest(Uri.parse(widget.url));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('WebView'),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(3),
child: LinearProgressIndicator(
value: _loadingProgress / 100,
backgroundColor: Colors.transparent,
),
),
),
body: WebViewWidget(controller: _controller),
);
}
}
5.2 错误处理
妥善处理加载错误:
dart
controller.setNavigationDelegate(NavigationDelegate(
onWebResourceError: (WebResourceError error) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('加载失败'),
content: Text('错误代码: ${error.errorCode}\n错误信息: ${error.description}'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
controller.reload();
},
child: const Text('重试'),
),
],
),
);
},
));
5.3 处理权限请求
处理网页的权限请求:
dart
controller = WebViewController(
onPermissionRequest: (WebViewPermissionRequest request) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('权限请求'),
content: Text('网页请求以下权限: ${request.types.map((t) => t.name).join(', ')}'),
actions: [
TextButton(
onPressed: () {
request.deny();
Navigator.pop(context);
},
child: const Text('拒绝'),
),
TextButton(
onPressed: () {
request.grant();
Navigator.pop(context);
},
child: const Text('允许'),
),
],
),
);
},
);
六、常见问题
Q1:如何获取 WebView 内容高度?
dart
Future<double> getContentHeight() async {
final result = await controller.runJavaScriptReturningResult(
'document.body.scrollHeight',
);
return double.parse(result.toString());
}
Q2:如何注入自定义 CSS?
dart
await controller.runJavaScript('''
var style = document.createElement('style');
style.innerHTML = 'body { background-color: #f0f0f0; }';
document.head.appendChild(style);
''');
Q3:如何处理文件下载?
WebView 本身不直接支持文件下载,需要通过导航代理拦截下载链接:
dart
controller.setNavigationDelegate(NavigationDelegate(
onNavigationRequest: (request) {
if (request.url.endsWith('.pdf') || request.url.endsWith('.zip')) {
downloadFile(request.url);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
));
Q4:如何清除缓存?
dart
await controller.clearCache();
await controller.clearLocalStorage();
七、总结
webview_flutter 库为 Flutter for OpenHarmony 开发提供了完整的 WebView 功能。通过 WebViewController、WebViewWidget、NavigationDelegate 等核心类,开发者可以轻松实现网页展示、JavaScript 交互、导航控制、Cookie 管理等功能。
八、完整代码示例
以下是一个完整的可运行示例,展示了 webview_flutter 库的核心功能:
pubspec.yaml
yaml
name: webview_flutter_demo
description: Flutter for OpenHarmony webview_flutter 演示项目
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
webview_flutter:
git:
url: https://atomgit.com/openharmony-tpc/flutter_packages.git
path: packages/webview_flutter/webview_flutter
flutter:
uses-material-design: true
assets:
- assets/www/
assets/www/index.html
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebView Demo</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
margin: 0;
}
.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;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
margin: 8px 0;
width: 100%;
}
button:active {
transform: scale(0.98);
}
#output {
background: #f5f5f5;
padding: 16px;
border-radius: 8px;
margin-top: 16px;
min-height: 60px;
}
</style>
</head>
<body>
<div class="container">
<h1>WebView 通信演示</h1>
<p>点击按钮向 Flutter 发送消息</p>
<button onclick="sendToFlutter()">发送消息到 Flutter</button>
<button onclick="changeBackground()">改变背景色</button>
<button onclick="showInfo()">获取设备信息</button>
<div id="output">等待操作...</div>
</div>
<script>
function sendToFlutter() {
FlutterChannel.postMessage('来自 WebView 的问候!时间: ' + new Date().toLocaleTimeString());
document.getElementById('output').innerText = '消息已发送到 Flutter';
}
function changeBackground() {
const colors = ['#667eea', '#764ba2', '#f093fb', '#f5576c', '#4facfe'];
const randomColor = colors[Math.floor(Math.random() * colors.length)];
document.body.style.background = 'linear-gradient(135deg, ' + randomColor + ' 0%, ' + colors[(colors.indexOf(randomColor) + 1) % colors.length] + ' 100%)';
document.getElementById('output').innerText = '背景色已改变';
}
function showInfo() {
const info = 'User Agent: ' + navigator.userAgent + '\n屏幕: ' + screen.width + 'x' + screen.height;
document.getElementById('output').innerText = info;
}
</script>
</body>
</html>

main.dart
dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WebView Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late final WebViewController _controller;
int _loadingProgress = 0;
String _currentUrl = '';
bool _canGoBack = false;
bool _canGoForward = false;
@override
void initState() {
super.initState();
_initController();
}
void _initController() {
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.white)
..setNavigationDelegate(NavigationDelegate(
onProgress: (int progress) {
setState(() => _loadingProgress = progress);
},
onPageStarted: (String url) {
setState(() => _currentUrl = url);
},
onPageFinished: (String url) async {
setState(() {
_canGoBack = await _controller.canGoBack();
_canGoForward = await _controller.canGoForward();
});
},
onWebResourceError: (WebResourceError error) {
debugPrint('WebView error: ${error.description}');
},
onNavigationRequest: (NavigationRequest request) {
if (request.url.contains('blocked')) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已阻止访问该页面')),
);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
))
..addJavaScriptChannel(
'FlutterChannel',
onMessageReceived: (JavaScriptMessage message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message.message),
duration: const Duration(seconds: 3),
),
);
},
)
..loadFlutterAsset('assets/www/index.html');
}
Future<void> _goBack() async {
if (await _controller.canGoBack()) {
await _controller.goBack();
}
}
Future<void> _goForward() async {
if (await _controller.canGoForward()) {
await _controller.goForward();
}
}
Future<void> _reload() async {
await _controller.reload();
}
Future<void> _loadUrl(String url) async {
await _controller.loadRequest(Uri.parse(url));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('WebView 演示'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
IconButton(
icon: const Icon(Icons.home),
onPressed: () => _loadUrl('https://flutter.dev'),
tooltip: 'Flutter 官网',
),
PopupMenuButton<String>(
onSelected: (String value) => _loadUrl(value),
itemBuilder: (BuildContext context) => [
const PopupMenuItem(
value: 'https://flutter.dev',
child: Text('Flutter 官网'),
),
const PopupMenuItem(
value: 'https://pub.dev',
child: Text('Pub.dev'),
),
const PopupMenuItem(
value: 'https://github.com',
child: Text('GitHub'),
),
],
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(4),
child: LinearProgressIndicator(
value: _loadingProgress / 100,
backgroundColor: Colors.transparent,
),
),
),
body: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
color: Colors.grey.shade100,
child: Row(
children: [
Expanded(
child: Text(
_currentUrl.isEmpty ? '加载中...' : _currentUrl,
style: const TextStyle(fontSize: 12, color: Colors.grey),
overflow: TextOverflow.ellipsis,
),
),
Text(
'$_loadingProgress%',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
Expanded(
child: WebViewWidget(controller: _controller),
),
],
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: _canGoBack ? _goBack : null,
tooltip: '后退',
),
IconButton(
icon: const Icon(Icons.arrow_forward),
onPressed: _canGoForward ? _goForward : null,
tooltip: '前进',
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _reload,
tooltip: '刷新',
),
IconButton(
icon: const Icon(Icons.code),
onPressed: () {
_controller.runJavaScript('''
FlutterChannel.postMessage('执行了 JavaScript 代码');
''');
},
tooltip: '执行 JS',
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final String? title = await _controller.getTitle();
if (mounted && title != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('页面标题: $title')),
);
}
},
child: const Icon(Icons.title),
),
);
}
}