《Flutter全栈开发实战指南:从零到高级》- 23 -混合开发与WebView

混合开发为何如此重要?

在实际项目中,我们常面临这样的困境:业务需要快速迭代,但原生发版周期长;H5页面体验不佳,但开发速度快。混合开发正是解决这一矛盾的最佳平衡点

graph TD A[业务需求] --> B{开发方案选择} B --> C[原生开发] B --> D[Web开发] B --> E[混合开发] C --> F[优势: 性能最佳] C --> G[劣势: 迭代慢, 双端开发] D --> H[优势: 跨平台, 热更新] D --> I[劣势: 体验差, 能力受限] E --> J[融合两者优势] E --> K[平衡性能与效率] J --> L[原生体验 + Web灵活性] K --> M[快速迭代 + 一致体验]

一:WebView核心原理

1.1 WebView的本质是什么?

很多人以为WebView只是一个内置浏览器,其实远不止如此。WebView实际上是一个微型浏览器,它包含了HTML解析器、CSS渲染器、JavaScript引擎等完整组件。

graph TB subgraph "WebView内部架构" A[WebView容器] --> B[渲染引擎] A --> C[JavaScript引擎] A --> D[网络模块] B --> E[HTML解析器] B --> F[CSS渲染器] B --> G[布局引擎] C --> H[V8/JSCore引擎] D --> I[网络请求处理] end subgraph "Flutter侧" J[Dart VM] --> K[Flutter Engine] K --> L[Skia渲染] end A -.-> K C -.-> J

WebView和Flutter运行在不同的隔离环境中:

  • Flutter:运行在Dart VM,使用Skia渲染
  • WebView:运行在浏览器引擎中,有自己的渲染管线

1.2 Flutter中的WebView实现原理

Flutter的WebView并不是自己实现的浏览器引擎,而是对原生WebView的桥接封装

Platform Channels工作原理

dart 复制代码
// Flutter调用原生方法的流程
1. Dart代码调用WebViewController的方法
2. 通过MethodChannel将二进制消息发送到原生端
3. 原生端调用对应的WebView API
4. 结果通过MethodChannel返回Dart

二:封装WebView

2.1 基础封装

先看一个在实际项目中使用的WebView封装,这个版本已经处理了大部分常见问题:

dart 复制代码
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class TestWebView extends StatefulWidget {
  final String url;
  final Map<String, String>? headers;
  
  const TestWebView({
    Key? key,
    required this.url,
    this.headers,
  }) : super(key: key);
  
  @override
  _TestWebViewState createState() => _TestWebViewState();
}

class _TestWebViewState extends State<TestWebView> {
  // 控制器
  late WebViewController _controller;
  
  // 状态管理
  double _progress = 0.0;
  bool _isLoading = true;
  bool _hasError = false;
  String? _pageTitle;
  
  @override
  void initState() {
    super.initState();
    _initWebView();
  }
  
  void _initWebView() {
    _controller = WebViewController()
      // 1. 基础配置
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(Colors.transparent)
      
      // 2. 注册JavaScript通信通道
      ..addJavaScriptChannel(
        'FlutterBridge',
        onMessageReceived: (message) {
          _handleJavaScriptMessage(message.message);
        },
      )
      
      // 3. 导航
      ..setNavigationDelegate(
        NavigationDelegate(
          onPageStarted: (url) {
            setState(() {
              _progress = 0.0;
              _isLoading = true;
              _hasError = false;
            });
          },
          
          onProgress: (progress) {
            setState(() => _progress = progress / 100.0);
          },
          
          onPageFinished: (url) async {
            // 获取页面标题
            final title = await _controller.getTitle();
            setState(() {
              _pageTitle = title;
              _isLoading = false;
            });
            
            // 注入自定义脚本
            await _injectCustomScripts();
          },
          
          // URL拦截
          onNavigationRequest: (request) {
            return _handleNavigation(request);
          },
        ),
      )
      
      // 4. 加载页面
      ..loadRequest(
        Uri.parse(widget.url),
        headers: widget.headers ?? {},
      );
  }
  
  // 处理JS消息
  void _handleJavaScriptMessage(String message) {
    try {
      final data = jsonDecode(message);
      final type = data['type'];
      final payload = data['data'];
      
      switch (type) {
        case 'userAction':
          _handleUserAction(payload);
          break;
        case 'getUserInfo':
          _sendUserInfoToWeb();
          break;
      }
    } catch (e) {
      print('JS消息解析失败: $e');
    }
  }
  
