在前端监控体系中,静态资源加载失败是常见但容易被忽略的问题。尤其当 HTML 文档中引用的 CSS、JavaScript 文件加载失败时,可能导致页面样式错乱、功能失效,直接影响用户体验。本文将详细介绍如何有效监控并捕获 HTML 加载阶段的资源错误。
为什么需要监控 HTML 加载错误?
页面中常见的静态资源包括:
-
<link>引入的 CSS 样式文件 -
<script>引入的 JavaScript 文件 -
<img>引入的图片资源 -
字体文件、音视频等
这些资源加载失败时,浏览器通常会静默处理,不会向用户显示明显错误提示,但会导致:
-
页面样式丢失或错乱
-
JavaScript 功能无法使用
-
图片无法显示
-
影响用户交互体验
如何捕获 HTML 加载错误?
核心原理:全局错误事件监听
浏览器提供了
window.addEventListener('error', handler, true)接口,通过在捕获阶段(第三个参数为 true)监听全局错误事件,我们可以捕获到资源加载失败的错误。window.addEventListener('error', function (event) {
// 排除非资源加载错误
if (event.target && (event.target.tagName === 'LINK' || event.target.tagName === 'SCRIPT')) {
const target = event.target;
const errorInfo = {
tagName: target.tagName, // 标签类型
resourceType: target.tagName === 'LINK' ? 'CSS' : 'JS', // 资源类型
src: target.src || target.href, // 资源地址
outerHTML: target.outerHTML, // 完整标签
time: new Date().toISOString(), // 发生时间
userAgent: navigator.userAgent, // 浏览器信息
pageURL: window.location.href // 当前页面URL
};
console.error('资源加载失败:', errorInfo);
// 发送错误信息到监控服务器
reportError(errorInfo);
}
}, true);
完整实现方案
以下是一个完整的前端资源加载错误监控方案:
class ResourceErrorMonitor {
constructor(options = {}) {
this.reportURL = options.reportURL || '/api/error-report';
this.sampleRate = options.sampleRate || 1; // 采样率
this.ignoredPatterns = options.ignoredPatterns || [];
this.isInit = false;
}
// 初始化监控
init() {
if (this.isInit) return;
if (Math.random() > this.sampleRate) return;
this.bindErrorListener();
this.bindUnhandledRejection();
this.isInit = true;
console.log('资源错误监控已启动');
}
// 绑定错误监听
bindErrorListener() {
window.addEventListener('error', (event) => {
this.handleResourceError(event);
}, true);
}
// 处理资源错误
handleResourceError(event) {
const target = event.target;
// 只处理特定标签的资源错误
if (!target || !target.tagName) return;
const tagName = target.tagName;
if (!['LINK', 'SCRIPT', 'IMG'].includes(tagName)) return;
// 获取资源URL
let resourceURL = '';
if (tagName === 'LINK') {
resourceURL = target.href;
} else if (tagName === 'SCRIPT' || tagName === 'IMG') {
resourceURL = target.src;
}
// 检查是否在忽略列表中
if (this.shouldIgnore(resourceURL)) return;
// 收集错误信息
const errorInfo = this.collectErrorInfo(event, target, resourceURL);
// 上报错误
this.report(errorInfo);
// 可选的降级处理
this.fallbackHandler(errorInfo);
}
// 收集错误信息
collectErrorInfo(event, target, resourceURL) {
return {
// 基础信息
type: 'resource_error',
timestamp: Date.now(),
pageURL: window.location.href,
// 资源信息
resourceURL: resourceURL,
tagName: target.tagName,
resourceType: this.getResourceType(target),
// 环境信息
userAgent: navigator.userAgent,
language: navigator.language,
referrer: document.referrer,
// 网络信息
online: navigator.onLine,
effectiveType: navigator.connection ? navigator.connection.effectiveType : 'unknown',
// 性能信息
navigationTiming: this.getNavigationTiming()
};
}
// 获取资源类型
getResourceType(element) {
if (element.tagName === 'LINK') {
return element.rel.includes('stylesheet') ? 'CSS' : 'OTHER';
} else if (element.tagName === 'SCRIPT') {
return 'JS';
} else if (element.tagName === 'IMG') {
return 'IMAGE';
}
return 'UNKNOWN';
}
// 获取页面性能数据
getNavigationTiming() {
if (!performance || !performance.timing) return null;
const timing = performance.timing;
return {
dnsTime: timing.domainLookupEnd - timing.domainLookupStart,
tcpTime: timing.connectEnd - timing.connectStart,
requestTime: timing.responseStart - timing.requestStart,
responseTime: timing.responseEnd - timing.responseStart,
domReadyTime: timing.domContentLoadedEventStart - timing.navigationStart,
loadTime: timing.loadEventStart - timing.navigationStart
};
}
// 检查是否忽略该资源
shouldIgnore(url) {
if (!url) return true;
return this.ignoredPatterns.some(pattern => {
if (pattern instanceof RegExp) {
return pattern.test(url);
} else if (typeof pattern === 'string') {
return url.includes(pattern);
}
return false;
});
}
// 降级处理
fallbackHandler(errorInfo) {
// 这里可以实现一些降级策略
// 例如:加载备用资源、显示友好提示等
if (errorInfo.resourceType === 'CSS') {
console.warn(`CSS资源加载失败: ${errorInfo.resourceURL}`);
// 可以在这里加载备用样式
}
}
// 上报错误
report(data) {
// 使用navigator.sendBeacon异步上报,不影响页面卸载
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
navigator.sendBeacon(this.reportURL, blob);
} else {
// 回退方案:使用fetch或Image对象上报
this.fallbackReport(data);
}
}
// 回退上报方案
fallbackReport(data) {
const img = new Image();
const params = new URLSearchParams();
params.append('data', JSON.stringify(data));
img.src = `${this.reportURL}?${params.toString()}`;
}
// 处理未捕获的Promise错误
bindUnhandledRejection() {
window.addEventListener('unhandledrejection', (event) => {
const errorInfo = {
type: 'promise_error',
timestamp: Date.now(),
reason: event.reason ? event.reason.toString() : 'Unknown',
pageURL: window.location.href
};
this.report(errorInfo);
});
}
}
// 使用示例
const monitor = new ResourceErrorMonitor({
reportURL: 'https://your-api.com/error-report',
sampleRate: 0.1, // 10%采样率
ignoredPatterns: [
'analytics.js', // 忽略分析脚本
/localhost/, // 忽略本地资源
'chrome-extension://' // 忽略浏览器扩展
]
});
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => monitor.init());
} else {
monitor.init();
}
服务器端接收错误日志
前端上报的错误信息需要在服务器端进行收集和分析:
// Node.js Express 示例
const express = require('express');
const app = express();
app.use(express.json());
// 错误日志存储
const errorLogs = [];
app.post('/api/error-report', (req, res) => {
const errorData = req.body;
// 添加服务器时间戳
errorData.serverTime = new Date().toISOString();
errorData.clientIP = req.ip;
// 存储到数据库(这里用数组模拟)
errorLogs.push(errorData);
// 实时告警:连续出现同一资源错误
const recentErrors = errorLogs.filter(log =>
log.resourceURL === errorData.resourceURL &&
Date.now() - log.timestamp < 5 * 60 * 1000 // 5分钟内
);
if (recentErrors.length > 10) {
// 发送告警通知
sendAlert({
type: 'resource_error_alert',
resourceURL: errorData.resourceURL,
errorCount: recentErrors.length,
firstOccurrence: recentErrors[0].timestamp
});
}
res.status(200).send('OK');
});
// 错误数据查询接口
app.get('/api/error-logs', (req, res) => {
const { startTime, endTime, resourceType } = req.query;
let filteredLogs = errorLogs;
if (startTime) {
filteredLogs = filteredLogs.filter(log => log.timestamp >= startTime);
}
if (endTime) {
filteredLogs = filteredLogs.filter(log => log.timestamp <= endTime);
}
if (resourceType) {
filteredLogs = filteredLogs.filter(log => log.resourceType === resourceType);
}
res.json({
total: filteredLogs.length,
logs: filteredLogs.slice(-100) // 返回最近100条
});
});
错误分析与可视化
收集到错误数据后,可以进行以下分析:
-
错误趋势分析:统计每天/每周的资源错误数量变化
-
资源错误排行:找出最常出错的资源文件
-
浏览器分布:分析不同浏览器下的错误情况
-
地域分析:结合IP信息分析地域性网络问题
-
关联分析:错误与页面性能指标的关联关系
最佳实践建议
-
合理的采样率:生产环境建议使用 1%-10% 的采样率,避免产生过多监控数据
-
资源忽略列表:忽略第三方统计、浏览器扩展等非核心资源
-
错误聚合:相同资源短时间内多次错误应合并上报
-
实时告警:关键资源频繁出错时应及时告警
-
数据保留策略:设置合理的日志保留期限
-
用户隐私保护:避免收集敏感信息,对用户IP进行脱敏处理
总结
HTML 资源加载错误监控是前端异常监控体系中的重要环节。通过全局错误事件监听,我们可以有效捕获 CSS、JavaScript 等关键资源的加载失败情况。结合完善的错误上报、存储和分析机制,能够帮助开发者快速定位和解决资源加载问题,提升页面稳定性和用户体验。
实现时需要注意性能影响、数据采样、隐私保护等问题,建立完善的监控、告警、分析闭环,让前端异常监控真正为业务稳定性保驾护航。