WebView中如何实现文件上传和下载功能?
WebView中如何实现文件上传和下载功能?
文件上传
Android端实现
Android WebView默认不支持<input type="file">标签,需要开发者手动处理文件选择回调。核心思路是重写WebChromeClient中的文件选择方法,启动系统文件选择器,然后将用户选择的文件通过ValueCallback回传给WebView。
版本适配是关键难点。 Android不同版本中回调方法签名不同,需要处理三个重载版本:
- Android 4.x:
openFileChooser(ValueCallback<Uri>, String) - Android 5.0+:
onShowFileChooser(WebView, ValueCallback<Uri[]>, FileChooserParams) - Android 4.4 KitKat:存在已知Bug,文件上传回调不会被触发,这是面试高频追问点
具体实现步骤:
第一步,在WebChromeClient中重写文件选择方法,保存ValueCallback引用:
kotlinprivate var filePathCallback: ValueCallback<Array<Uri>>? = null override fun onShowFileChooser( webView: WebView?, filePathCallback: ValueCallback<Array<Uri>>?, fileChooserParams: FileChooserParams? ): Boolean { this.filePathCallback = filePathCallback val intent = fileChooserParams?.createIntent() startActivityForResult(intent, REQUEST_CODE_FILE_CHOOSER) return true }
第二步,在onActivityResult中处理选择结果并回传:
kotlinoverride fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == REQUEST_CODE_FILE_CHOOSER) { val result = if (resultCode == Activity.RESULT_OK) { data?.data?.let { arrayOf(it) } } else null filePathCallback?.onReceiveValue(result) filePathCallback = null } }
第三步,处理运行时权限。Android 6.0+需要动态申请存储权限:
kotlinif (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERM_REQUEST) }
面试常问: 为什么onShowFileChooser返回true?——返回true表示由应用自己处理文件选择,返回false则WebView不会等待结果。
iOS端实现
iOS的WKWebView对文件上传的支持相对完善。需要实现WKUIDelegate中的文件选择代理方法:
swiftfunc webView(_ webView: WKWebView, runOpenPanelWithParameters parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping ([URL]?) -> Void) { let documentPicker = UIDocumentPickerViewController(documentTypes: ["public.item"], in: .import) documentPicker.delegate = self self.completionHandler = completionHandler present(documentPicker, animated: true) }
在documentPicker的代理回调中,将选择的文件URL传回:
swiftfunc documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { completionHandler?(urls) completionHandler = nil }
iOS相比Android更简单,因为WKWebView在iOS 12+已经原生支持了基本的文件选择,只有需要自定义选择行为时才需要实现上述代理方法。
文件下载
Android端实现
Android WebView不会自动处理下载请求,需要设置DownloadListener拦截下载链接:
kotlinwebView.setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> val request = DownloadManager.Request(Uri.parse(url)).apply { setTitle(URLUtil.guessFileName(url, contentDisposition, mimetype)) setDescription("下载文件中...") setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, URLUtil.guessFileName(url, contentDisposition, mimetype)) } val dm = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager dm.enqueue(request) }
面试追问:DownloadManager和自定义下载如何选择?
- DownloadManager:系统级服务,自动处理网络切换、断点续传、通知栏进度,适合大多数场景
- 自定义下载(OkHttp/HttpURLConnection):需要更多控制(自定义证书、进度回调到页面、加密存储)时使用
自定义下载的核心代码:
kotlinval client = OkHttpClient() val request = Request.Builder().url(downloadUrl).build() client.newCall(request).enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val body = response.body ?: return val total = body.contentLength() body.byteStream().use { input -> FileOutputStream(outputFile).use { output -> val buffer = ByteArray(8192) var bytesRead: Int var downloaded = 0L while (input.read(buffer).also { bytesRead = it } != -1) { output.write(buffer, 0, bytesRead) downloaded += bytesRead val progress = (downloaded * 100 / total).toInt() runOnUiThread { updateProgress(progress) } } } } } override fun onFailure(call: Call, e: IOException) { /* 错误处理 */ } })
iOS端实现
iOS端通过WKNavigationDelegate拦截导航响应,判断Content-Type来识别下载请求:
swiftfunc webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { if let response = navigationResponse.response as? HTTPURLResponse, let mimeType = response.mimeType, mimeType != "text/html" { // 非HTML响应,按下载处理 URLSession.shared.downloadTask(with: response.url!) { tempUrl, _, error in guard let tempUrl = tempUrl else { return } let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let destinationUrl = documentsUrl.appendingPathComponent(response.suggestedFilename ?? "download") try? FileManager.default.moveItem(at: tempUrl, to: destinationUrl) }.resume() decisionHandler(.cancel) } else { decisionHandler(.allow) } }
两端差异与常见坑点
Android端主要坑点:
- KitKat(4.4)文件上传完全不工作,需引导用户使用系统浏览器
- openFileChooser方法在Android源码中是隐藏API,不同ROM可能有差异
- onActivityResult中必须调用ValueCallback,即使用户取消选择也要传null,否则下次无法触发
- Android 10+ Scoped Storage限制,不能直接访问外部存储
iOS端主要坑点:
- iOS 13+ UIDocumentPicker需要配置com.apple.developer.icloud-container-identifiers
- 下载大文件时NSURLSession需配置background session才能在App退到后台后继续
- Content-Type判断不够准确时可能误拦截正常页面请求
安全要点
文件上传下载涉及用户数据安全,需重点关注:
- 上传前校验文件类型和大小,不能仅依赖前端accept属性,服务端也要校验
- 下载文件存储到应用沙盒目录,避免外部存储被其他应用篡改
- 对文件名做sanitize处理,防止路径遍历攻击(如"../../data")
- HTTPS环境下注意证书校验,中间人攻击可能替换下载内容
- 大文件上传考虑分片和断点续传,避免网络波动导致重头开始