React TS PWA中iframe跨域调用showSaveFilePicker报错求助
解决iframe跨域场景下showSaveFilePicker无法使用的问题
问题场景
开发React TypeScript PWA应用时,全局应用通过iframe嵌套子应用,子应用中使用showSaveFilePicker实现图片下载,核心代码如下:
export async function downloadPhoto(selectedImageBlob: any, downloadPhotoName: any) { try { const handle = await showSaveFilePicker({ suggestedName: downloadPhotoName, types: [ { accept: { "image/png": [".png"] } }, { accept: { "image/jpg": [".jpg"] } }, { accept: { "image/jpeg": [".jpeg"] } }, { accept: { "image/webp": [".webp"] } }, ], excludeAcceptAllOption: false, }); const writableStream = await handle.createWritable(); await writableStream.write(selectedImageBlob); await writableStream.close(); } catch (err) { console.error(err); } }
报错信息
执行下载操作时,抛出跨域限制异常:
index.js:1 DOMException: Failed to execute 'showSaveFilePicker' on 'Window': Cross origin sub frames aren't allowed to show a file picker. at downloadPhoto (http://localhost:3001/static/js/main.chunk.js:53502:26) at onClick (http://localhost:3001/static/js/main.chunk.js:32079:111) at HTMLUnknownElement.callCallback (http://localhost:3001/static/js/vendors~main.chunk.js:240900:18) at Object.invokeGuardedCallbackDev (http://localhost:3001/static/js/vendors~main.chunk.js:240944:20) at invokeGuardedCallback (http://localhost:3001/static/js/vendors~main.chunk.js:241001:35) at invokeGuardedCallbackAndCatchFirstError (http://localhost:3001/static/js/vendors~main.chunk.js:241015:29) at executeDispatch (http://localhost:3001/static/js/vendors~main.chunk.js:245159:7) at processDispatchQueueItemsInOrder (http://localhost:3001/static/js/vendors~main.chunk.js:245185:11) at processDispatchQueue (http://localhost:3001/static/js/vendors~main.chunk.js:245196:9) at dispatchEventsForPlugins (http://localhost:3001/static/js/vendors~main.chunk.js:245205:7)
替代方案
方案1:由父应用处理下载逻辑
跨域iframe无法调用文件选择器,可通过postMessage将文件数据传递给父应用,由父应用触发下载流程:
- 子应用修改代码,传递文件数据:
export async function downloadPhoto(selectedImageBlob: Blob, downloadPhotoName: string) { try { // 将Blob转为可传递的base64格式 const reader = new FileReader(); reader.onload = () => { // 生产环境请替换*为父应用的具体origin,提升安全性 window.parent.postMessage({ type: 'DOWNLOAD_IMAGE', data: reader.result as string, filename: downloadPhotoName, mimeType: selectedImageBlob.type }, '*'); }; reader.readAsDataURL(selectedImageBlob); } catch (err) { console.error(err); } }
- 父应用添加消息监听,处理下载:
// 在父应用的根组件中添加监听 useEffect(() => { const handleDownloadRequest = async (event: MessageEvent) => { if (event.data.type !== 'DOWNLOAD_IMAGE') return; const { data, filename, mimeType } = event.data; // 将base64转回Blob const byteString = atob(data.split(',')[1]); const arrayBuffer = new ArrayBuffer(byteString.length); const uint8Array = new Uint8Array(arrayBuffer); for (let i = 0; i < byteString.length; i++) { uint8Array[i] = byteString.charCodeAt(i); } const blob = new Blob([arrayBuffer], { type: mimeType }); // 父应用调用文件选择器完成下载 const handle = await showSaveFilePicker({ suggestedName: filename, types: [ { accept: { "image/png": [".png"] } }, { accept: { "image/jpg": [".jpg"] } }, { accept: { "image/jpeg": [".jpeg"] } }, { accept: { "image/webp": [".webp"] } }, ], excludeAcceptAllOption: false, }); const writableStream = await handle.createWritable(); await writableStream.write(blob); await writableStream.close(); }; window.addEventListener('message', handleDownloadRequest); return () => window.removeEventListener('message', handleDownloadRequest); }, []);
方案2:使用传统a标签下载(无需文件选择器)
如果不需要用户自定义保存路径,可直接创建a标签触发下载,该方式不受iframe跨域限制:
export async function downloadPhoto(selectedImageBlob: Blob, downloadPhotoName: string) { try { const url = URL.createObjectURL(selectedImageBlob); const link = document.createElement('a'); link.href = url; link.download = downloadPhotoName; document.body.appendChild(link); link.click(); document.body.removeChild(link); // 释放URL对象,避免内存泄漏 URL.revokeObjectURL(url); } catch (err) { console.error(err); } }
注:此方式会直接下载到浏览器默认路径,无法让用户选择保存位置,适合无需自定义路径的场景。
方案3:调整iframe为同域(业务允许时)
如果可以调整子应用部署策略,让子应用与父应用处于同一域名,则跨域限制自动解除,原showSaveFilePicker代码可正常运行。
内容的提问来源于stack exchange,提问作者Mukesh Kushwah
相关产品推荐
相关产品推荐

