Flutter WebView 在 OpenHarmony 上的企业级网页集成方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

一、引言

WebView 是移动应用中常用的组件,用于在原生应用中集成网页内容。在现代应用开发中,WebView 不仅用于展示静态网页,还广泛应用于混合开发、H5 支付、在线客服、活动页面等场景。

本文将详细介绍如何在 Flutter-OH 项目中构建一套企业级的 WebView 集成方案,涵盖从基础配置、架构设计、JS 通信、安全机制,到 OpenHarmony 平台深度适配的完整技术体系。

二、WebView 技术架构设计

2.1 分层架构设计

我们采用分层架构设计,将 WebView 系统分为以下几个层次:

┌─────────────────────────────────────────┐
│     UI 层 (WebView Widgets)            │
├─────────────────────────────────────────┤
│     控制层 (WebViewController)         │
├─────────────────────────────────────────┤
│     通信层 (JSBridge)                  │
├─────────────────────────────────────────┤
│     业务层 (WebView Business Logic)   │
├─────────────────────────────────────────┤
│     安全层 (Security Manager)         │
└─────────────────────────────────────────┘

2.2 核心设计原则

  1. 安全性优先:完善的安全机制,防止 XSS 攻击
  2. 通信稳定:可靠的 JS-Native 双向通信
  3. 性能优化:优化 WebView 渲染性能
  4. 可扩展性:易于添加新的 JS 接口
  5. 鸿蒙化适配:针对 OpenHarmony 平台进行深度优化

三、项目配置与依赖

3.1 依赖配置

pubspec.yaml 中添加必要的依赖:

name: flutter_webview_harmony
description: Flutter for OpenHarmony WebView 集成方案
version: 1.0.0

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  webview_flutter: ^4.4.0
  webview_flutter_android: ^3.12.0
  webview_flutter_wkwebview: ^3.9.4
  url_launcher: ^6.2.2
  shared_preferences: ^2.2.2
  crypto: ^3.0.3
  path_provider: ^2.1.1
  connectivity_plus: ^5.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0
  mocktail: ^1.0.3

3.2 项目目录结构

lib/
├── core/
│   ├── constants/
│   │   └── webview_constants.dart
│   ├── services/
│   │   ├── webview_service.dart
│   │   └── js_bridge_service.dart
│   ├── utils/
│   │   ├── webview_cache_manager.dart
│   │   └── webview_cookie_manager.dart
│   └── exceptions/
│       └── webview_exceptions.dart
├── features/
│   └── webview/
│       ├── widgets/
│       │   ├── harmony_webview.dart
│       │   └── webview_navigation_bar.dart
│       ├── controllers/
│       │   └── harmony_webview_controller.dart
│       └── pages/
│           └── webview_page.dart
└── main.dart

四、核心组件实现

4.1 常量定义

创建 lib/core/constants/webview_constants.dart

class WebViewConstants {
  static const String defaultUserAgent =
      'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1';

  static const String jsBridgeName = 'FlutterBridge';

  static const Duration defaultTimeout = Duration(seconds: 30);
  static const int defaultCacheSize = 50 * 1024 * 1024; // 50MB

  static const List<String> allowedSchemes = [
    'http',
    'https',
    'file',
  ];

  static const List<String> safeDomains = [
    'your-domain.com',
    'your-cdn.com',
  ];

  static const List<String> blockedUrls = [
    'javascript:',
    'blob:',
  ];
}

class JsBridgeMethods {
  static const String getAppVersion = 'getAppVersion';
  static const String setTitle = 'setTitle';
  static const String showToast = 'showToast';
  static const String navigate = 'navigate';
  static const String getLocation = 'getLocation';
  static const String takePhoto = 'takePhoto';
  static const String chooseImage = 'chooseImage';
  static const String share = 'share';
  static const String pay = 'pay';
  static const String openNewWindow = 'openNewWindow';
  static const String closeWindow = 'closeWindow';
  static const String setStorage = 'setStorage';
  static const String getStorage = 'getStorage';
  static const String removeStorage = 'removeStorage';
  static const String clearStorage = 'clearStorage';
}

