服务端阅读 05月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引用:private var filePathCallback: ValueCallback<Array<Uri>>? = nulloverride 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中处理选择结果并回传: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+需要动态申请存储权限: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中的文件选择代理方法: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传回:func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { completionHandler?(urls) completionHandler = nil}iOS相比Android更简单,因为WKWebView在iOS 12+已经原生支持了基本的文件选择,只有需要自定义选择行为时才需要实现上述代理方法。文件下载Android端实现Android WebView不会自动处理下载请求,需要设置DownloadListener拦截下载链接: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):需要更多控制(自定义证书、进度回调到页面、加密存储)时使用自定义下载的核心代码: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来识别下载请求: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环境下注意证书校验,中间人攻击可能替换下载内容大文件上传考虑分片和断点续传,避免网络波动导致重头开始