  // URL导航处理逻辑
  NavigationDecision _handleNavigation(NavigationRequest request) {
    final url = request.url;
    
    // 白名单
    if (!_isUrlInWhitelist(url)) {
      return NavigationDecision.prevent;
    }
    
    // 链接处理
    if (url.startsWith('myapp://')) {
      _handleDeepLink(url);
      return NavigationDecision.prevent;
    }
    
    return NavigationDecision.navigate;
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _buildAppBar(),
      body: Stack(
        children: [
          // WebView主体
          WebViewWidget(controller: _controller),
          
          // 进度条
          if (_isLoading && _progress < 1.0)
            LinearProgressIndicator(
              value: _progress,
              backgroundColor: Colors.grey[200],
            ),
          
          // 错误状态
          if (_hasError)
            _buildErrorWidget(),
        ],
      ),
      // 底部导航栏
      bottomNavigationBar: _buildBottomBar(),
    );
  }
}

2.2 核心功能点

2.2.1 JavaScript通信原理

JavaScript与Flutter的通信是通过桥接实现的:

sequenceDiagram participant W as WebView(JS环境) participant B as JavaScriptChannel participant F as Flutter(Dart环境) participant H as 消息处理器 W->>B: window.FlutterBridge.postMessage(JSON) B->>F: 通过Platform Channel传递消息 F->>H: 解析并处理消息 H->>F: 返回处理结果 F->>W: _controller.runJavaScript()

核心技术点

  • 消息序列化:所有数据必须转为JSON串
  • 异步处理:异步通信并处理回调
  • 错误处理:JS或Flutter都可能出错,需要有错误处理

2.2.2 性能优化

dart 复制代码
class WebViewOptimizer {
  // 1. 缓存
  static void setupCache(WebViewController controller) async {
    await controller.runJavaScript('''
      // 启用Service Worker缓存
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('/sw.js');
      }
      
      // 本地存储
      if (window.localStorage) {
        localStorage.setItem('lastVisit', new Date().toISOString());
      }
    ''');
  }
  
  // 2. 内存管理
  static void manageMemory(WebViewController controller) {
    // 清理缓存
    Timer.periodic(Duration(minutes: 5), (_) {
      controller.clearCache();
    });
  }
  
  // 3. 预加载
  static Future<void> preloadWebView({
    required String url,
    required BuildContext context,
  }) async {
    // 提前初始化WebView但不显示
    final controller = WebViewController();
    await controller.loadRequest(Uri.parse(url));
    
    // 保存到全局缓存
    WebViewCache.instance.cache(url, controller);
  }
}

三:混合应用设计

3.1 分层架构

一个良好的混合架构应该分为四个层次:

graph TB subgraph "1.表现层 Presentation Layer" A1[Flutter原生页面] A2[WebView容器] A3[混合页面] end subgraph "2.桥接层 Bridge Layer" B1[JavaScript Bridge] B2[消息路由器] B3[协议编解码器] end subgraph "3.业务层 Business Layer" C1[用户服务] C2[支付服务] C3[数据服务] end subgraph "4.基础设施层 Infrastructure" D1[WebView池] D2[缓存管理器] D3[网络层] D4[安全模块] end A1 --> B1 A2 --> B1 A3 --> B1 B1 --> B2 B2 --> C1 B2 --> C2 B2 --> C3 C1 --> D4 C2 --> D3 C3 --> D2 A2 --> D1

3.2 路由管理

混合应用最复杂的是路由管理。我们需要决定什么时候用原生页面,什么时候用WebView。

dart 复制代码
class HybridRouter {
  static final Map<String, RouteConfig> _routeTable = {
    '/home': RouteConfig(
      type: RouteType.native,
      path: '/home',
      webUrl: null,
    ),
    '/product/:id': RouteConfig(
      type: RouteType.hybrid,
      path: '/product/:id',
      webUrl: 'https://api.xxxx.com/product/{id}',
      nativeFallback: '/productDetail',
    ),
    '/promotion/:code': RouteConfig(
      type: RouteType.web,
      path: '/promotion/:code',
      webUrl: 'https://promo.xxxx.com/{code}',
    ),
  };
  