class WebViewErrorCodes {
  static const int unknown = -1;
  static const int hostLookup = 1;
  static const int connectionFailed = 2;
  static const int timeout = 3;
  static const int tooManyRequests = 4;
  static const int httpError = 5;
  static const int securityError = 6;
}

4.2 异常定义

创建 lib/core/exceptions/webview_exceptions.dart

class WebViewException implements Exception {
  final String message;
  final int? code;
  final Object? cause;
  final StackTrace? stackTrace;

  WebViewException(
    this.message, {
    this.code,
    this.cause,
    this.stackTrace,
  });

  
  String toString() {
    final buffer = StringBuffer('WebViewException: $message');
    if (code != null) {
      buffer.writeln('Code: $code');
    }
    if (cause != null) {
      buffer.writeln('Cause: $cause');
    }
    if (stackTrace != null) {
      buffer.writeln('StackTrace: $stackTrace');
    }
    return buffer.toString();
  }
}

class JsBridgeException extends WebViewException {
  final String? method;
  final Map<String, dynamic>? params;

  JsBridgeException(
    String message, {
    this.method,
    this.params,
    int? code,
    Object? cause,
    StackTrace? stackTrace,
  }) : super(
          message,
          code: code,
          cause: cause,
          stackTrace: stackTrace,
        );
}

class SecurityException extends WebViewException {
  final String? url;
  final String? reason;

  SecurityException(
    String message, {
    this.url,
    this.reason,
    int? code,
    Object? cause,
    StackTrace? stackTrace,
  }) : super(
          message,
          code: code,
          cause: cause,
          stackTrace: stackTrace,
        );
}

class NavigationBlockedException extends WebViewException {
  final String blockedUrl;
  final String? reason;

  NavigationBlockedException(
    this.blockedUrl, {
    this.reason,
    int? code,
    Object? cause,
    StackTrace? stackTrace,
  }) : super(
          'Navigation blocked: $blockedUrl',
          code: code,
          cause: cause,
          stackTrace: stackTrace,
        );
}

4.3 JS Bridge 核心实现

创建 lib/core/services/js_bridge_service.dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../constants/webview_constants.dart';
import '../exceptions/webview_exceptions.dart';

class JsBridgeService {
  static final JsBridgeService _instance = JsBridgeService._internal();
  static JsBridgeService get instance => _instance;

  JsBridgeService._internal();

  final Map<String, Function> _handlers = {};
  WebViewController? _controller;
  bool _isInitialized = false;

  Future<void> initialize(WebViewController controller) async {
    if (_isInitialized) return;

    _controller = controller;
    _registerDefaultHandlers();
    _isInitialized = true;
  }

  void dispose() {
    _controller = null;
    _handlers.clear();
    _isInitialized = false;
  }

  void _registerDefaultHandlers() {
    _handlers[JsBridgeMethods.getAppVersion] = _handleGetAppVersion;
    _handlers[JsBridgeMethods.setTitle] = _handleSetTitle;
    _handlers[JsBridgeMethods.showToast] = _handleShowToast;
    _handlers[JsBridgeMethods.navigate] = _handleNavigate;
    _handlers[JsBridgeMethods.openNewWindow] = _handleOpenNewWindow;
    _handlers[JsBridgeMethods.closeWindow] = _handleCloseWindow;
    _handlers[JsBridgeMethods.setStorage] = _handleSetStorage;
    _handlers[JsBridgeMethods.getStorage] = _handleGetStorage;
    _handlers[JsBridgeMethods.removeStorage] = _handleRemoveStorage;
    _handlers[JsBridgeMethods.clearStorage] = _handleClearStorage;
  }

  void registerHandler(String method, Function handler) {
    _handlers[method] = handler;
  }

  void unregisterHandler(String method) {
    _handlers.remove(method);
  }

