1. 为什么需要全局捕获资源加载错误?
在前端开发中,图片、脚本等静态资源加载失败是常见但容易被忽视的问题。当用户网络不稳定或CDN节点异常时,页面可能静默地丢失关键内容。我曾遇到一个电商项目,促销活动期间因第三方统计脚本加载失败,导致转化率数据完全丢失却无人察觉。
传统的错误处理方式是在每个资源上单独添加onerror回调,但这种方式存在三个明显缺陷:
- 代码重复率高,维护成本大
- 动态注入的资源容易遗漏处理
- 无法统计整体错误率
通过window.onerror和error事件捕获的组合方案,我们可以实现:
- 实时监控所有资源加载状态
- 统一错误上报逻辑
- 动态降级处理机制
2. 基础实现方案与核心API
2.1 window.onerror的基础用法
这个全局错误处理器可以捕获运行时异常和脚本加载失败:
window.onerror = function(message, source, lineno, colno, error) { console.log('错误信息:', message) console.log('脚本URL:', source) console.log('行号:', lineno) console.log('列号:', colno) console.log('Error对象:', error) // 返回true阻止默认错误提示 return true }但需要注意三个关键限制:
- 跨域脚本只会返回"Script error."(需设置crossorigin属性)
- 无法捕获资源如图片、CSS的加载错误
- 不同浏览器参数顺序可能不一致
2.2 增强版的error事件监听
通过事件冒泡捕获所有资源错误:
window.addEventListener('error', function(event) { // 区分资源类型 const tagName = event.target.tagName if (tagName === 'IMG') { handleImageError(event.target) } else if (tagName === 'SCRIPT') { reportScriptError(event.target.src) } }, true) // 使用捕获阶段这里必须设置useCapture为true,因为资源加载错误不会冒泡。
3. 生产环境完整解决方案
3.1 错误分类处理策略
根据资源类型采取不同降级方案:
| 错误类型 | 处理方案 | 示例 |
|---|---|---|
| 关键脚本 | 重试加载 | 支付SDK |
| 非关键脚本 | 延迟加载 | 统计分析 |
| 首屏图片 | 替换备用图 | 商品主图 |
| 装饰图片 | 隐藏元素 | 背景图 |
实现示例:
function handleResourceError(event) { const target = event.target const resourceType = target.tagName switch(resourceType) { case 'IMG': if (isCriticalImage(target)) { target.src = getFallbackImage() } else { target.style.display = 'none' } break case 'SCRIPT': if (isCriticalScript(target)) { retryLoading(target, 3) // 最多重试3次 } break } reportToAnalytics({ type: resourceType, url: target.src || target.href, timestamp: Date.now() }) }3.2 性能优化注意事项
- 错误回调中避免同步操作:
// 错误做法 window.onerror = function() { sendBeacon() // 同步XHR会阻塞页面 } // 正确做法 window.onerror = function() { setTimeout(() => { navigator.sendBeacon('/log', data) }, 0) }- 使用WeakMap存储重试次数避免内存泄漏:
const retryMap = new WeakMap() function retryLoading(element, max) { const count = retryMap.get(element) || 0 if (count >= max) return retryMap.set(element, count + 1) element.src += `${element.src.includes('?') ? '&' : '?'}_retry=${Date.now()}` }4. 监控上报与数据分析
4.1 错误数据聚合方案
建议采用以下维度聚合:
- 资源类型(img/script/link)
- 域名分布(区分自有和第三方)
- 地理区域(通过IP解析)
- 浏览器版本
- 发生时间
function reportError(data) { const payload = { ...data, ua: navigator.userAgent, url: window.location.href, resolution: `${window.screen.width}x${window.screen.height}`, lang: navigator.language } // 使用sendBeacon保证页面关闭时也能上报 navigator.sendBeacon('/api/error-log', JSON.stringify(payload)) }4.2 报警阈值设置建议
根据历史数据设置动态阈值:
- 全局错误率突增50%
- 特定CDN域名错误率>2%
- 关键资源连续3次加载失败
- 移动端错误率是桌面端的2倍以上
5. 特殊场景处理经验
5.1 动态注入资源的捕获
对于通过createElement动态添加的资源,需要特殊处理:
const originalCreateElement = document.createElement.bind(document) document.createElement = function(tagName) { const element = originalCreateElement(tagName) if (tagName.toLowerCase() === 'img') { element.onerror = function() { handleImageError(this) } } return element }5.2 Service Worker中的处理
在Service Worker中需要额外监听fetch事件:
self.addEventListener('fetch', event => { event.respondWith( fetch(event.request).catch(err => { reportSwError({ url: event.request.url, type: 'fetch', stack: err.stack }) return caches.match('/fallback') }) ) })5.3 第三方脚本的错误隔离
为防止第三方脚本错误影响主流程:
function loadThirdPartyScript(url) { const iframe = document.createElement('iframe') iframe.style.display = 'none' document.body.appendChild(iframe) const doc = iframe.contentDocument const script = doc.createElement('script') script.src = url script.onerror = () => { iframe.remove() console.warn(`第三方脚本加载失败: ${url}`) } doc.body.appendChild(script) }6. 实际项目中的踩坑记录
- 跨域脚本的详细错误信息: 必须同时满足:
- 脚本添加crossorigin属性
- 服务器返回Access-Control-Allow-Origin
- 脚本必须严格匹配的MIME类型
- error事件重复触发: 图片加载失败后如果修改src属性会再次触发error,需要设置标记位:
let isHandled = false img.onerror = function() { if (isHandled) return isHandled = true // 处理逻辑 }- 浏览器兼容性差异:
- IE8/9不支持标准的error事件
- Safari早期版本对script.error的支持不完整
- 移动端WebView可能有特殊行为
- 性能影响评估: 在低端设备上测试发现,全局error监听会使页面加载时间增加约15-30ms,建议在DOMContentLoaded后再添加非关键监听器。