  // 路由
  static Future<void> navigateTo({
    required BuildContext context,
    required String path,
    Map<String, dynamic>? params,
  }) async {
    final config = _findRouteConfig(path);
    
    if (config == null) {
      // WebView
      await _openWebView(context, path, params);
      return;
    }
    
    final useWeb = await _shouldUseWebVersion(config);
    
    if (useWeb) {
      await _openWebView(context, config.webUrl!, params);
    } else {
      await _openNativePage(context, config.nativeFallback!, params);
    }
  }
  
  // 条件
  static Future<bool> _shouldUseWebVersion(RouteConfig config) async {
    // 1. 检查网络状况
    final connectivity = await Connectivity().checkConnectivity();
    if (connectivity == ConnectivityResult.none) {
      return false; // 离线时用原生
    }
    
    // 2. 检查用户偏好
    final prefs = await SharedPreferences.getInstance();
    final preferNative = prefs.getBool('prefer_native') ?? false;
    
    // 3. 检查页面类型
    switch (config.type) {
      case RouteType.native:
        return false;
      case RouteType.web:
        return true;
      case RouteType.hybrid:
        return await _businessDecision(config);
    }
  }
}

3.3 状态管理

混合应用的状态管理比纯原生应用更复杂,因为状态可能在三个地方:

markdown 复制代码
状态存储位置:
1. Flutter/Dart状态
2. WebView/JavaScript状态  
3. 原生平台状态(iOS/Android)

实现方案:

dart 复制代码
class HybridStateManager {
  // 状态存储
  final Map<String, dynamic> _globalState = {};
  
  // 状态同步方法
  Future<void> syncStateToWeb(WebViewController controller) async {
    final stateJson = jsonEncode(_globalState);
    await controller.runJavaScript('''
      // 更新Web端状态
      window.appState = $stateJson;
      
      // 触发状态更新事件
      window.dispatchEvent(new CustomEvent('appStateChanged', {
        detail: $stateJson
      }));
    ''');
  }
  
  // 从Web接收状态更新
  void handleStateFromWeb(Map<String, dynamic> newState) {
    _globalState.addAll(newState);
    
    // 通知Flutter组件
    _stateNotifier.value = {..._globalState};
    
    // 持久化
    _persistState();
  }
}

四:通信协议

4.1 消息协议

良好的通信从定义协议开始,实际项目中使用的协议规范,如下:

dart 复制代码
// 定义消息协议
class BridgeMessage {
  final String id;           // 消息ID
  final String type;         // 消息类型
  final String method;       // 方法名
  final dynamic data;        // 消息数据
  final int timestamp;       // 时间戳
  final String? callbackId;  // 回调ID
  
  // 消息类型
  static const String TYPE_REQUEST = 'request';
  static const String TYPE_RESPONSE = 'response';
  static const String TYPE_EVENT = 'event';
  
  // 常用方法
  static const String METHOD_GET_USER_INFO = 'getUserInfo';
  static const String METHOD_PAYMENT = 'startPayment';
  static const String METHOD_SHARE = 'shareContent';
  
  // 序列化
  String toJson() {
    return jsonEncode({
      'id': id,
      'type': type,
      'method': method,
      'data': data,
      'timestamp': timestamp,
      'callbackId': callbackId,
    });
  }
  
  // 反序列化
  static BridgeMessage fromJson(String jsonStr) {
    final map = jsonDecode(jsonStr);
    return BridgeMessage(
      id: map['id'],
      type: map['type'],
      method: map['method'],
      data: map['data'],
      timestamp: map['timestamp'],
      callbackId: map['callbackId'],
    );
  }
}

4.2 通信流程

sequenceDiagram participant H as H5页面(JS) participant B as JavaScript Bridge participant D as Dart消息分发器 participant S as 业务服务 participant N as 原生功能 H->>B: 发送请求<br/>BridgeMessage Note over H,B: 1. 用户点击购买按钮 B->>D: 通过Channel传递 Note over B,D: 2. 平台通道传输 D->>D: 解析验证消息 Note over D: 3. 安全检查与验证 alt 需要原生功能 D->>N: 调用原生模块 N->>D: 返回结果 else 需要业务服务 D->>S: 调用业务服务 S->>D: 返回业务数据 end D->>B: 构造响应消息 B->>H: 返回结果 Note over B,H: 6. 更新H5页面状态

4.3 错误处理

dart 复制代码
class BridgeErrorHandler {
  // 定义错误码
  static const Map<int, String> errorCodes = {
    1001: '网络连接失败',
    1002: '用户未登录',
    1003: '参数验证失败',
    1004: '权限不足',
    1005: '服务端错误',
  };
  
