第194集:Vue 3监控与告警系统
概述
在本集中,我们将深入探讨如何为Vue 3应用构建完整的监控与告警系统。监控系统是生产环境中不可或缺的组成部分,它能帮助我们实时了解应用性能、快速定位问题并进行预警。
一、性能指标体系
1. 核心Web Vitals指标
Web Vitals是Google提出的用于衡量网页用户体验的核心指标,包括:
- **FCP (First Contentful Paint)**:首次内容绘制,衡量页面开始加载到首次渲染文本、图像、非白色画布或SVG的时间。
- **LCP (Largest Contentful Paint)**:最大内容绘制,衡量页面主要内容加载完成的时间。
- **FID (First Input Delay)**:首次输入延迟,衡量用户首次与页面交互到浏览器开始处理的时间。
- **CLS (Cumulative Layout Shift)**:累积布局偏移,衡量页面元素意外移动的程度。
- **TTI (Time to Interactive)**:可交互时间,衡量页面从开始加载到完全可交互的时间。
2. 自定义性能指标
除了Web Vitals,我们还可以监控自定义指标:
- API请求耗时与成功率
- 组件渲染时间
- 内存使用情况
- 错误率与错误类型
- 用户行为轨迹
二、前端监控数据采集
1. 性能监控类实现
// src/utils/PerformanceMonitor.js
/**
* 性能监控类,用于采集和上报前端性能数据
*/
export class PerformanceMonitor {
constructor(options = {}) {
this.options = {
sampleRate: 1, // 采样率,1表示100%
上报URL: '/api/monitor/performance',
...options
};
this.performanceData = {};
}
/**
* 初始化性能监控
*/
init() {
// 采样控制
if (Math.random() > this.options.sampleRate) return;
// 监听页面加载完成
if (document.readyState === 'complete') {
this.collectPerformanceData();
} else {
window.addEventListener('load', () => this.collectPerformanceData());
}
// 监听页面卸载
window.addEventListener('beforeunload', () => this.reportPerformanceData());
}
/**
* 采集性能数据
*/
collectPerformanceData() {
if (!window.performance) return;
const navigation = performance.getEntriesByType('navigation')[0];
const resource = performance.getEntriesByType('resource');
// 采集核心指标
this.performanceData = {
// 页面加载时间
loadTime: navigation.loadEventEnd - navigation.fetchStart,
// DOM解析时间
domParseTime: navigation.domContentLoadedEventEnd - navigation.fetchStart,
// 首次内容绘制
fcp: this.getFCP(),
// 最大内容绘制
lcp: this.getLCP(),
// 资源加载情况
resourceCount: resource.length,
resourceDetails: resource.map(item => ({
name: item.name,
type: item.initiatorType,
duration: item.duration,
size: item.transferSize
})),
// 时间戳
timestamp: Date.now(),
// 用户信息
userAgent: navigator.userAgent,
url: window.location.href
};
}
/**
* 获取FCP指标
*/
async getFCP() {
try {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntriesByName('first-contentful-paint');
if (entries.length > 0) {
this.performanceData.fcp = entries[0].startTime;
}
});
observer.observe({ entryTypes: ['paint'] });
} catch (error) {
console.error('获取FCP失败:', error);
}
}
/**
* 获取LCP指标
*/
async getLCP() {
try {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
this.performanceData.lcp = lastEntry.startTime;
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
} catch (error) {
console.error('获取LCP失败:', error);
}
}
/**
* 上报性能数据
*/
reportPerformanceData() {
if (Object.keys(this.performanceData).length === 0) return;
// 使用navigator.sendBeacon发送数据,确保页面卸载时也能上报
if (navigator.sendBeacon) {
navigator.sendBeacon(
this.options.上报URL,
JSON.stringify(this.performanceData)
);
} else {
// 降级方案
fetch(this.options.上报URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.performanceData),
keepalive: true
});
}
}
}2. 错误监控类实现
// src/utils/ErrorMonitor.js
/**
* 错误监控类,用于捕获和上报前端错误
*/
export class ErrorMonitor {
constructor(options = {}) {
this.options = {
上报URL: '/api/monitor/error',
sampleRate: 1,
...options
};
this.errors = [];
}
/**
* 初始化错误监控
*/
init() {
// 采样控制
if (Math.random() > this.options.sampleRate) return;
// 监听全局错误
window.addEventListener('error', (event) => this.handleError(event));
// 监听未捕获的Promise错误
window.addEventListener('unhandledrejection', (event) => this.handlePromiseError(event));
// 监听Vue组件错误
this.setupVueErrorHandler();
}
/**
* 配置Vue错误处理器
*/
setupVueErrorHandler() {
// 在main.js中使用:app.config.errorHandler = errorMonitor.handleVueError.bind(errorMonitor);
}
/**
* 处理Vue组件错误
*/
handleVueError(err, vm, info) {
const errorData = {
type: 'vue',
message: err.message,
stack: err.stack,
info,
componentName: vm.$options.name || vm.$options._componentTag,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
};
this.errors.push(errorData);
this.reportErrors();
}
/**
* 处理全局错误
*/
handleError(event) {
const errorData = {
type: 'js',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
};
this.errors.push(errorData);
this.reportErrors();
}
/**
* 处理Promise错误
*/
handlePromiseError(event) {
const errorData = {
type: 'promise',
message: event.reason?.message || 'Unhandled Promise Rejection',
stack: event.reason?.stack,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
};
this.errors.push(errorData);
this.reportErrors();
}
/**
* 上报错误数据
*/
reportErrors() {
if (this.errors.length === 0) return;
// 批量上报,每次最多上报10条
const reportErrors = this.errors.splice(0, 10);
fetch(this.options.上报URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ errors: reportErrors })
});
}
}三、实时监控面板实现
1. 使用ECharts实现可视化
<!-- src/components/MonitorDashboard.vue -->
<template>
<div class="monitor-dashboard">
<h2>实时监控面板</h2>
<div class="dashboard-grid">
<!-- 核心指标卡片 -->
<div class="metric-card" v-for="metric in metrics" :key="metric.name">
<div class="metric-name">{{ metric.name }}</div>
<div class="metric-value">{{ metric.value }}</div>
<div class="metric-trend" :class="metric.trend > 0 ? 'trend-up' : 'trend-down'">
{{ metric.trend > 0 ? '↑' : '↓' }} {{ Math.abs(metric.trend) }}%
</div>
</div>
<!-- 性能趋势图 -->
<div class="chart-container">
<h3>性能趋势</h3>
<div ref="performanceChart" class="chart"></div>
</div>
<!-- 错误分布饼图 -->
<div class="chart-container">
<h3>错误分布</h3>
<div ref="errorChart" class="chart"></div>
</div>
<!-- 最近错误列表 -->
<div class="error-list-container">
<h3>最近错误</h3>
<div class="error-list">
<div class="error-item" v-for="error in recentErrors" :key="error.id">
<div class="error-message">{{ error.message }}</div>
<div class="error-meta">{{ error.type }} - {{ formatTime(error.timestamp) }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, reactive } from 'vue';
import * as echarts from 'echarts';
// 核心指标数据
const metrics = reactive([
{ name: 'FCP', value: '1.2s', trend: -5 },
{ name: 'LCP', value: '2.5s', trend: -2 },
{ name: 'FID', value: '10ms', trend: 3 },
{ name: 'CLS', value: '0.02', trend: -10 },
{ name: '错误率', value: '0.1%', trend: -20 },
{ name: 'API成功率', value: '99.9%', trend: 0.5 }
]);
// 最近错误数据
const recentErrors = ref([
{ id: 1, message: 'ReferenceError: xxx is not defined', type: 'js', timestamp: Date.now() - 10000 },
{ id: 2, message: 'Failed to fetch', type: 'api', timestamp: Date.now() - 20000 },
{ id: 3, message: 'TypeError: Cannot read property of undefined', type: 'js', timestamp: Date.now() - 30000 }
]);
// 图表实例
let performanceChart = null;
let errorChart = null;
// 性能趋势图引用
const performanceChartRef = ref(null);
// 错误分布饼图引用
const errorChartRef = ref(null);
// 格式化时间
const formatTime = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleTimeString();
};
// 初始化性能趋势图
const initPerformanceChart = () => {
performanceChart = echarts.init(performanceChartRef.value);
const option = {
xAxis: {
type: 'category',
data: ['00:00', '01:00', '02:00', '03:00', '04:00', '05:00', '06:00']
},
yAxis: {
type: 'value',
name: '时间(ms)'
},
series: [
{
name: 'FCP',
type: 'line',
data: [1200, 1100, 1300, 1250, 1150, 1350, 1200]
},
{
name: 'LCP',
type: 'line',
data: [2500, 2400, 2600, 2550, 2450, 2650, 2500]
}
]
};
performanceChart.setOption(option);
};
// 初始化错误分布饼图
const initErrorChart = () => {
errorChart = echarts.init(errorChartRef.value);
const option = {
series: [
{
type: 'pie',
data: [
{ value: 60, name: 'JavaScript错误' },
{ value: 20, name: 'API错误' },
{ value: 10, name: 'Vue组件错误' },
{ value: 10, name: '其他错误' }
]
}
]
};
errorChart.setOption(option);
};
// 监听窗口大小变化,自适应图表
const handleResize = () => {
performanceChart?.resize();
errorChart?.resize();
};
onMounted(() => {
initPerformanceChart();
initErrorChart();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
performanceChart?.dispose();
errorChart?.dispose();
window.removeEventListener('resize', handleResize);
});
</script>
<style scoped>
.monitor-dashboard {
padding: 20px;
background-color: #f5f7fa;
border-radius: 8px;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.metric-card {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
text-align: center;
}
.metric-name {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.metric-value {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.metric-trend {
font-size: 12px;
font-weight: bold;
}
.trend-up {
color: #f56c6c;
}
.trend-down {
color: #67c23a;
}
.chart-container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
grid-column: span 2;
}
.chart {
width: 100%;
height: 300px;
margin-top: 10px;
}
.error-list-container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
grid-column: span 2;
}
.error-list {
max-height: 300px;
overflow-y: auto;
margin-top: 10px;
}
.error-item {
padding: 15px;
border-bottom: 1px solid #eee;
}
.error-item:last-child {
border-bottom: none;
}
.error-message {
font-weight: bold;
margin-bottom: 5px;
color: #f56c6c;
}
.error-meta {
font-size: 12px;
color: #999;
}
</style>四、告警系统实现
1. 告警规则配置
// src/config/alertRules.js
/**
* 告警规则配置
*/
export const alertRules = [
{
id: 1,
name: 'FCP超时',
metric: 'fcp',
operator: '>',
threshold: 2000, // 2秒
duration: 60, // 持续60秒
level: 'warning', // warning, error, critical
message: 'FCP超过2秒,当前值: {{value}}ms',
通知渠道: ['email', 'slack']
},
{
id: 2,
name: '错误率过高',
metric: 'errorRate',
operator: '>',
threshold: 1, // 1%
duration: 300, // 持续5分钟
level: 'error',
message: '错误率超过1%,当前值: {{value}}%',
通知渠道: ['email', 'slack', 'sms']
},
{
id: 3,
name: 'API成功率过低',
metric: 'apiSuccessRate',
operator: '<',
threshold: 99, // 99%
duration: 120, // 持续2分钟
level: 'critical',
message: 'API成功率低于99%,当前值: {{value}}%',
通知渠道: ['email', 'slack', 'sms', 'phone']
}
];2. 告警引擎实现
// src/services/AlertEngine.js
import { alertRules } from '../config/alertRules';
/**
* 告警引擎,用于根据监控数据触发告警
*/
export class AlertEngine {
constructor() {
this.alertStates = new Map(); // 存储告警状态
this.dataBuffer = new Map(); // 存储最近的数据
this.checkInterval = 10000; // 每10秒检查一次
this.start();
}
/**
* 启动告警引擎
*/
start() {
setInterval(() => this.checkAlerts(), this.checkInterval);
}
/**
* 添加监控数据
*/
addData(metric, value, timestamp = Date.now()) {
if (!this.dataBuffer.has(metric)) {
this.dataBuffer.set(metric, []);
}
const buffer = this.dataBuffer.get(metric);
buffer.push({ value, timestamp });
// 只保留最近5分钟的数据
const fiveMinutesAgo = timestamp - 5 * 60 * 1000;
this.dataBuffer.set(metric, buffer.filter(item => item.timestamp > fiveMinutesAgo));
}
/**
* 检查告警规则
*/
checkAlerts() {
const now = Date.now();
alertRules.forEach(rule => {
const buffer = this.dataBuffer.get(rule.metric) || [];
// 过滤出最近duration时间内的数据
const recentData = buffer.filter(item => {
return now - item.timestamp <= rule.duration * 1000;
});
if (recentData.length === 0) return;
// 计算平均值
const avgValue = recentData.reduce((sum, item) => sum + item.value, 0) / recentData.length;
// 检查是否触发告警
const isTriggered = this.checkCondition(avgValue, rule.operator, rule.threshold);
const currentState = this.alertStates.get(rule.id) || { triggered: false, lastTriggered: 0 };
if (isTriggered && !currentState.triggered) {
// 触发告警
this.triggerAlert(rule, avgValue);
this.alertStates.set(rule.id, { triggered: true, lastTriggered: now });
} else if (!isTriggered && currentState.triggered) {
// 恢复告警
this.resolveAlert(rule, avgValue);
this.alertStates.set(rule.id, { triggered: false, lastTriggered: 0 });
}
});
}
/**
* 检查条件是否满足
*/
checkCondition(value, operator, threshold) {
switch (operator) {
case '>':
return value > threshold;
case '<':
return value < threshold;
case '>=':
return value >= threshold;
case '<=':
return value <= threshold;
case '==':
return value === threshold;
case '!=':
return value !== threshold;
default:
return false;
}
}
/**
* 触发告警
*/
triggerAlert(rule, value) {
const message = rule.message.replace('{{value}}', value.toFixed(2));
const alertData = {
ruleId: rule.id,
name: rule.name,
level: rule.level,
message,
metric: rule.metric,
value,
timestamp: Date.now()
};
// 发送告警
this.sendAlert(alertData, rule.通知渠道);
console.warn(`[告警触发] ${message}`);
}
/**
* 恢复告警
*/
resolveAlert(rule, value) {
const message = `${rule.name}已恢复正常,当前值: ${value.toFixed(2)}`;
const alertData = {
ruleId: rule.id,
name: rule.name,
level: 'resolved',
message,
metric: rule.metric,
value,
timestamp: Date.now()
};
// 发送恢复通知
this.sendAlert(alertData, rule.通知渠道);
console.info(`[告警恢复] ${message}`);
}
/**
* 发送告警通知
*/
sendAlert(alertData, channels) {
// 实现各种通知渠道的发送逻辑
channels.forEach(channel => {
switch (channel) {
case 'email':
this.sendEmail(alertData);
break;
case 'slack':
this.sendSlack(alertData);
break;
case 'sms':
this.sendSMS(alertData);
break;
case 'phone':
this.sendPhoneCall(alertData);
break;
default:
break;
}
});
}
// 各种通知渠道的实现
sendEmail(alertData) {
// 邮件发送逻辑
}
sendSlack(alertData) {
// Slack发送逻辑
}
sendSMS(alertData) {
// SMS发送逻辑
}
sendPhoneCall(alertData) {
// 电话呼叫逻辑
}
}五、集成到Vue 3应用
1. 在main.js中初始化监控
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import { PerformanceMonitor } from './utils/PerformanceMonitor';
import { ErrorMonitor } from './utils/ErrorMonitor';
const app = createApp(App);
// 初始化性能监控
const performanceMonitor = new PerformanceMonitor({
sampleRate: 0.5, // 50%采样率
上报URL: '/api/monitor/performance'
});
performanceMonitor.init();
// 初始化错误监控
const errorMonitor = new ErrorMonitor({
sampleRate: 1, // 100%采样率
上报URL: '/api/monitor/error'
});
errorMonitor.init();
app.config.errorHandler = errorMonitor.handleVueError.bind(errorMonitor);
app.mount('#app');2. 在管理后台使用监控面板
<!-- src/views/MonitorView.vue -->
<template>
<div class="monitor-view">
<h1>应用监控中心</h1>
<MonitorDashboard />
</div>
</template>
<script setup>
import MonitorDashboard from '../components/MonitorDashboard.vue';
</script>
<style scoped>
.monitor-view {
padding: 20px;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
color: #333;
}
</style>六、最佳实践
- 合理设置采样率:根据应用规模和服务器负载,设置合适的采样率,避免过多数据占用资源。
- 分级告警:根据问题严重程度设置不同级别的告警,避免告警风暴。
- 告警抑制:对于同一问题,设置合理的告警间隔,避免重复告警。
- 数据可视化:使用图表直观展示监控数据,便于分析和定位问题。
- 定期分析:定期分析监控数据,找出性能瓶颈和潜在问题。
- 持续优化:根据监控结果,持续优化应用性能和稳定性。
总结
在本集中,我们构建了一个完整的Vue 3监控与告警系统,包括:
- 性能指标体系设计,涵盖Web Vitals和自定义指标
- 前端监控数据采集类,用于收集性能数据和错误信息
- 实时监控面板,使用ECharts可视化展示监控数据
- 告警规则配置和告警引擎实现
- 集成到Vue 3应用的方法
通过这套监控与告警系统,我们可以实时了解应用性能状况,快速定位和解决问题,提高应用的可靠性和用户体验。
在下一集中,我们将探讨日志收集与分析系统的实现。