feat: 添加移动端适配和测试功能
refactor(layout): 优化页脚布局和备案信息展示 feat(constants): 添加ICP备案和公安备案信息 feat(header): 实现移动端加载时的骨架屏效果 style(globals): 调整文字颜色和添加移动端响应样式 feat(breadcrumb): 增加返回按钮和响应式优化 feat(e2e): 添加移动端测试工具和测试用例 docs: 添加页脚重设计文档
This commit is contained in:
@@ -314,4 +314,201 @@ export class PerformanceMonitor {
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
async measureFirstMeaningfulPaint(): Promise<number> {
|
||||
const fmp = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
if (entries.length > 0) {
|
||||
resolve(entries[0].startTime);
|
||||
}
|
||||
});
|
||||
observer.observe({ entryTypes: ['first-meaningful-paint'] });
|
||||
setTimeout(() => resolve(0), 5000);
|
||||
} else {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
return fmp;
|
||||
}
|
||||
|
||||
async measureNetworkPerformance(): Promise<{
|
||||
dnsLookup: number;
|
||||
tcpConnection: number;
|
||||
sslHandshake: number;
|
||||
requestTime: number;
|
||||
responseTime: number;
|
||||
totalTime: number;
|
||||
}> {
|
||||
const timing = await this.page.evaluate(() => {
|
||||
const perf = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
||||
return {
|
||||
dnsLookup: perf.domainLookupEnd - perf.domainLookupStart,
|
||||
tcpConnection: perf.connectEnd - perf.connectStart,
|
||||
sslHandshake: perf.secureConnectionStart > 0 ? perf.connectEnd - perf.secureConnectionStart : 0,
|
||||
requestTime: perf.responseStart - perf.requestStart,
|
||||
responseTime: perf.responseEnd - perf.responseStart,
|
||||
totalTime: perf.loadEventEnd - perf.fetchStart,
|
||||
};
|
||||
});
|
||||
return timing;
|
||||
}
|
||||
|
||||
async measureBatteryImpact(): Promise<{
|
||||
estimatedImpact: string;
|
||||
recommendations: string[];
|
||||
}> {
|
||||
const metrics = await this.collectMetrics();
|
||||
const recommendations: string[] = [];
|
||||
let impact = 'low';
|
||||
|
||||
if (metrics.loadTime > 3000) {
|
||||
recommendations.push('页面加载时间过长,建议优化资源加载');
|
||||
impact = 'high';
|
||||
}
|
||||
if (metrics.firstInputDelay > 100) {
|
||||
recommendations.push('首次输入延迟较高,建议优化JavaScript执行');
|
||||
impact = impact === 'high' ? 'high' : 'medium';
|
||||
}
|
||||
if (metrics.largestContentfulPaint > 2500) {
|
||||
recommendations.push('最大内容绘制时间过长,建议优化关键渲染路径');
|
||||
impact = impact === 'high' ? 'high' : 'medium';
|
||||
}
|
||||
|
||||
return {
|
||||
estimatedImpact: impact,
|
||||
recommendations,
|
||||
};
|
||||
}
|
||||
|
||||
async validateLCP(value: number, threshold: number = 2500): boolean {
|
||||
return value <= threshold;
|
||||
}
|
||||
|
||||
async validateFID(value: number, threshold: number = 100): boolean {
|
||||
return value <= threshold;
|
||||
}
|
||||
|
||||
async validateCLS(value: number, threshold: number = 0.1): boolean {
|
||||
return value <= threshold;
|
||||
}
|
||||
|
||||
async validateTTI(value: number, threshold: number = 3500): boolean {
|
||||
return value <= threshold;
|
||||
}
|
||||
|
||||
async validateTTFB(value: number, threshold: number = 600): boolean {
|
||||
return value <= threshold;
|
||||
}
|
||||
|
||||
async validateFCP(value: number, threshold: number = 1800): boolean {
|
||||
return value <= threshold;
|
||||
}
|
||||
|
||||
async getCoreWebVitalsSummary(): Promise<{
|
||||
lcp: { value: number; threshold: number; passed: boolean };
|
||||
fid: { value: number; threshold: number; passed: boolean };
|
||||
cls: { value: number; threshold: number; passed: boolean };
|
||||
tti: { value: number; threshold: number; passed: boolean };
|
||||
ttfb: { value: number; threshold: number; passed: boolean };
|
||||
fcp: { value: number; threshold: number; passed: boolean };
|
||||
}> {
|
||||
const metrics = await this.collectMetrics();
|
||||
const ttfb = await this.measureFirstByteTime();
|
||||
const fcp = await this.measureFirstContentfulPaint();
|
||||
|
||||
return {
|
||||
lcp: {
|
||||
value: metrics.largestContentfulPaint,
|
||||
threshold: 2500,
|
||||
passed: await this.validateLCP(metrics.largestContentfulPaint),
|
||||
},
|
||||
fid: {
|
||||
value: metrics.firstInputDelay,
|
||||
threshold: 100,
|
||||
passed: await this.validateFID(metrics.firstInputDelay),
|
||||
},
|
||||
cls: {
|
||||
value: metrics.cumulativeLayoutShift,
|
||||
threshold: 0.1,
|
||||
passed: await this.validateCLS(metrics.cumulativeLayoutShift),
|
||||
},
|
||||
tti: {
|
||||
value: metrics.timeToInteractive,
|
||||
threshold: 3500,
|
||||
passed: await this.validateTTI(metrics.timeToInteractive),
|
||||
},
|
||||
ttfb: {
|
||||
value: ttfb,
|
||||
threshold: 600,
|
||||
passed: await this.validateTTFB(ttfb),
|
||||
},
|
||||
fcp: {
|
||||
value: fcp,
|
||||
threshold: 1800,
|
||||
passed: await this.validateFCP(fcp),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async measureFirstByteTime(): Promise<number> {
|
||||
const ttfb = await this.page.evaluate(() => {
|
||||
const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
||||
return timing.responseStart - timing.fetchStart;
|
||||
});
|
||||
return ttfb;
|
||||
}
|
||||
|
||||
async measureMobileSpecificMetrics(): Promise<{
|
||||
touchResponseTime: number;
|
||||
scrollPerformance: number;
|
||||
gestureLatency: number;
|
||||
}> {
|
||||
const touchResponseTime = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
const startTime = performance.now();
|
||||
document.addEventListener('touchstart', () => {
|
||||
resolve(performance.now() - startTime);
|
||||
}, { once: true });
|
||||
setTimeout(() => resolve(0), 1000);
|
||||
});
|
||||
});
|
||||
|
||||
const scrollPerformance = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
const startTime = performance.now();
|
||||
let frames = 0;
|
||||
|
||||
function countFrames() {
|
||||
frames++;
|
||||
if (performance.now() - startTime >= 1000) {
|
||||
resolve(frames);
|
||||
} else {
|
||||
requestAnimationFrame(countFrames);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(countFrames);
|
||||
});
|
||||
});
|
||||
|
||||
const gestureLatency = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
const startTime = performance.now();
|
||||
document.addEventListener('touchmove', () => {
|
||||
resolve(performance.now() - startTime);
|
||||
}, { once: true });
|
||||
setTimeout(() => resolve(0), 500);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
touchResponseTime,
|
||||
scrollPerformance,
|
||||
gestureLatency,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user