  // 统一错误处理
  static BridgeMessage handleError(
    dynamic error, 
    String messageId,
    String method,
  ) {
    int code = 1005; // 默认错误码
    String message = '未知错误';
    
    if (error is PlatformException) {
      code = int.parse(error.code);
      message = error.message ?? '平台异常';
    } else if (error is HttpException) {
      code = 1001;
      message = '网络请求失败';
    }
    
    return BridgeMessage(
      id: messageId,
      type: BridgeMessage.TYPE_RESPONSE,
      method: method,
      data: {
        'success': false,
        'error': {
          'code': code,
          'message': errorCodes[code] ?? message,
          'detail': error.toString(),
        },
      },
      timestamp: DateTime.now().millisecondsSinceEpoch,
    );
  }
}

五:性能优化

5.1 WebView启动优化

WebView首次启动慢是常见问题。我们可以通过预加载和复用来优化:

dart 复制代码
// 实现WebView池
class WebViewPool {
  static final Map<String, WebViewController> _pool = {};
  static final Map<String, DateTime> _lastUsed = {};
  
  // 获取WebView
  static Future<WebViewController> getWebView({
    required String key,
    required Future<WebViewController> Function() builder,
  }) async {
    // 1. 检查池中是否有可复用的
    if (_pool.containsKey(key)) {
      _lastUsed[key] = DateTime.now();
      return _pool[key]!;
    }
    
    // 2. 创建新的WebView
    final controller = await builder();
    _pool[key] = controller;
    _lastUsed[key] = DateTime.now();
    
    // 3. 清理过期缓存
    _cleanup();
    
    return controller;
  }
  
  // 预加载
  static Future<void> preload(List<String> urls) async {
    for (final url in urls) {
      final controller = WebViewController();
      await controller.loadRequest(Uri.parse(url));
      _pool[url] = controller;
    }
  }
}

5.2 内存管理

WebView是内存消耗大户,需要精细管理:

dart 复制代码
class WebViewMemoryManager {
  // 处理内存压力
  static void setupMemoryPressureHandler() {
    SystemChannels.lifecycle.setMessageHandler((msg) async {
      if (msg == AppLifecycleState.paused.toString()) {
        // App进入后台,释放WebView内存
        await _releaseWebViewMemory();
      } else if (msg == AppLifecycleState.resumed.toString()) {
        // App回到前台,恢复必要状态
        await _restoreWebViewState();
      }
      return null;
    });
  }
  
  static Future<void> _releaseWebViewMemory() async {
    // 1. 清除缓存
    for (final controller in WebViewPool._pool.values) {
      await controller.clearCache();
    }
    
    // 2. 卸载不活动的WebView
    final now = DateTime.now();
    WebViewPool._pool.entries
      .where((entry) {
        final lastUsed = WebViewPool._lastUsed[entry.key];
        return lastUsed != null && 
               now.difference(lastUsed) > Duration(minutes: 10);
      })
      .forEach((entry) {
        WebViewPool._pool.remove(entry.key);
        WebViewPool._lastUsed.remove(entry.key);
      });
  }
}

5.3 渲染性能优化

dart 复制代码
class WebViewPerformance {
  // 启用硬件加速
  static void enableHardwareAcceleration(WebViewController controller) {
    controller.runJavaScript('''
      // 启用CSS硬件加速
      const style = document.createElement('style');
      style.textContent = \`
        .animate-element {
          transform: translateZ(0);
          will-change: transform;
        }
        .fixed-element {
          position: fixed;
          backface-visibility: hidden;
        }
      \`;
      document.head.appendChild(style);
    ''');
  }
  
  // 监控性能指标
  static void setupPerformanceMonitor(WebViewController controller) {
    controller.runJavaScript('''
      // 使用Performance API监控
      const observer = new PerformanceObserver((list) => {
        const entries = list.getEntries();
        entries.forEach(entry => {
          if (entry.duration > 100) { 
            console.warn('长任务:', entry.name, entry.duration);
            
            // 发送到Flutter监控
            if (window.FlutterBridge) {
              window.FlutterBridge.postMessage(JSON.stringify({
                type: 'performance',
                data: {
                  metric: 'long_task',
                  name: entry.name,
                  duration: entry.duration,
                  timestamp: Date.now()
                }
              }));
            }
          }
        });
      });
      
      observer.observe({entryTypes: ['longtask']});
    ''');
  }
}

六:安全防护

