You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter InAppWebView处理Blob URL PDF下载异常问题排查

解决Flutter中flutter_inappwebview处理Blob URL PDF下载的问题

先明确现有代码的几个核心问题:

  • initState中webViewController尚未初始化,此时注册base64String的JavaScript Handler不会生效
  • onWebViewCreated中重复注册base64String Handler,覆盖了原本的文件写入与打开逻辑
  • 仅针对特定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),
    ));
  }
}

代码关键改进点:

  1. 统一在onWebViewCreated中注册Handler,确保webViewController已初始化
  2. 使用ArrayBuffer转Base64的方式传递数据,避免冗余的MIME前缀解析
  3. 完善Android平台WebView配置,确保Blob资源可正常访问
  4. 增加错误提示,通过SnackBar告知用户下载失败信息

内容的提问来源于stack exchange,提问作者monti

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 18:24:50