  void handleMessage(JavaScriptMessage message) {
    try {
      final data = jsonDecode(message.message);
      final callbackId = data['callbackId'];
      final method = data['method'];
      final params = data['params'];

      final handler = _handlers[method];
      if (handler == null) {
        _sendError(callbackId, 'Method not found: $method');
        return;
      }

      _executeHandler(callbackId, method, params, handler);
    } catch (e, stackTrace) {
      if (e is FormatException) {
        debugPrint('Invalid JSON format: ${message.message}');
      } else {
        debugPrint('Error handling JS message: $e');
      }
    }
  }

  Future<void> _executeHandler(
    String? callbackId,
    String method,
    dynamic params,
    Function handler,
  ) async {
    try {
      final result = await _callHandler(handler, params);
      _sendSuccess(callbackId, result);
    } catch (e, stackTrace) {
      _sendError(
        callbackId,
        'Error executing $method: $e',
        JsBridgeException(
          'Error executing method',
          method: method,
          params: params,
          cause: e,
          stackTrace: stackTrace,
        ),
      );
    }
  }

  Future<dynamic> _callHandler(Function handler, dynamic params) async {
    if (params is Map) {
      return await handler(params);
    } else {
      return await handler(params);
    }
  }

  void _sendSuccess(String? callbackId, dynamic data) {
    if (callbackId == null || _controller == null) return;

    final result = jsonEncode({
      'code': 0,
      'data': data,
    });

    _executeJs('window.$callbackId.success($result)');
  }

  void _sendError(String? callbackId, String message, [Exception? error]) {
    if (callbackId == null || _controller == null) return;

    final result = jsonEncode({
      'code': -1,
      'message': message,
    });

    _executeJs('window.$callbackId.error($result)');
  }

  Future<dynamic> _executeJs(String script) async {
    if (_controller == null) {
      throw WebViewException('WebView not initialized');
    }

    return await _controller!.runJavaScriptReturningResult(script);
  }

  Future<dynamic> invokeMethod(
    String method, {
    Map<String, dynamic>? params,
  }) async {
    final script = '''
      (function() {
        var method = ${jsonEncode(method)};
        var params = ${jsonEncode(params ?? {})};
        if (window.$method && typeof window.$method === 'function') {
          return JSON.stringify({
            code: 0,
            data: window.$method(params)
          });
        } else {
          return JSON.stringify({
            code: -1,
            message: 'Method not found'
          });
        }
      })();
    ''';

    final result = await _executeJs(script);
    final jsonResult = jsonDecode(result.toString());

    if (jsonResult['code'] != 0) {
      throw JsBridgeException(
        jsonResult['message'] ?? 'Error invoking JS method',
        method: method,
        params: params,
      );
    }

    return jsonResult['data'];
  }

  Map<String, String> _getHeaders() {
    return {
      'User-Agent': WebViewConstants.defaultUserAgent,
      'X-Requested-With': 'FlutterWebView',
    };
  }

  bool _isUrlAllowed(String url) {
    try {
      final uri = Uri.parse(url);
      if (!WebViewConstants.allowedSchemes.contains(uri.scheme)) {
        return false;
      }

      final host = uri.host;
      for (final domain in WebViewConstants.safeDomains) {
        if (host.endsWith(domain)) {
          return true;
        }
      }

      return false;
    } catch (e) {
      return false;
    }
  }

  Future<Map<String, dynamic>> _handleGetAppVersion(
    Map<String, dynamic> params,
  ) async {
    return {
      'version': '1.0.0',
      'platform': 'OpenHarmony',
      'buildNumber': '100',
    };
  }

  Future<void> _handleSetTitle(Map<String, dynamic> params) async {
    final title = params['title'] as String?;
    if (title != null) {
      debugPrint('Setting title: $title');
    }
  }

  Future<void> _handleShowToast(Map<String, dynamic> params) async {
    final message = params['message'] as String?;
    final duration = params['duration'] as String?;

    if (message != null) {
      debugPrint('Toast: $message');
    }
  }