6.1 多层安全防护

graph TD subgraph "安全防护体系" A[输入层防护] --> B[通信层防护] B --> C[执行层防护] C --> D[数据层防护] end subgraph "具体措施" A1[URL白名单验证] A2[输入参数过滤] B1[HTTPS强制] B2[消息签名] B3[防重放攻击] C1[JS沙盒隔离] C2[权限最小化] D1[数据加密] D2[本地存储安全] end A1 --> A A2 --> A B1 --> B B2 --> B B3 --> B C1 --> C C2 --> C D1 --> D D2 --> D

6.2 具体实现

dart 复制代码
class WebViewSecurity {
  // 验证URL白名单
  static final List<RegExp> _urlWhitelist = [
    RegExp(r'^https://api\.xxxx\.com/'),
    RegExp(r'^https://cdn\.xxxx\.com/'),
    RegExp(r'^https://sso\.xxxx\.com/'),
  ];
  
  static bool isUrlAllowed(String url) {
    return _urlWhitelist.any((pattern) => pattern.hasMatch(url));
  }
  
  // 验证消息签名
  static bool verifyMessageSignature(
    Map<String, dynamic> message,
    String signature,
  ) {
    // 1. 检查时间戳
    final timestamp = message['timestamp'];
    final now = DateTime.now().millisecondsSinceEpoch;
    if ((now - timestamp).abs() > 300000) { // 5分钟有效期
      return false;
    }
    
    // 2. 验证签名
    final secretKey = 'your_secret_key_here';
    final dataToSign = '${message['id']}:${timestamp}:$secretKey';
    final expectedSig = sha256.convert(utf8.encode(dataToSign)).toString();
    
    return expectedSig == signature;
  }
  
  // 防XSS注入
  static String sanitizeInput(String input) {
    // 移除危险标签和属性
    return input
        .replaceAll(RegExp(r'<script[^>]*>.*?</script>', caseSensitive: false), '')
        .replaceAll(RegExp(r'on\w+="[^"]*"', caseSensitive: false), '')
        .replaceAll(RegExp(r'javascript:', caseSensitive: false), '')
        .replaceAll(RegExp(r'data:', caseSensitive: false), '');
  }
}

6.3 Content Security Policy

dart 复制代码
Future<void> setupContentSecurityPolicy(WebViewController controller) async {
  await controller.runJavaScript('''
    // 添加CSP Meta标签
    const cspMeta = document.createElement('meta');
    cspMeta.httpEquiv = 'Content-Security-Policy';
    cspMeta.content = \`
      default-src 'self' https://api.xxxx.com;
      script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.xxxx.com;
      style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
      img-src 'self' data: https:;
      font-src 'self' https://fonts.gstatic.com;
      connect-src 'self' https://api.xxxx.com wss://ws.xxxx.com;
      frame-ancestors 'self';
      form-action 'self' https://sso.xxxx.com;
    \`;
    
    document.head.appendChild(cspMeta);
    
    // 禁用危险API
    Object.defineProperty(window, 'eval', {
      value: function() {
        console.warn('eval() is disabled for security reasons');
        return null;
      }
    });
    
    // 监控可疑行为
    const originalPostMessage = window.postMessage;
    window.postMessage = function(message, targetOrigin) {
      if (!targetOrigin || targetOrigin === '*') {
        console.warn('postMessage without targetOrigin is restricted');
        return;
      }
      return originalPostMessage.call(this, message, targetOrigin);
    };
  ''');
}

七:调试

7.1 集成调试工具

dart 复制代码
class WebViewDebugger {
  // 启用远程调试
  static void enableRemoteDebugging(WebViewController controller) {
    // Android: Chrome DevTools
    // iOS: Safari Web Inspector
    
    controller.runJavaScript('''
      console.log = function(...args) {
        // 重定向console到Flutter
        const message = args.map(arg => 
          typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
        ).join(' ');
        
        if (window.FlutterBridge) {
          window.FlutterBridge.postMessage(JSON.stringify({
            type: 'console',
            level: 'log',
            message: message,
            timestamp: Date.now()
          }));
        }
        
        // 保留原始console功能
        originalConsoleLog.apply(console, args);
      };
      
      const originalConsoleLog = console.log;
    ''');
  }
}

7.2 监控错误与上报

dart 复制代码
class WebViewErrorMonitor {
  static final List<WebViewError> _errors = [];
  
