尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

离谱!竟然是这样——论如何彻底解决 Mac Chrome 文件上传卡顿:从原理到实战的完整方案

离谱!竟然是这样——论如何彻底解决 Mac Chrome 文件上传卡顿:从原理到实战的完整方案 前言使用苹果电脑的大大们不知道大家有没有遇到过在谷歌浏览器的任何网页上传文件的时候会很慢即使已经点击了确定文件上传之后整个页面没有任何反应过了十几秒才会出现那个你在上传的文件情况在今天之前以及过去百度或谷歌得到的答案永远都是在分析的苹果电脑或者谷歌浏览器的文件安全扫描导致的但是当我和我的同事对比讨论才发现其实根本原因竟然只是长期没关电脑重启就好了救命 离了个大谱但是作为前端开发者在发现根本问题之前还是从技术层面解决这个问题也找到过一样情况的文章研究学习比如https://teams.public.onecdn.static.microsoft/evergreen-assets/safelinks/2/atp-safelinks.html但是终究是没有一份文章提到到底要怎么解决最后还是通过询问谷歌浏览器的AI找到了一份从UIUX的层面的答案那就是加Loading具体请看下文问题分析其实棘手的问题是在 Mac Chrome 发生空白无响应时它发生在用户点击了“确定”之后、文件数据流还没到达前端 JS 之前也就是在change事件触发前。更糟糕的是HTML 原生的input typefile /在用户点击“取消”时没有任何标准的系统级回调事件。所以前端无法直接通过一个类似onCancel的钩子来判断用户到底是点了确定还是取消。为了在发生卡顿时完美控制 Loading 的显示与隐藏我们需要用一些“黑科技”组合拳来做精准判断。以下是目前前端业界最成熟的2 种判断与处理方案AI给的哈哈哈哈方案一Focus 聚焦追踪法最推荐专门用来捕获“取消”动作原理解析当用户点击“选择文件”时Mac 的 Finder 弹窗会抢走浏览器的焦点。当用户点击“确定”或“取消”关闭弹窗时浏览器窗体window会重新获得焦点触发focus事件。如果用户点了“确定”并卡死流程是点击确定-Mac系统卡死数秒-window 获得 focus-JS 触发 change 事件。如果用户点了“取消”流程是点击取消-window 立即获得 focus-JS 绝对不会触发 change 事件。利用这个时间差和事件先后顺序我们可以写出完美的判断逻辑import React, { useState, useRef } from react; const SmartUpload () { const [isLoading, setIsLoading] useState(false); const isFileSelected useRef(false); // 用来标记用户是否真的选了文件 const handleFileChange (e) { // 只要触发了 change 证明用户绝对点了“确定”且卡死已经结束了 isFileSelected.current true; console.log(用户点击了确定文件已成功到达前端:, e.target.files); // 执行你的上传、预览等后续逻辑... setIsLoading(false); }; const handleButtonClick () { // 1. 用户一点击按钮立刻开启 Loading setIsLoading(true); isFileSelected.current false; // 重置状态 // 2. 核心黑科技监听浏览器窗口重新获得焦点的事件 const onWindowFocus () { // 必须使用 setTimeout(0) 延迟执行让 change 事件有机会先触发 setTimeout(() { // 如果窗口恢复了焦点但 change 事件没有触发说明用户百分之百点了“取消” if (!isFileSelected.current) { console.log(用户点击了取消关闭 Loading); setIsLoading(false); } // 销毁监听器防止内存泄漏 window.removeEventListener(focus, onWindowFocus); }, 300); // 这里的 300ms 关键给 Mac 底层卡死结束后、将数据塞给 change 事件留出微小的调度时间 }; // 3. 激活窗口焦点监听 window.addEventListener(focus, onWindowFocus); }; return ( div {isLoading ( div classNamegpu-loading-overlay div classNamepure-css-spinner/div p正在处理 Mac 系统文件服务请稍候.../p /div )} {/* 触发器 */} label htmlForfile-picker classNameupload-btn onClick{handleButtonClick} 选择文件 /label input idfile-picker typefile style{{ display: none }} onChange{handleFileChange} / /div ); };方案二针对拖拽Drag Drop的绝对控制如果是拖拽文件释放Drop时卡死情况完全不同。拖拽不需要打开 Finder 弹窗不存在“取消”的概念。用户一旦把文件在网页上松开Drop就代表他绝对是要上传文件了。所以在拖拽场景下你的代码可以有 100% 的主导权不需要猜测用户的意图处理起来更纯粹const handleDrop (e) { e.preventDefault(); // 1. 用户一松手100% 确定要上传立刻在主线程被卡死前挂载 Loading setTimeout(() { setIsLoading(true); }, 0); // 2. 获取文件数据此时 Mac 系统开始安全鉴权可能会在这里卡死数秒 const files e.dataTransfer.files; if (files.length 0) { // 3. 正常解析、上传文件完成后关闭 Loading uploadToServer(files).finally(() { setIsLoading(false); }); } else { // 防御性代码如果没有抓到文件直接关闭 setIsLoading(false); } }; 总结你的核心疑问这个卡顿是在文件真正上传之前吗是的完全在网页上传网络请求之前。这是 Mac 系统把文件交给浏览器Chrome的磁盘 I/O 和沙盒鉴权阶段。怎么知道用户是确定还是取消点击确定 ➡️ 会依次触发系统的focus重新聚焦和input的change事件。点击取消 ➡️只会触发系统的focus重新聚焦绝对不会触发change事件。采用方案一的 Focus 追踪法就可以利用这个逻辑闭环完美控制 Loading 的开启和关闭不用担心 Loading 被卡在页面上消不掉。但是问题又来了如果是大公司往往使用的都是自己内部的UI框架或者市场上已经成熟的UI框架那么点击确定 - Mac系统卡死数秒 - window 获得 focus - JS 触发 change 事件 Mac系统卡死数秒中如果使用的是公司内部的组件点击了确定并不会触发onChange方法AI给的答案如下亲测有效这是一个非常典型的企业内部组件二次封装导致的“事件被吞”问题。很多公司内部封装的Upload组件为了实现统一的样式或批处理并没有直接暴露原生的onChange。相反它们会拦截底层的input typefile等到文件完全解析、甚至是前端文件切片、哈希计算完成后才会触发组件对外暴露的onChange或onSuccess。在 Mac Chrome 伪死机期间组件底层的原生input确实触发了change但由于主线程卡死组件上层的状态还没来得及更新导致你拿不到组件的onChange。 核心解决思路不要指望组件的onChange我们需要绕过内部组件的黑盒利用冒泡机制在window或者是全局的document层面强行捕获底层那个原生input的change和cancel事件。HTML5 针对原生文件弹窗其实提供了一个秘密的事件cancel事件。用户点击“确定”➡️ 触发原生change事件。用户点击“取消”➡️ 触发原生cancel事件。利用这个特性再结合window.focus我们可以在公司内部组件的外层套一个逻辑壳精准控制 Loading。 解决方案全局捕获拦截法请在包裹你公司内部组件的父级页面中使用以下逻辑。它不仅能判断是“确定”还是“取消”还能完美在卡死期间撑起 Loading。import React, { useState, useEffect, useRef } from react; // 假设这是你们公司的内部组件 import { CompanyUpload } from your-internal-ui-library; const MyUploadPage () { const [showLoading, setShowLoading] useState(false); const isActionResolved useRef(false); // 标记用户本次弹窗是否有明确结果确定/取消 const handleUploadClick () { // 1. 用户一点击上传按钮立刻开启 GPU 加速的 Loading setShowLoading(true); isActionResolved.current false; // 2. 准备捕获原生 input 的确定和取消事件利用事件冒泡 const handleNativeChange (e) { // 只要抓到了原生的 change说明用户绝对点了确定且 Mac 卡死结束文件流到了浏览器 if (e.target e.target.type file) { console.log(捕获到原生确定事件卡死结束); isActionResolved.current true; setShowLoading(false); // 关闭 Loading cleanUp(); } }; const handleNativeCancel (e) { // 只要抓到了原生的 cancel说明用户点了取消 if (e.target e.target.type file) { console.log(捕获到原生取消事件); isActionResolved.current true; setShowLoading(false); // 关闭 Loading cleanUp(); } }; // 3. 兜底策略如果因为极特殊情况没触发上面两个事件利用焦点恢复关闭 Loading const handleWindowFocus () { setTimeout(() { // 如果窗口恢复了焦点但既没走 change 也没走 cancel说明可能被判定为了取消 if (!isActionResolved.current) { console.log(窗口恢复焦点但未捕获到文件安全关闭 Loading); setShowLoading(false); cleanUp(); } }, 400); // 400ms 的延迟确保让 Mac 系统卡死结束后的 change 事件有足够时间冒泡出来 }; // 清理全局监听器防止内存泄漏 const cleanUp () { document.removeEventListener(change, handleNativeChange, true); document.removeEventListener(cancel, handleNativeCancel, true); window.removeEventListener(focus, handleWindowFocus); }; // 4. 绑定全局监听使用事件捕获/冒泡确保能抓到内部组件里隐藏的 input document.addEventListener(change, handleNativeChange, true); document.addEventListener(cancel, handleNativeCancel, true); window.addEventListener(focus, handleWindowFocus); }; return ( div style{{ position: relative }} {/* 确保这里的 Loading 遮罩使用的是前文提到的“纯 CSS GPU 加速动画” */} {showLoading ( div classNamegpu-loading-overlay div classNamepure-css-spinner/div p正在读取 Mac 系统文件请稍候.../p /div )} {/* 外层包裹一层 click 监听用来触发 Loading 的启动 */} div onClick{handleUploadClick} CompanyUpload // 这里的 onChange 依然走你们公司的正常业务逻辑不影响它 onChange{(files) console.log(公司组件业务逻辑, files)} / /div /div ); }; 为什么这个方案能彻底解决痛点摆脱了对公司组件onChange的依赖我们通过document.addEventListener(change, ...)监听了全页面的file变化。即使公司组件把数据吞了或者延迟处理只要用户在 Mac 弹窗里点了“确定”并度过了卡死期浏览器最底层的change事件就一定会冒泡到document上。此时 Loading 就会立刻被关闭。完美支持“取消”判定HTML5 原生的cancel事件在用户点击取消的一瞬间就会触发。即便由于某些框架重写导致cancel没冒泡出来最后的window.focusisActionResolved.current检查也会作为最终防线在 400ms 后稳稳地把 Loading 关掉。 [1]卡死期间动画不会定格只要你的.pure-css-spinner严格按照前文使用了will-change: transform和纯 CSSkeyframes动画即使 Mac 系统和 Chrome 主线程在点击“确定”后陷入了几秒的伪死机状态这个 Loading 依然能在界面上流畅旋转极大地缓解了用户的焦虑。其实最主要的就是cancel事件的监听因为focus和change这两个方法完全可以用UI框架封装的上传组件是否上传成功的回调函数代替大家根据需求自取吧
返回列表