5月28日 02:08

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引用:

kotlin
private 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中处理选择结果并回传:

kotlin
override 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+需要动态申请存储权限:

kotlin
if (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中的文件选择代理方法:

swift
func 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传回:

swift
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { completionHandler?(urls) completionHandler = nil }

iOS相比Android更简单,因为WKWebView在iOS 12+已经原生支持了基本的文件选择,只有需要自定义选择行为时才需要实现上述代理方法。

文件下载

Android端实现

Android WebView不会自动处理下载请求,需要设置DownloadListener拦截下载链接:

kotlin
webView.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):需要更多控制(自定义证书、进度回调到页面、加密存储)时使用

自定义下载的核心代码:

kotlin
val 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来识别下载请求:

swift
func 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环境下注意证书校验,中间人攻击可能替换下载内容
  • 大文件上传考虑分片和断点续传,避免网络波动导致重头开始
标签:Webview