  Future<void> _handleNavigate(Map<String, dynamic> params) async {
    final url = params['url'] as String?;
    if (url != null && _controller != null) {
      await _controller!.loadRequest(Uri.parse(url));
    }
  }

  Future<void> _handleOpenNewWindow(Map<String, dynamic> params) async {
    final url = params['url'] as String?;
    if (url != null) {
      debugPrint('Opening new window: $url');
    }
  }

  Future<void> _handleCloseWindow(Map<String, dynamic> params) async {
    debugPrint('Closing window');
  }

  Future<void> _handleSetStorage(Map<String, dynamic> params) async {
    final key = params['key'] as String?;
    final value = params['value'];

    if (key != null && value != null) {
      debugPrint('Setting storage: $key = $value');
    }
  }

  Future<dynamic> _handleGetStorage(Map<String, dynamic> params) async {
    final key = params['key'] as String?;
    return key != null ? {} : null;
  }

  Future<void> _handleRemoveStorage(Map<String, dynamic> params) async {
    final key = params['key'] as String?;
    if (key != null) {
      debugPrint('Removing storage: $key');
    }
  }

  Future<void> _handleClearStorage(Map<String, dynamic> params) async {
    debugPrint('Clearing storage');
  }
}

4.4 WebView 服务实现

创建 lib/core/services/webview_service.dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import '../constants/webview_constants.dart';
import '../exceptions/webview_exceptions.dart';
import 'js_bridge_service.dart';

class WebViewService {
  static final WebViewService _instance = WebViewService._internal();
  static WebViewService get instance => _instance;

  WebViewService._internal();

  WebViewController? _controller;
  final JsBridgeService _jsBridge = JsBridgeService.instance;
  bool _isLoading = false;
  String? _currentUrl;

  Future<void> initialize(WebViewController controller) async {
    _controller = controller;
    await _configureWebView();
    await _jsBridge.initialize(controller);
  }

  Future<void> _configureWebView() async {
    await _controller!.setJavaScriptMode(JavaScriptMode.unrestricted);
    await _controller!.setNavigationDelegate(
      NavigationDelegate(
        onProgress: _onProgress,
        onPageStarted: _onPageStarted,
        onPageFinished: _onPageFinished,
        onWebResourceError: _onWebResourceError,
        onNavigationRequest: _onNavigationRequest,
        onUrlChange: _onUrlChange,
      ),
    );
    await _controller!.addJavaScriptChannel(
      WebViewConstants.jsBridgeName,
      onMessageReceived: _jsBridge.handleMessage,
    );
    await _controller!.setUserAgent(WebViewConstants.defaultUserAgent);
  }

  void _onProgress(int progress) {
    debugPrint('WebView progress: $progress%');
  }

  Future<void> _onPageStarted(String url) async {
    _isLoading = true;
    _currentUrl = url;
    debugPrint('Page started: $url');
  }

  Future<void> _onPageFinished(String url) async {
    _isLoading = false;
    _currentUrl = url;
    debugPrint('Page finished: $url');
    await _injectJavascript();
  }

  Future<void> _injectJavascript() async {
    const injectionScript = '''
      (function() {
        if (window.__flutterBridgeInjected__) {
          return;
        }
        window.__flutterBridgeInjected__ = true;

        window.$WebViewConstants.jsBridgeName = Object.assign(
          window.$WebViewConstants.jsBridgeName || {},
          {
            invoke: function(method, params) {
              return new Promise(function(resolve, reject) {
                var callbackId = 'callback_' + Date.now() + '_' + Math.random();
                window[callbackId] = {
                  success: function(result) {
                    try {
                      var data = JSON.parse(result);
                      if (data.code === 0) {
                        resolve(data.data);
                      } else {
                        reject(new Error(data.message));
                      }
                    } catch (e) {
                      reject(e);
                    } finally {
                      delete window[callbackId];
                    }
                  },
                  error: function(error) {
                    try {
                      var err = JSON.parse(error);
                      reject(new Error(err.message));
                    } catch (e) {
                      reject(error);
                    } finally {
                      delete window[callbackId];
                    }
                  }
                };

                var message = JSON.stringify({
                  callbackId: callbackId,
                  method: method,
                  params: params
                });

                try {
                  window.$WebViewConstants.jsBridgeName.postMessage(message);
                } catch (e) {
                  reject(e);
                  delete window[callbackId];
                }
              });
            }
          }
        );
      })();
    ''';

    await _controller!.runJavaScript(injectionScript);
  }