  static void setupErrorMonitoring(WebViewController controller) {
    // 监控JS错误
    controller.runJavaScript('''
      window.addEventListener('error', function(event) {
        const errorData = {
          message: event.message,
          filename: event.filename,
          lineno: event.lineno,
          colno: event.colno,
          error: event.error?.toString(),
          stack: event.error?.stack,
          timestamp: Date.now(),
          url: window.location.href
        };
        
        if (window.FlutterBridge) {
          window.FlutterBridge.postMessage(JSON.stringify({
            type: 'error',
            data: errorData
          }));
        }
      }, true);
      
      // 监控未处理的Promise拒绝
      window.addEventListener('unhandledrejection', function(event) {
        const errorData = {
          type: 'promise_rejection',
          reason: event.reason?.toString(),
          timestamp: Date.now()
        };
        
        if (window.FlutterBridge) {
          window.FlutterBridge.postMessage(JSON.stringify({
            type: 'error',
            data: errorData
          }));
        }
      });
    ''');
  }
  
  // 上报error到服务端
  static Future<void> reportErrors() async {
    if (_errors.isEmpty) return;
    
    try {
      await http.post(
        Uri.parse('https://api.xxxx.com/error-report'),
        body: jsonEncode({
          'appVersion': '1.0.0',
          'platform': Platform.operatingSystem,
          'errors': _errors,
        }),
        headers: {'Content-Type': 'application/json'},
      );
      
      _errors.clear();
    } catch (e) {
      print('错误上报失败: $e');
    }
  }
}

八:以电商混合应用为例

8.1 项目架构

下面我们通过一个电商App的案例,把前面所有知识点串联起来:

bash 复制代码
lib/
├── main.dart
├── core/
│   ├── hybrid/           # 混合开发
│   │   ├── manager.dart     # 混合管理器
│   │   ├── bridge.dart      # 桥接文件
│   │   ├── router.dart      # 混合路由
│   │   └── security.dart    # 安全模块
│   └── di/               # 依赖注入
├── modules/
│   ├── product/          # 商品模块
│   │   ├── list_page.dart   # 原生列表
│   │   └── detail_page.dart # WebView详情
│   ├── cart/             # 购物车模块
│   └── order/            # 订单模块
└── shared/
    ├── widgets/          # 共享组件
    ├── utils/            # 工具类
    └── constants/        # 常量定义

8.2 实现商品详情页

dart 复制代码
class ProductDetailPage extends StatefulWidget {
  final String productId;
  
  const ProductDetailPage({Key? key, required this.productId}) 
      : super(key: key);
  
  @override
  _ProductDetailPageState createState() => _ProductDetailPageState();
}

class _ProductDetailPageState extends State<ProductDetailPage> {
  late WebViewController _controller;
  final ProductService _productService = ProductService();
  
  @override
  void initState() {
    super.initState();
    _initWebView();
    _prefetchProductData();
  }
  
  void _initWebView() {
    // 从WebView池获取或创建
    _controller = WebViewPool.getWebView(
      key: 'product_${widget.productId}',
      builder: () => _createWebViewController(),
    );
  }
  
  Future<WebViewController> _createWebViewController() async {
    final controller = WebViewController();
    
    // 获取用户信息和商品数据
    final userInfo = await UserService().getCurrentUser();
    final productData = await _productService.getProduct(widget.productId);
    
    // 含参URL
    final url = _buildProductUrl(productData, userInfo);
    
    await controller.loadRequest(Uri.parse(url));
    
    return controller;
  }
  
