Flutter InAppWebView处理Blob URL PDF下载异常问题排查
解决Flutter中flutter_inappwebview处理Blob URL PDF下载的问题
先明确现有代码的几个核心问题:
initState中webViewController尚未初始化,此时注册base64String的JavaScript Handler不会生效onWebViewCreated中重复注册base64StringHandler,覆盖了原本的文件写入与打开逻辑- 仅针对特定ID的
link元素绑定事件,无法覆盖页面动态生成链接、JS触发下载等场景
针对你的三个疑问的解答
1. Flutter WebView处理Blob URL下载的更优方案
Base64编码会增加约30%的数据体积,转换和传输效率偏低。更优的方案是直接传递Blob的原始字节数据:
- 在JavaScript中将Blob转为ArrayBuffer,再编码为Base64字符串传递(避免二进制直接传输的兼容性问题)
- Flutter端接收后直接解码为
Uint8List写入文件,跳过冗余的MIME前缀解析步骤
2. InAppWebView遗漏的配置项
确保开启以下关键配置,避免Blob资源访问失败:
- 跨平台基础配置:
javaScriptEnabled: true、useOnDownloadStart: true、allowFileAccess: true - Android平台额外配置:在
AndroidInAppWebViewOptions中设置allowFileAccessFromFileURLs: true、allowUniversalAccessFromFileURLs: true,允许WebView访问本地文件与跨域资源 - iOS平台:确保
Info.plist中配置了必要的ATS例外(若目标URL为HTTP协议),IOSInAppWebViewOptions中保持javaScriptEnabled: true即可
3. 可运行的Blob URL PDF下载实践代码
修正后的完整代码,解决现有问题并实现可靠的Blob下载:
import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:yeniweb/core/pdf_ac.dart'; class WebViewScreen extends StatefulWidget { final String url; WebViewScreen({required this.url}); @override _WebViewScreenState createState() => _WebViewScreenState(); } class _WebViewScreenState extends State<WebViewScreen> { final GlobalKey<ScaffoldMessengerState> _scaffoldKey = GlobalKey<ScaffoldMessengerState>(); InAppWebViewController? webViewController; Dio dio = Dio(); double downloadProgress = 0.0; bool isLoading = true; @override Widget build(BuildContext context) { return Scaffold( key: _scaffoldKey, appBar: AppBar( backgroundColor: Colors.white70, elevation: 0, title: const Text("Web View"), ), body: Stack( children: <Widget>[ InAppWebView( onLoadStart: (controller, url) { setState(() { isLoading = true; }); }, onLoadStop: (controller, url) { setState(() { isLoading = false; }); }, initialUrlRequest: URLRequest(url: Uri.parse(widget.url)), initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( javaScriptEnabled: true, useOnDownloadStart: true, allowFileAccess: true, ), android: AndroidInAppWebViewOptions( allowFileAccessFromFileURLs: true, allowUniversalAccessFromFileURLs: true, ), ), onWebViewCreated: (controller) { webViewController = controller; // 注册处理Blob字节数据的Handler webViewController?.addJavaScriptHandler( handlerName: 'handleBlobBytes', callback: (args) async { final Uint8List bytes = base64.decode(args[0]); final dir = await getApplicationDocumentsDirectory(); final filePath = p.join(dir.path, 'downloaded_pdf.pdf'); File(filePath).writeAsBytesSync(bytes); openPDF(filePath); }, ); // 注册接收Blob URL的Handler webViewController?.addJavaScriptHandler( handlerName: 'downloadBlob', callback: (args) { handleBlobUrl(args[0]); }, ); }, onDownloadStartRequest: (controller, request) async { final url = request.url.toString(); if (url.startsWith('blob:')) { print("Blob URL 已检测,开始处理"); await handleBlobUrl(url); } else { showDownloadDialog(url); print("非Blob URL,使用常规下载"); } }, ), isLoading ? const Center(child: CircularProgressIndicator()) : const SizedBox.shrink(), ], ), ); } Future<void> handleBlobUrl(String blobUrl) async { try { await webViewController?.evaluateJavascript(source: """ (async function() { try { const response = await fetch('$blobUrl'); const blob = await response.blob(); const arrayBuffer = await blob.arrayBuffer(); const base64String = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))); window.flutter_inappwebview.callHandler('handleBlobBytes', base64String); } catch (error) { console.error('Blob处理失败:', error); } })(); """); } catch (e) { print('处理Blob URL时出错: $e'); } } Future<void> startDownload(String url) async { try { if (url.isEmpty || !Uri.parse(url).isAbsolute) { throw '无效URL'; } final fileName = p.basename(url); final dir = await getApplicationDocumentsDirectory(); final filePath = p.join(dir.path, fileName); await dio.download(url, filePath, onReceiveProgress: (received, total) { setState(() { downloadProgress = received / total; }); }); openPDF(filePath); } catch (e) { print('下载错误: $e'); _scaffoldKey.currentState?.showSnackBar( SnackBar(content: Text('下载失败: $e')), ); } } void showDownloadDialog(String url) { setState(() { downloadProgress = 0.0; }); showDialog( context: context, barrierDismissible: false, builder: (context) { startDownload(url); return AlertDialog( title: const Text('正在下载...'), content: StreamBuilder( stream: Stream.periodic(const Duration(milliseconds: 500)), builder: (context, snapshot) { return LinearProgressIndicator(value: downloadProgress); }, ), actions: <Widget>[ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: const Text('关闭'), ), ], ); }, ); } void openPDF(String filePath) { Navigator.of(context).push(MaterialPageRoute( builder: (context) => PDFViewPage(filePath: filePath), )); } }
代码关键改进点:
- 统一在
onWebViewCreated中注册Handler,确保webViewController已初始化 - 使用ArrayBuffer转Base64的方式传递数据,避免冗余的MIME前缀解析
- 完善Android平台WebView配置,确保Blob资源可正常访问
- 增加错误提示,通过SnackBar告知用户下载失败信息
内容的提问来源于stack exchange,提问作者monti
相关产品推荐
相关产品推荐