  void _onWebResourceError(WebResourceError error) {
    debugPrint('WebResourceError: ${error.errorCode} - ${error.description}');
  }

  Future<NavigationDecision> _onNavigationRequest(
    NavigationRequest request,
  ) async {
    final url = request.url;

    if (_shouldOpenExternal(url)) {
      await launchUrl(Uri.parse(url));
      return NavigationDecision.prevent;
    }

    if (!_isNavigationAllowed(url)) {
      return NavigationDecision.prevent;
    }

    return NavigationDecision.navigate;
  }

  bool _shouldOpenExternal(String url) {
    final uri = Uri.parse(url);

    if (['tel', 'mailto', 'sms', 'geo', 'market'].contains(uri.scheme)) {
      return true;
    }

    return false;
  }

  bool _isNavigationAllowed(String url) {
    try {
      final uri = Uri.parse(url);

      for (final blockedUrl in WebViewConstants.blockedUrls) {
        if (url.startsWith(blockedUrl)) {
          return false;
        }
      }

      return true;
    } catch (e) {
      return false;
    }
  }

  void _onUrlChange(UrlChange change) {
    _currentUrl = change.url;
    debugPrint('URL changed: ${change.url}');
  }

  Future<void> loadUrl(String url, {Map<String, String>? headers}) async {
    if (_controller == null) {
      throw WebViewException('WebView not initialized');
    }

    await _controller!.loadRequest(
      Uri.parse(url),
      headers: headers ?? {},
    );
  }

  Future<void> reload() async {
    if (_controller == null) {
      throw WebViewException('WebView not initialized');
    }

    await _controller!.reload();
  }

  Future<void> goBack() async {
    if (_controller == null) {
      throw WebViewException('WebView not initialized');
    }

    if (await _controller!.canGoBack()) {
      await _controller!.goBack();
    }
  }

  Future<void> goForward() async {
    if (_controller == null) {
      throw WebViewException('WebView not initialized');
    }

    if (await _controller!.canGoForward()) {
      await _controller!.goForward();
    }
  }

  Future<void> clearCache() async {
    if (_controller == null) {
      throw WebViewException('WebView not initialized');
    }

    await _controller!.clearCache();
    await _controller!.clearLocalStorage();
  }

  Future<void> clearCookies() async {
    final cookieManager = WebViewCookieManager();
    await cookieManager.clearCookies();
  }

  Future<String?> getCurrentUrl() async {
    return _currentUrl;
  }

  bool get isLoading => _isLoading;

  void dispose() {
    _controller = null;
    _jsBridge.dispose();
  }
}

五、鸿蒙化适配与实战案例

5.1 鸿蒙平台权限配置

module.json5 中添加必要的权限:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone",
      "tablet"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      },
      {
        "name": "ohos.permission.READ_MEDIA"
      },
      {
        "name": "ohos.permission.CAMERA"
      },
      {
        "name": "ohos.permission.MICROPHONE"
      },
      {
        "name": "ohos.permission.LOCATION"
      }
    ]
  }
}

5.2 Harmony WebView Widget 实现

创建 lib/features/webview/widgets/harmony_webview.dart

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../../core/services/webview_service.dart';
import '../../../core/constants/webview_constants.dart';

class HarmonyWebView extends StatefulWidget {
  final String initialUrl;
  final String? title;
  final bool showNavigationBar;
  final bool showProgressBar;
  final bool enableJavaScript;
  final Map<String, String>? headers;
  final Function(String)? onUrlChanged;
  final Function(Map<String, dynamic>)? onJsMessage;