  String _buildProductUrl(Product product, User? user) {
    final params = {
      'product_id': product.id,
      'product_name': Uri.encodeComponent(product.name),
      'price': product.price.toString(),
      'user_id': user?.id ?? '',
      'user_token': user?.token ?? '',
      'platform': Platform.operatingSystem,
      'app_version': '1.0.0',
      'timestamp': DateTime.now().millisecondsSinceEpoch.toString(),
    };
    
    // 添加签名
    final signature = _generateSignature(params);
    params['sign'] = signature;
    
    final uri = Uri.parse('https://m.xxxx.com/product/detail')
        .replace(queryParameters: params);
    
    return uri.toString();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('商品详情'),
        actions: _buildAppBarActions(),
      ),
      body: Column(
        children: [
          // 顶部:商品简介
          _buildProductSummary(),
          
          // WebView详情部分
          Expanded(
            child: WebViewWidget(controller: _controller),
          ),
          
          // 底部:原生操作栏
          _buildBottomActionBar(),
        ],
      ),
    );
  }
  
  Widget _buildProductSummary() {
    return Container(
      padding: EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        border: Border(bottom: BorderSide(color: Colors.grey[200]!)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            '商品名称',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          SizedBox(height: 8),
          Row(
            children: [
              Text(
                '¥ 299.00',
                style: TextStyle(
                  fontSize: 24,
                  color: Colors.red,
                  fontWeight: FontWeight.bold,
                ),
              ),
              SizedBox(width: 8),
              Text(
                '¥ 399.00',
                style: TextStyle(
                  fontSize: 16,
                  color: Colors.grey,
                  decoration: TextDecoration.lineThrough,
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
  
  Widget _buildBottomActionBar() {
    return Container(
      height: 60,
      decoration: BoxDecoration(
        color: Colors.white,
        border: Border(top: BorderSide(color: Colors.grey[200]!)),
      ),
      child: Row(
        children: [
          // 客服
          Expanded(
            child: TextButton.icon(
              onPressed: _contactCustomerService,
              icon: Icon(Icons.chat),
              label: Text('客服'),
            ),
          ),
          
          // 加入购物车
          Expanded(
            child: ElevatedButton.icon(
              onPressed: _addToCart,
              icon: Icon(Icons.shopping_cart),
              label: Text('加入购物车'),
              style: ElevatedButton.styleFrom(
                primary: Colors.orange,
              ),
            ),
          ),
          
          // 立即购买
          Expanded(
            child: ElevatedButton.icon(
              onPressed: _buyNow,
              icon: Icon(Icons.shopping_bag),
              label: Text('立即购买'),
              style: ElevatedButton.styleFrom(
                primary: Colors.red,
              ),
            ),
          ),
        ],
      ),
    );
  }
  
  Future<void> _addToCart() async {
    // 通过桥接通知H5页面
    await _controller.runJavaScript('''
      if (window.addToCart) {
        window.addToCart();
      } else {
        // 调用Flutter原生方法
        window.FlutterBridge.postMessage(JSON.stringify({
          type: 'action',
          method: 'addToCart',
          data: {productId: '${widget.productId}'}
        }));
      }
    ''');
  }
}

8.3 适配H5页面

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>商品详情</title>
    <script>
        // Flutter桥接适配
        class FlutterAdapter {
            constructor() {
                this.callbacks = new Map();
                this.messageId = 0;
                this.setupBridge();
            }
            
            setupBridge() {
                // 注册Flutter调用方法
                window.addToCart = () => this.addToCart();
                window.buyNow = () => this.buyNow();
                window.getUserInfo = () => this.getUserInfo();
                
                // 初始化消息监听
                if (window.FlutterBridge) {
                    console.log('Flutter桥接已OK');
                }
            }
            
            // 添加购物车
            async addToCart() {
                const productId = this.getQueryParam('product_id');
                
                try {
                    // 通过桥接调用Flutter
                    const result = await this.callFlutter('addToCart', {
                        productId: productId,
                        quantity: 1
                    });
                    
                    if (result.success) {
                        this.showToast('添加成功');
                    } else {
                        this.showToast('添加失败: ' + result.message);
                    }
                } catch (error) {
                    console.error('添加购物车失败:', error);
                    this.showToast('网络异常,请重试');
                }
            }
            
            // 调用Flutter方法
            callFlutter(method, data) {
                return new Promise((resolve, reject) => {
                    const messageId = ++this.messageId;
                    
                    this.callbacks.set(messageId, { resolve, reject });
                    
                    // 设置超时
                    setTimeout(() => {
                        if (this.callbacks.has(messageId)) {
                            this.callbacks.delete(messageId);
                            reject(new Error('请求超时'));
                        }
                    }, 10000);
                    
                    // 发送消息
                    window.FlutterBridge.postMessage(JSON.stringify({
                        id: messageId.toString(),
                        type: 'request',
                        method: method,
                        data: data,
                        timestamp: Date.now()
                    }));
                });
            }
            
            // 接收Flutter消息
            onFlutterMessage(message) {
                try {
                    const data = JSON.parse(message);
                    
                    if (data.type === 'response' && data.id) {
                        const callback = this.callbacks.get(parseInt(data.id));
                        if (callback) {
                            this.callbacks.delete(parseInt(data.id));
                            
                            if (data.data.success) {
                                callback.resolve(data.data);
                            } else {
                                callback.reject(new Error(data.data.message));
                            }
                        }
                    } else if (data.type === 'event') {
                        // 处理Flutter发来的事件
                        this.handleEvent(data);
                    }
                } catch (error) {
                    console.error('处理Flutter消息失败:', error);
                }
            }
            
            getQueryParam(name) {
                const urlParams = new URLSearchParams(window.location.search);
                return urlParams.get(name);
            }
            
            showToast(message) {
                // 显示提示
                const toast = document.createElement('div');
                toast.textContent = message;
                toast.style.cssText = `
                    position: fixed;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    background: rgba(0,0,0,0.8);
                    color: white;
                    padding: 12px 24px;
                    border-radius: 8px;
                    z-index: 1000;
                `;
                document.body.appendChild(toast);
                
                setTimeout(() => {
                    document.body.removeChild(toast);
                }, 2000);
            }
        }
        
        // 页面初始化
        document.addEventListener('DOMContentLoaded', function() {
            const adapter = new FlutterAdapter();
            
            // 检测运行环境
            const isInApp = navigator.userAgent.includes('FlutterWebView');
            
            if (isInApp) {
                // App内特有逻辑
                document.body.classList.add('in-app');
                
                // 适配安全区域
                document.documentElement.style.setProperty(
                    '--safe-area-top', 
                    'env(safe-area-inset-top, 0px)'
                );
                document.documentElement.style.setProperty(
                    '--safe-area-bottom', 
                    'env(safe-area-inset-bottom, 0px)'
                );
                
                // 隐藏H5导航
                const h5Nav = document.querySelector('.h5-navigation');
                if (h5Nav) h5Nav.style.display = 'none';
            }
            
            // 加载商品数据
            loadProductData();
        });
        
        async function loadProductData() {
            const productId = new URLSearchParams(window.location.search)
                .get('product_id');
            
            if (!productId) return;
            
            try {
                const response = await fetch(
                    `https://api.xxxx.com/products/${productId}`
                );
                const product = await response.json();
                
                renderProduct(product);
            } catch (error) {
                console.error('加载商品失败:', error);
                showError('加载失败,请重试');
            }
        }
        
        function renderProduct(product) {
            // 渲染商品信息
            document.getElementById('product-title').textContent = product.name;
            document.getElementById('product-price').textContent = 
                `¥ ${product.price}`;
            document.getElementById('product-desc').innerHTML = 
                product.description;
            
            // 渲染图片
            const gallery = document.getElementById('product-gallery');
            product.images.forEach(img => {
                const imgEl = document.createElement('img');
                imgEl.src = img.url;
                imgEl.alt = product.name;
                gallery.appendChild(imgEl);
            });
        }
    </script>
</head>
<body>
    <div class="product-container">
        <h1 id="product-title"></h1>
        <div class="price" id="product-price"></div>
        <div class="gallery" id="product-gallery"></div>
        <div class="description" id="product-desc"></div>
    </div>
</body>
</html>

总结

至此Flutter混合开发与WebView相关知识点就全部介绍完了,牢记一下核心原则:

  • 优先性能:WebView预加载、内存管理、缓存
  • 安全第一:输入验证、通信加密、权限控制

避坑指南:

常见问题及解决方案:

  1. WebView白屏
  • 原因:内存不足或初始化问题

  • 解决:实现WebView复用,添加重试机制

  1. 通信延迟高
  • 原因:频繁小消息通信

  • 解决:批量处理,二进制协议

  1. 内存泄漏
  • 原因:未正确释放WebView

  • 解决:使用WeakReference,管理生命周期

  1. 跨平台差异
  • 原因:iOS/Android WebView实现不同
  • 解决:平台适配层,功能降级

结语

混合开发不是简单的炫技,而是在原生与web之间的性能、安全、体验、效率等方面寻找一个最佳的平衡点。技术只是手段,用户体验才是目的。

如果觉得本文对你有帮助,别忘了一键三连~~~,有任何问题或想法,欢迎在评论区交流讨论! 转载请注明出处!!!

相关推荐
千里马学框架16 小时前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台16 小时前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone17 小时前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc17 小时前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo19 小时前
Kotlin 协程中的 Job 结构化并发与取消
android
sun00770019 小时前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼20 小时前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
茶底世界之下21 小时前
为什么预览、录制、离线导出不能共享同一个背压策略?
ios·swift
AFinalStone21 小时前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen21 小时前
属性动画原理与高级动画实现
android·kotlin·canvas