  const HarmonyWebView({
    super.key,
    required this.initialUrl,
    this.title,
    this.showNavigationBar = true,
    this.showProgressBar = true,
    this.enableJavaScript = true,
    this.headers,
    this.onUrlChanged,
    this.onJsMessage,
  });

  
  State<HarmonyWebView> createState() => _HarmonyWebViewState();
}

class _HarmonyWebViewState extends State<HarmonyWebView> {
  late final WebViewController _controller;
  final WebViewService _webViewService = WebViewService.instance;
  final ValueNotifier<bool> _isLoading = ValueNotifier(true);
  final ValueNotifier<int> _progress = ValueNotifier(0);
  final ValueNotifier<bool> _canGoBack = ValueNotifier(false);
  final ValueNotifier<bool> _canGoForward = ValueNotifier(false);
  String? _currentTitle;

  
  void initState() {
    super.initState();
    _initWebView();
  }

  Future<void> _initWebView() async {
    _controller = WebViewController();

    await _controller.setJavaScriptMode(JavaScriptMode.unrestricted);
    await _controller.setNavigationDelegate(
      NavigationDelegate(
        onProgress: (progress) {
          _progress.value = progress;
        },
        onPageStarted: (url) {
          _isLoading.value = true;
          widget.onUrlChanged?.call(url);
        },
        onPageFinished: (url) async {
          _isLoading.value = false;
          await _updateNavigationState();
          await _updateTitle();
        },
        onWebResourceError: (error) {
          _isLoading.value = false;
        },
        onNavigationRequest: (request) async {
          if (_shouldOpenExternal(request.url)) {
            return NavigationDecision.prevent;
          }
          return NavigationDecision.navigate;
        },
        onUrlChange: (change) {
          widget.onUrlChanged?.call(change.url ?? '');
        },
      ),
    );

    await _webViewService.initialize(_controller);

    if (widget.headers != null && widget.headers!.isNotEmpty) {
      await _controller.loadRequest(
        Uri.parse(widget.initialUrl),
        headers: widget.headers,
      );
    } else {
      await _controller.loadRequest(Uri.parse(widget.initialUrl));
    }
  }

  bool _shouldOpenExternal(String url) {
    final uri = Uri.parse(url);
    return ['tel', 'mailto', 'sms'].contains(uri.scheme);
  }

  Future<void> _updateNavigationState() async {
    _canGoBack.value = await _controller.canGoBack();
    _canGoForward.value = await _controller.canGoForward();
  }

  Future<void> _updateTitle() async {
    final title = await _controller.getTitle();
    if (mounted) {
      setState(() {
        _currentTitle = title;
      });
    }
  }

  
  void dispose() {
    _webViewService.dispose();
    _isLoading.dispose();
    _progress.dispose();
    _canGoBack.dispose();
    _canGoForward.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: widget.showNavigationBar
          ? AppBar(
              title: Text(widget.title ?? _currentTitle ?? 'WebView'),
              actions: [
                ValueListenableBuilder<bool>(
                  valueListenable: _canGoBack,
                  builder: (context, canGoBack, child) {
                    return IconButton(
                      icon: const Icon(Icons.arrow_back),
                      onPressed: canGoBack ? () => _controller.goBack() : null,
                    );
                  },
                ),
                ValueListenableBuilder<bool>(
                  valueListenable: _canGoForward,
                  builder: (context, canGoForward, child) {
                    return IconButton(
                      icon: const Icon(Icons.arrow_forward),
                      onPressed: canGoForward ? () => _controller.goForward() : null,
                    );
                  },
                ),
                IconButton(
                  icon: const Icon(Icons.refresh),
                  onPressed: () => _controller.reload(),
                ),
              ],
            )
          : null,
      body: Column(
        children: [
          if (widget.showProgressBar)
            ValueListenableBuilder<int>(
              valueListenable: _progress,
              builder: (context, progress, child) {
                if (progress == 0 || progress == 100) {
                  return const SizedBox.shrink();
                }
                return LinearProgressIndicator(value: progress / 100);
              },
            ),
          Expanded(
            child: WebViewWidget(controller: _controller),
          ),
        ],
      ),
    );
  }
}

5.3 H5 支付集成实战

创建 H5 支付集成方案:

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class PaymentWebView extends StatefulWidget {
  final String paymentUrl;
  final String? callbackUrl;
  final Function(bool)? onPaymentResult;

  const PaymentWebView({
    super.key,
    required this.paymentUrl,
    this.callbackUrl,
    this.onPaymentResult,
  });

  
  State<PaymentWebView> createState() => _PaymentWebViewState();
}

class _PaymentWebViewState extends State<PaymentWebView> {
  late final WebViewController _controller;

  
  void initState() {
    super.initState();
    _initWebView();
  }

  Future<void> _initWebView() async {
    _controller = WebViewController();

    await _controller.setJavaScriptMode(JavaScriptMode.unrestricted);
    await _controller.setNavigationDelegate(
      NavigationDelegate(
        onProgress: (progress) => debugPrint('Payment progress: $progress%'),
        onPageStarted: (url) => debugPrint('Payment page started: $url'),
        onPageFinished: (url) => debugPrint('Payment page finished: $url'),
        onNavigationRequest: (request) {
          if (widget.callbackUrl != null &&
              request.url.startsWith(widget.callbackUrl!)) {
            final uri = Uri.parse(request.url);
            final success = uri.queryParameters['success'] == 'true';
            widget.onPaymentResult?.call(success);
            return NavigationDecision.prevent;
          }

          if (request.url.startsWith('weixin://') ||
              request.url.startsWith('alipay://') ||
              request.url.startsWith('alipays://')) {
            return NavigationDecision.prevent;
          }

          return NavigationDecision.navigate;
        },
      ),
    );

    await _controller.loadRequest(Uri.parse(widget.paymentUrl));
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('支付')),
      body: WebViewWidget(controller: _controller),
    );
  }
}

六、安全机制与性能优化

6.1 安全防护措施

  1. XSS 防护:对 JS 通信进行安全验证
  2. URL 白名单:只允许访问受信任的域名
  3. Cookie 管理:安全处理 cookies
  4. 内容安全策略:使用 CSP 限制资源加载
  5. https 强制:默认只允许 https 访问

6.2 性能优化策略

  1. 缓存优化:合理配置缓存策略
  2. 资源预加载:提前加载常用资源
  3. 图片优化:使用 WebP 格式和懒加载
  4. JS 优化:减少 JS 执行阻塞
  5. DOM 优化:优化 DOM 操作

七、测试策略

7.1 单元测试

import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import '../lib/core/services/js_bridge_service.dart';
import '../lib/core/exceptions/webview_exceptions.dart';

class MockWebViewController extends Mock implements WebViewController {}

void main() {
  group('JsBridgeService', () {
    late JsBridgeService jsBridge;
    late MockWebViewController mockController;

    setUp(() {
      jsBridge = JsBridgeService();
      mockController = MockWebViewController();
    });

    test('should initialize successfully', () async {
      await expectLater(
        jsBridge.initialize(mockController),
        completes,
      );
    });

    test('should register and invoke handler', () async {
      await jsBridge.initialize(mockController);

      var invoked = false;
      jsBridge.registerHandler('testMethod', (params) async {
        invoked = true;
        return {'success': true};
      });

      expect(invoked, false);
    });
  });
}

八、总结

本文详细介绍了 Flutter WebView 在 OpenHarmony 上的企业级网页集成方案。通过完善的架构设计、安全的 JS 通信机制和深度的平台适配,可以在 OpenHarmony 平台上构建稳定、安全、高性能的 WebView 应用。

核心要点:

  1. 使用 webview_flutter 实现 WebView 组件
  2. 构建可靠的 JSBridge 双向通信机制
  3. 针对 OpenHarmony 平台进行权限配置
  4. 实现完善的安全防护机制
  5. 提供 H5 支付等实战场景方案
  6. 优化 WebView 性能和用户体验

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