性能优化是 UniApp 开发中的重要环节,良好的性能可以提升用户体验,减少用户流失。本章将详细介绍 UniApp 的性能分析方法、优化策略和调试技巧。
7.1 性能分析
7.1.1 性能指标
关键性能指标
// utils/performance.js
class PerformanceMonitor {
constructor() {
this.metrics = {
// 页面加载时间
pageLoadTime: 0,
// 首屏渲染时间
firstPaint: 0,
// 可交互时间
timeToInteractive: 0,
// 内存使用情况
memoryUsage: 0,
// 网络请求时间
networkTime: 0,
// 组件渲染时间
componentRenderTime: 0
};
this.observers = [];
this.init();
}
init() {
// 监听页面加载
this.observePageLoad();
// 监听网络请求
this.observeNetwork();
// 监听内存使用
this.observeMemory();
// 监听组件渲染
this.observeComponentRender();
}
// 页面加载性能监控
observePageLoad() {
const startTime = Date.now();
// 页面显示时记录加载时间
uni.onPageShow(() => {
this.metrics.pageLoadTime = Date.now() - startTime;
this.reportMetric('pageLoadTime', this.metrics.pageLoadTime);
});
}
// 网络请求性能监控
observeNetwork() {
const originalRequest = uni.request;
uni.request = (options) => {
const startTime = Date.now();
const originalSuccess = options.success;
const originalFail = options.fail;
const originalComplete = options.complete;
options.success = (res) => {
const endTime = Date.now();
const duration = endTime - startTime;
this.recordNetworkMetric({
url: options.url,
method: options.method || 'GET',
duration,
status: res.statusCode,
size: JSON.stringify(res.data).length
});
originalSuccess && originalSuccess(res);
};
options.fail = (err) => {
const endTime = Date.now();
const duration = endTime - startTime;
this.recordNetworkMetric({
url: options.url,
method: options.method || 'GET',
duration,
status: 'error',
error: err
});
originalFail && originalFail(err);
};
return originalRequest(options);
};
}
// 内存使用监控
observeMemory() {
// #ifdef APP-PLUS
setInterval(() => {
plus.runtime.getProperty(plus.runtime.appid, (info) => {
this.metrics.memoryUsage = info.memory;
this.reportMetric('memoryUsage', info.memory);
});
}, 5000);
// #endif
// #ifdef H5
if (performance.memory) {
setInterval(() => {
this.metrics.memoryUsage = performance.memory.usedJSHeapSize;
this.reportMetric('memoryUsage', performance.memory.usedJSHeapSize);
}, 5000);
}
// #endif
}
// 组件渲染性能监控
observeComponentRender() {
// Vue 组件渲染时间监控
const originalMount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el, hydrating) {
const startTime = performance.now();
const result = originalMount.call(this, el, hydrating);
this.$nextTick(() => {
const endTime = performance.now();
const renderTime = endTime - startTime;
PerformanceMonitor.instance.recordComponentRender({
component: this.$options.name || 'Anonymous',
renderTime
});
});
return result;
};
}
// 记录网络请求指标
recordNetworkMetric(metric) {
console.log('Network Metric:', metric);
// 发送到分析服务
this.sendToAnalytics('network', metric);
}
// 记录组件渲染指标
recordComponentRender(metric) {
console.log('Component Render Metric:', metric);
// 发送到分析服务
this.sendToAnalytics('component', metric);
}
// 报告性能指标
reportMetric(name, value) {
console.log(`Performance Metric - ${name}:`, value);
// 发送到分析服务
this.sendToAnalytics('performance', { name, value });
}
// 发送数据到分析服务
sendToAnalytics(type, data) {
// 这里可以集成第三方分析服务
// 如 Google Analytics、百度统计等
// 示例:发送到自定义分析服务
uni.request({
url: 'https://api.example.com/analytics',
method: 'POST',
data: {
type,
data,
timestamp: Date.now(),
userAgent: navigator.userAgent,
platform: uni.getSystemInfoSync().platform
}
});
}
// 获取性能报告
getPerformanceReport() {
return {
metrics: this.metrics,
timestamp: Date.now(),
platform: uni.getSystemInfoSync()
};
}
// 清理监控器
destroy() {
this.observers.forEach(observer => {
if (observer.disconnect) {
observer.disconnect();
}
});
}
}
// 创建全局实例
PerformanceMonitor.instance = new PerformanceMonitor();
export default PerformanceMonitor;
性能数据收集
// utils/analytics.js
class AnalyticsManager {
constructor() {
this.queue = [];
this.isOnline = true;
this.batchSize = 10;
this.flushInterval = 30000; // 30秒
this.init();
}
init() {
// 监听网络状态
uni.onNetworkStatusChange((res) => {
this.isOnline = res.isConnected;
if (this.isOnline && this.queue.length > 0) {
this.flush();
}
});
// 定时发送数据
setInterval(() => {
if (this.queue.length > 0) {
this.flush();
}
}, this.flushInterval);
// 应用进入后台时发送数据
uni.onAppHide(() => {
this.flush();
});
}
// 添加事件到队列
track(event, properties = {}) {
const eventData = {
event,
properties: {
...properties,
timestamp: Date.now(),
platform: uni.getSystemInfoSync().platform,
version: uni.getSystemInfoSync().version
}
};
this.queue.push(eventData);
// 队列满时立即发送
if (this.queue.length >= this.batchSize) {
this.flush();
}
}
// 发送队列中的数据
flush() {
if (!this.isOnline || this.queue.length === 0) {
return;
}
const data = [...this.queue];
this.queue = [];
uni.request({
url: 'https://api.example.com/analytics/batch',
method: 'POST',
data: {
events: data
},
success: (res) => {
console.log('Analytics data sent successfully');
},
fail: (err) => {
console.error('Failed to send analytics data:', err);
// 重新加入队列
this.queue.unshift(...data);
}
});
}
// 页面访问统计
trackPageView(pageName, properties = {}) {
this.track('page_view', {
page_name: pageName,
...properties
});
}
// 用户行为统计
trackUserAction(action, properties = {}) {
this.track('user_action', {
action,
...properties
});
}
// 错误统计
trackError(error, properties = {}) {
this.track('error', {
error_message: error.message,
error_stack: error.stack,
...properties
});
}
// 性能统计
trackPerformance(metric, value, properties = {}) {
this.track('performance', {
metric,
value,
...properties
});
}
}
export default new AnalyticsManager();
7.1.2 性能监控工具
自定义性能监控面板
<!-- components/PerformancePanel.vue -->
<template>
<view v-if="visible" class="performance-panel">
<view class="panel-header">
<text class="panel-title">性能监控</text>
<view class="panel-controls">
<button @click="toggleRecording" :class="recordingClass">
{{ recording ? '停止' : '开始' }}
</button>
<button @click="clearData">清除</button>
<button @click="exportData">导出</button>
<button @click="close">关闭</button>
</view>
</view>
<scroll-view class="panel-content" scroll-y>
<!-- 实时指标 -->
<view class="metrics-section">
<text class="section-title">实时指标</text>
<view class="metrics-grid">
<view class="metric-item">
<text class="metric-label">FPS</text>
<text class="metric-value">{{ metrics.fps }}</text>
</view>
<view class="metric-item">
<text class="metric-label">内存</text>
<text class="metric-value">{{ formatMemory(metrics.memory) }}</text>
</view>
<view class="metric-item">
<text class="metric-label">网络</text>
<text class="metric-value">{{ metrics.networkLatency }}ms</text>
</view>
<view class="metric-item">
<text class="metric-label">渲染</text>
<text class="metric-value">{{ metrics.renderTime }}ms</text>
</view>
</view>
</view>
<!-- 性能图表 -->
<view class="chart-section">
<text class="section-title">性能趋势</text>
<canvas
canvas-id="performanceChart"
class="performance-chart"
@touchstart="onChartTouch"
></canvas>
</view>
<!-- 网络请求列表 -->
<view class="requests-section">
<text class="section-title">网络请求</text>
<view class="request-list">
<view
v-for="request in networkRequests"
:key="request.id"
class="request-item"
:class="getRequestClass(request)"
>
<view class="request-info">
<text class="request-method">{{ request.method }}</text>
<text class="request-url">{{ request.url }}</text>
<text class="request-status">{{ request.status }}</text>
</view>
<view class="request-timing">
<text class="request-duration">{{ request.duration }}ms</text>
<text class="request-size">{{ formatSize(request.size) }}</text>
</view>
</view>
</view>
</view>
<!-- 组件渲染统计 -->
<view class="components-section">
<text class="section-title">组件渲染</text>
<view class="component-list">
<view
v-for="component in componentStats"
:key="component.name"
class="component-item"
>
<text class="component-name">{{ component.name }}</text>
<text class="component-count">{{ component.renderCount }}</text>
<text class="component-time">{{ component.totalTime }}ms</text>
<text class="component-avg">{{ component.avgTime }}ms</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
name: 'PerformancePanel',
data() {
return {
visible: false,
recording: false,
metrics: {
fps: 60,
memory: 0,
networkLatency: 0,
renderTime: 0
},
networkRequests: [],
componentStats: [],
chartData: [],
updateTimer: null
};
},
computed: {
recordingClass() {
return {
'btn-recording': this.recording,
'btn-stopped': !this.recording
};
}
},
methods: {
// 显示面板
show() {
this.visible = true;
this.startRecording();
},
// 隐藏面板
close() {
this.visible = false;
this.stopRecording();
},
// 开始/停止记录
toggleRecording() {
if (this.recording) {
this.stopRecording();
} else {
this.startRecording();
}
},
// 开始记录
startRecording() {
this.recording = true;
// 开始更新指标
this.updateTimer = setInterval(() => {
this.updateMetrics();
}, 1000);
// 监听性能事件
this.listenToPerformanceEvents();
},
// 停止记录
stopRecording() {
this.recording = false;
if (this.updateTimer) {
clearInterval(this.updateTimer);
this.updateTimer = null;
}
},
// 更新性能指标
updateMetrics() {
// 更新 FPS
this.updateFPS();
// 更新内存使用
this.updateMemoryUsage();
// 更新网络延迟
this.updateNetworkLatency();
// 更新图表数据
this.updateChartData();
},
// 更新 FPS
updateFPS() {
let lastTime = performance.now();
let frameCount = 0;
const countFPS = () => {
frameCount++;
const currentTime = performance.now();
if (currentTime - lastTime >= 1000) {
this.metrics.fps = Math.round((frameCount * 1000) / (currentTime - lastTime));
frameCount = 0;
lastTime = currentTime;
}
if (this.recording) {
requestAnimationFrame(countFPS);
}
};
requestAnimationFrame(countFPS);
},
// 更新内存使用
updateMemoryUsage() {
// #ifdef H5
if (performance.memory) {
this.metrics.memory = performance.memory.usedJSHeapSize;
}
// #endif
// #ifdef APP-PLUS
plus.runtime.getProperty(plus.runtime.appid, (info) => {
this.metrics.memory = info.memory * 1024 * 1024; // 转换为字节
});
// #endif
},
// 更新网络延迟
updateNetworkLatency() {
const startTime = Date.now();
uni.request({
url: 'https://api.example.com/ping',
method: 'GET',
timeout: 5000,
success: () => {
this.metrics.networkLatency = Date.now() - startTime;
},
fail: () => {
this.metrics.networkLatency = -1; // 表示网络不可用
}
});
},
// 监听性能事件
listenToPerformanceEvents() {
// 监听网络请求
this.$on('network-request', this.onNetworkRequest);
// 监听组件渲染
this.$on('component-render', this.onComponentRender);
},
// 处理网络请求事件
onNetworkRequest(request) {
this.networkRequests.unshift({
id: Date.now(),
...request
});
// 只保留最近的 50 个请求
if (this.networkRequests.length > 50) {
this.networkRequests = this.networkRequests.slice(0, 50);
}
},
// 处理组件渲染事件
onComponentRender(renderInfo) {
const existing = this.componentStats.find(c => c.name === renderInfo.component);
if (existing) {
existing.renderCount++;
existing.totalTime += renderInfo.renderTime;
existing.avgTime = existing.totalTime / existing.renderCount;
} else {
this.componentStats.push({
name: renderInfo.component,
renderCount: 1,
totalTime: renderInfo.renderTime,
avgTime: renderInfo.renderTime
});
}
},
// 更新图表数据
updateChartData() {
const now = Date.now();
this.chartData.push({
timestamp: now,
fps: this.metrics.fps,
memory: this.metrics.memory,
networkLatency: this.metrics.networkLatency
});
// 只保留最近 60 个数据点(1分钟)
if (this.chartData.length > 60) {
this.chartData = this.chartData.slice(-60);
}
this.drawChart();
},
// 绘制性能图表
drawChart() {
const ctx = uni.createCanvasContext('performanceChart', this);
const width = 300;
const height = 150;
// 清除画布
ctx.clearRect(0, 0, width, height);
if (this.chartData.length < 2) return;
// 绘制 FPS 曲线
ctx.setStrokeStyle('#00ff00');
ctx.setLineWidth(2);
ctx.beginPath();
this.chartData.forEach((point, index) => {
const x = (index / (this.chartData.length - 1)) * width;
const y = height - (point.fps / 60) * height;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
ctx.draw();
},
// 清除数据
clearData() {
this.networkRequests = [];
this.componentStats = [];
this.chartData = [];
},
// 导出数据
exportData() {
const data = {
metrics: this.metrics,
networkRequests: this.networkRequests,
componentStats: this.componentStats,
chartData: this.chartData,
timestamp: Date.now()
};
// 将数据转换为 JSON 字符串
const jsonData = JSON.stringify(data, null, 2);
// #ifdef H5
// 在 H5 平台下载文件
const blob = new Blob([jsonData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `performance-data-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
// #endif
// #ifdef APP-PLUS
// 在 App 平台保存文件
plus.io.requestFileSystem(plus.io.PUBLIC_DOCUMENTS, (fs) => {
fs.root.getFile(
`performance-data-${Date.now()}.json`,
{ create: true },
(fileEntry) => {
fileEntry.createWriter((writer) => {
writer.write(jsonData);
uni.showToast({
title: '数据已导出',
icon: 'success'
});
});
}
);
});
// #endif
},
// 格式化内存大小
formatMemory(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
// 格式化文件大小
formatSize(bytes) {
return this.formatMemory(bytes);
},
// 获取请求状态样式
getRequestClass(request) {
if (request.status >= 200 && request.status < 300) {
return 'request-success';
} else if (request.status >= 400) {
return 'request-error';
} else {
return 'request-pending';
}
},
// 图表触摸事件
onChartTouch(e) {
// 处理图表交互
console.log('Chart touched:', e);
}
},
beforeDestroy() {
this.stopRecording();
}
};
</script>
<style scoped>
.performance-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
z-index: 9999;
display: flex;
flex-direction: column;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background-color: #333;
border-bottom: 1px solid #555;
}
.panel-title {
color: #fff;
font-size: 18px;
font-weight: bold;
}
.panel-controls {
display: flex;
gap: 8px;
}
.panel-controls button {
padding: 8px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.btn-recording {
background-color: #ff4444;
color: #fff;
}
.btn-stopped {
background-color: #44ff44;
color: #000;
}
.panel-content {
flex: 1;
padding: 16px;
color: #fff;
}
.section-title {
display: block;
font-size: 16px;
font-weight: bold;
margin-bottom: 12px;
color: #00ff00;
}
.metrics-section {
margin-bottom: 24px;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.metric-item {
background-color: #444;
padding: 12px;
border-radius: 8px;
text-align: center;
}
.metric-label {
display: block;
font-size: 12px;
color: #ccc;
margin-bottom: 4px;
}
.metric-value {
display: block;
font-size: 18px;
font-weight: bold;
color: #00ff00;
}
.chart-section {
margin-bottom: 24px;
}
.performance-chart {
width: 100%;
height: 150px;
background-color: #222;
border-radius: 8px;
}
.requests-section,
.components-section {
margin-bottom: 24px;
}
.request-list,
.component-list {
max-height: 200px;
overflow-y: auto;
}
.request-item,
.component-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
margin-bottom: 4px;
background-color: #444;
border-radius: 4px;
font-size: 12px;
}
.request-success {
border-left: 4px solid #00ff00;
}
.request-error {
border-left: 4px solid #ff0000;
}
.request-pending {
border-left: 4px solid #ffff00;
}
.request-info {
display: flex;
flex-direction: column;
flex: 1;
}
.request-method {
color: #00ff00;
font-weight: bold;
}
.request-url {
color: #ccc;
margin: 2px 0;
}
.request-status {
color: #fff;
}
.request-timing {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.request-duration,
.request-size {
color: #00ff00;
}
.component-name {
flex: 1;
color: #fff;
}
.component-count,
.component-time,
.component-avg {
color: #00ff00;
margin-left: 12px;
}
</style>
7.4.2 错误监控与上报
// utils/errorMonitor.js
class ErrorMonitor {
constructor(options = {}) {
this.options = {
maxErrors: 100,
reportUrl: '',
enableConsoleLog: true,
enableLocalStorage: true,
...options
};
this.errors = [];
this.init();
}
// 初始化错误监控
init() {
// 监听全局错误
this.setupGlobalErrorHandler();
// 监听 Promise 错误
this.setupPromiseErrorHandler();
// 监听 Vue 错误
this.setupVueErrorHandler();
// 监听网络错误
this.setupNetworkErrorHandler();
}
// 设置全局错误处理
setupGlobalErrorHandler() {
// #ifdef H5
window.addEventListener('error', (event) => {
this.captureError({
type: 'javascript',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error ? event.error.stack : '',
timestamp: Date.now()
});
});
// #endif
}
// 设置 Promise 错误处理
setupPromiseErrorHandler() {
// #ifdef H5
window.addEventListener('unhandledrejection', (event) => {
this.captureError({
type: 'promise',
message: event.reason ? event.reason.message : 'Unhandled Promise Rejection',
stack: event.reason ? event.reason.stack : '',
timestamp: Date.now()
});
});
// #endif
}
// 设置 Vue 错误处理
setupVueErrorHandler() {
const originalErrorHandler = Vue.config.errorHandler;
Vue.config.errorHandler = (err, vm, info) => {
this.captureError({
type: 'vue',
message: err.message,
stack: err.stack,
componentName: vm ? vm.$options.name || vm.$options._componentTag : '',
propsData: vm ? vm.$options.propsData : {},
info,
timestamp: Date.now()
});
// 调用原始错误处理器
if (originalErrorHandler) {
originalErrorHandler(err, vm, info);
}
};
}
// 设置网络错误处理
setupNetworkErrorHandler() {
const originalRequest = uni.request;
uni.request = (options) => {
const originalFail = options.fail;
options.fail = (err) => {
this.captureError({
type: 'network',
message: `Network request failed: ${options.url}`,
url: options.url,
method: options.method || 'GET',
data: options.data,
error: err,
timestamp: Date.now()
});
originalFail && originalFail(err);
};
return originalRequest(options);
};
}
// 捕获错误
captureError(error) {
// 添加设备信息
error.deviceInfo = this.getDeviceInfo();
// 添加用户信息
error.userInfo = this.getUserInfo();
// 添加页面信息
error.pageInfo = this.getPageInfo();
// 存储错误
this.errors.unshift(error);
// 限制错误数量
if (this.errors.length > this.options.maxErrors) {
this.errors = this.errors.slice(0, this.options.maxErrors);
}
// 控制台输出
if (this.options.enableConsoleLog) {
console.error('Error captured:', error);
}
// 本地存储
if (this.options.enableLocalStorage) {
this.saveToLocalStorage();
}
// 上报错误
if (this.options.reportUrl) {
this.reportError(error);
}
}
// 获取设备信息
getDeviceInfo() {
return {
platform: uni.getSystemInfoSync().platform,
system: uni.getSystemInfoSync().system,
version: uni.getSystemInfoSync().version,
model: uni.getSystemInfoSync().model,
pixelRatio: uni.getSystemInfoSync().pixelRatio,
screenWidth: uni.getSystemInfoSync().screenWidth,
screenHeight: uni.getSystemInfoSync().screenHeight,
windowWidth: uni.getSystemInfoSync().windowWidth,
windowHeight: uni.getSystemInfoSync().windowHeight
};
}
// 获取用户信息
getUserInfo() {
// 从 Vuex 或其他状态管理中获取用户信息
try {
const userInfo = uni.getStorageSync('userInfo');
return {
userId: userInfo ? userInfo.id : 'anonymous',
username: userInfo ? userInfo.username : 'anonymous'
};
} catch (e) {
return {
userId: 'anonymous',
username: 'anonymous'
};
}
}
// 获取页面信息
getPageInfo() {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
return {
route: currentPage ? currentPage.route : '',
options: currentPage ? currentPage.options : {},
pageStack: pages.map(page => page.route)
};
}
// 保存到本地存储
saveToLocalStorage() {
try {
uni.setStorageSync('errorLogs', this.errors);
} catch (e) {
console.error('Failed to save error logs to localStorage:', e);
}
}
// 上报错误
reportError(error) {
uni.request({
url: this.options.reportUrl,
method: 'POST',
data: {
error,
timestamp: Date.now(),
appVersion: this.getAppVersion()
},
success: (res) => {
console.log('Error reported successfully:', res);
},
fail: (err) => {
console.error('Failed to report error:', err);
}
});
}
// 获取应用版本
getAppVersion() {
// 从配置文件或其他地方获取应用版本
return '1.0.0';
}
// 手动捕获错误
captureException(error, extra = {}) {
this.captureError({
type: 'manual',
message: error.message || String(error),
stack: error.stack || '',
extra,
timestamp: Date.now()
});
}
// 获取错误列表
getErrors() {
return this.errors;
}
// 清除错误
clearErrors() {
this.errors = [];
if (this.options.enableLocalStorage) {
uni.removeStorageSync('errorLogs');
}
}
// 获取错误统计
getErrorStats() {
const stats = {
total: this.errors.length,
byType: {},
byPage: {},
recent: this.errors.slice(0, 10)
};
this.errors.forEach(error => {
// 按类型统计
stats.byType[error.type] = (stats.byType[error.type] || 0) + 1;
// 按页面统计
const route = error.pageInfo ? error.pageInfo.route : 'unknown';
stats.byPage[route] = (stats.byPage[route] || 0) + 1;
});
return stats;
}
}
export default new ErrorMonitor();
7.5 性能测试
7.5.1 自动化性能测试
// utils/performanceTest.js
class PerformanceTest {
constructor() {
this.tests = [];
this.results = [];
}
// 注册测试用例
registerTest(name, testFn, options = {}) {
this.tests.push({
name,
testFn,
options: {
iterations: 100,
warmup: 10,
timeout: 5000,
...options
}
});
}
// 运行单个测试
async runTest(test) {
const { name, testFn, options } = test;
const results = {
name,
iterations: options.iterations,
times: [],
errors: [],
startTime: Date.now()
};
console.log(`Running test: ${name}`);
try {
// 预热
for (let i = 0; i < options.warmup; i++) {
await testFn();
}
// 正式测试
for (let i = 0; i < options.iterations; i++) {
const startTime = performance.now();
try {
await Promise.race([
testFn(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), options.timeout)
)
]);
const endTime = performance.now();
results.times.push(endTime - startTime);
} catch (error) {
results.errors.push({
iteration: i,
error: error.message
});
}
}
// 计算统计信息
results.endTime = Date.now();
results.stats = this.calculateStats(results.times);
} catch (error) {
results.error = error.message;
}
return results;
}
// 运行所有测试
async runAllTests() {
console.log('Starting performance tests...');
const allResults = [];
for (const test of this.tests) {
const result = await this.runTest(test);
allResults.push(result);
// 测试间隔
await new Promise(resolve => setTimeout(resolve, 100));
}
this.results = allResults;
// 生成报告
const report = this.generateReport();
console.log('Performance test completed:', report);
return report;
}
// 计算统计信息
calculateStats(times) {
if (times.length === 0) {
return {
min: 0,
max: 0,
mean: 0,
median: 0,
p95: 0,
p99: 0,
stdDev: 0
};
}
const sorted = times.slice().sort((a, b) => a - b);
const sum = times.reduce((a, b) => a + b, 0);
const mean = sum / times.length;
// 计算标准差
const variance = times.reduce((acc, time) => {
return acc + Math.pow(time - mean, 2);
}, 0) / times.length;
const stdDev = Math.sqrt(variance);
return {
min: sorted[0],
max: sorted[sorted.length - 1],
mean,
median: sorted[Math.floor(sorted.length / 2)],
p95: sorted[Math.floor(sorted.length * 0.95)],
p99: sorted[Math.floor(sorted.length * 0.99)],
stdDev
};
}
// 生成测试报告
generateReport() {
const report = {
timestamp: new Date().toISOString(),
totalTests: this.results.length,
passedTests: this.results.filter(r => !r.error).length,
failedTests: this.results.filter(r => r.error).length,
results: this.results.map(result => ({
name: result.name,
status: result.error ? 'failed' : 'passed',
error: result.error,
iterations: result.iterations,
errorCount: result.errors ? result.errors.length : 0,
stats: result.stats,
duration: result.endTime - result.startTime
}))
};
return report;
}
// 导出报告
exportReport(format = 'json') {
const report = this.generateReport();
if (format === 'json') {
return JSON.stringify(report, null, 2);
} else if (format === 'csv') {
return this.generateCSVReport(report);
} else if (format === 'html') {
return this.generateHTMLReport(report);
}
return report;
}
// 生成 CSV 报告
generateCSVReport(report) {
const headers = [
'Test Name',
'Status',
'Iterations',
'Error Count',
'Min (ms)',
'Max (ms)',
'Mean (ms)',
'Median (ms)',
'P95 (ms)',
'P99 (ms)',
'Std Dev (ms)',
'Duration (ms)'
];
const rows = report.results.map(result => [
result.name,
result.status,
result.iterations,
result.errorCount,
result.stats ? result.stats.min.toFixed(2) : 'N/A',
result.stats ? result.stats.max.toFixed(2) : 'N/A',
result.stats ? result.stats.mean.toFixed(2) : 'N/A',
result.stats ? result.stats.median.toFixed(2) : 'N/A',
result.stats ? result.stats.p95.toFixed(2) : 'N/A',
result.stats ? result.stats.p99.toFixed(2) : 'N/A',
result.stats ? result.stats.stdDev.toFixed(2) : 'N/A',
result.duration
]);
return [headers, ...rows]
.map(row => row.join(','))
.join('\n');
}
// 生成 HTML 报告
generateHTMLReport(report) {
return `
<!DOCTYPE html>
<html>
<head>
<title>Performance Test Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
.passed { color: green; }
.failed { color: red; }
.summary { background-color: #f9f9f9; padding: 10px; margin-bottom: 20px; }
</style>
</head>
<body>
<h1>Performance Test Report</h1>
<div class="summary">
<h2>Summary</h2>
<p><strong>Generated:</strong> ${report.timestamp}</p>
<p><strong>Total Tests:</strong> ${report.totalTests}</p>
<p><strong>Passed:</strong> <span class="passed">${report.passedTests}</span></p>
<p><strong>Failed:</strong> <span class="failed">${report.failedTests}</span></p>
</div>
<h2>Test Results</h2>
<table>
<thead>
<tr>
<th>Test Name</th>
<th>Status</th>
<th>Iterations</th>
<th>Error Count</th>
<th>Min (ms)</th>
<th>Max (ms)</th>
<th>Mean (ms)</th>
<th>Median (ms)</th>
<th>P95 (ms)</th>
<th>P99 (ms)</th>
<th>Std Dev (ms)</th>
<th>Duration (ms)</th>
</tr>
</thead>
<tbody>
${report.results.map(result => `
<tr>
<td>${result.name}</td>
<td class="${result.status}">${result.status}</td>
<td>${result.iterations}</td>
<td>${result.errorCount}</td>
<td>${result.stats ? result.stats.min.toFixed(2) : 'N/A'}</td>
<td>${result.stats ? result.stats.max.toFixed(2) : 'N/A'}</td>
<td>${result.stats ? result.stats.mean.toFixed(2) : 'N/A'}</td>
<td>${result.stats ? result.stats.median.toFixed(2) : 'N/A'}</td>
<td>${result.stats ? result.stats.p95.toFixed(2) : 'N/A'}</td>
<td>${result.stats ? result.stats.p99.toFixed(2) : 'N/A'}</td>
<td>${result.stats ? result.stats.stdDev.toFixed(2) : 'N/A'}</td>
<td>${result.duration}</td>
</tr>
`).join('')}
</tbody>
</table>
</body>
</html>
`;
}
}
// 使用示例
const performanceTest = new PerformanceTest();
// 注册测试用例
performanceTest.registerTest('Array Push', () => {
const arr = [];
for (let i = 0; i < 1000; i++) {
arr.push(i);
}
}, { iterations: 1000 });
performanceTest.registerTest('Object Creation', () => {
const objects = [];
for (let i = 0; i < 100; i++) {
objects.push({ id: i, name: `Item ${i}` });
}
}, { iterations: 1000 });
performanceTest.registerTest('DOM Query', () => {
// #ifdef H5
document.querySelectorAll('div');
// #endif
}, { iterations: 500 });
performanceTest.registerTest('Local Storage', () => {
uni.setStorageSync('test', { data: 'test data' });
uni.getStorageSync('test');
uni.removeStorageSync('test');
}, { iterations: 100 });
export default performanceTest;
7.5.2 基准测试工具
// utils/benchmark.js
class Benchmark {
constructor() {
this.suites = new Map();
}
// 创建测试套件
suite(name) {
const suite = new BenchmarkSuite(name);
this.suites.set(name, suite);
return suite;
}
// 运行所有套件
async runAll() {
const results = [];
for (const [name, suite] of this.suites) {
console.log(`Running benchmark suite: ${name}`);
const result = await suite.run();
results.push(result);
}
return results;
}
}
class BenchmarkSuite {
constructor(name) {
this.name = name;
this.tests = [];
}
// 添加测试
add(name, fn, options = {}) {
this.tests.push({
name,
fn,
options: {
minSamples: 50,
maxTime: 5000,
...options
}
});
return this;
}
// 运行套件
async run() {
const results = {
suiteName: this.name,
tests: [],
fastest: null,
slowest: null
};
for (const test of this.tests) {
const result = await this.runTest(test);
results.tests.push(result);
}
// 找出最快和最慢的测试
if (results.tests.length > 0) {
results.fastest = results.tests.reduce((fastest, current) =>
current.hz > fastest.hz ? current : fastest
);
results.slowest = results.tests.reduce((slowest, current) =>
current.hz < slowest.hz ? current : slowest
);
}
this.printResults(results);
return results;
}
// 运行单个测试
async runTest(test) {
const { name, fn, options } = test;
const samples = [];
const startTime = Date.now();
let iterations = 0;
// 运行测试直到达到最小样本数或最大时间
while (samples.length < options.minSamples &&
(Date.now() - startTime) < options.maxTime) {
const sampleStart = performance.now();
// 运行多次以获得更准确的测量
let runs = 1;
let elapsed = 0;
do {
const runStart = performance.now();
for (let i = 0; i < runs; i++) {
fn();
iterations++;
}
elapsed = performance.now() - runStart;
// 如果运行时间太短,增加运行次数
if (elapsed < 1) {
runs *= 2;
}
} while (elapsed < 1 && runs < 1000000);
const sampleTime = elapsed / runs;
samples.push(sampleTime);
}
// 计算统计信息
const stats = this.calculateStats(samples);
return {
name,
samples: samples.length,
iterations,
hz: 1000 / stats.mean, // 每秒操作数
stats,
margin: this.calculateMarginOfError(samples)
};
}
// 计算统计信息
calculateStats(samples) {
const sorted = samples.slice().sort((a, b) => a - b);
const sum = samples.reduce((a, b) => a + b, 0);
const mean = sum / samples.length;
const variance = samples.reduce((acc, sample) => {
return acc + Math.pow(sample - mean, 2);
}, 0) / samples.length;
return {
min: sorted[0],
max: sorted[sorted.length - 1],
mean,
median: sorted[Math.floor(sorted.length / 2)],
stdDev: Math.sqrt(variance),
variance
};
}
// 计算误差范围
calculateMarginOfError(samples) {
const stats = this.calculateStats(samples);
const standardError = stats.stdDev / Math.sqrt(samples.length);
const tValue = 1.96; // 95% 置信区间
return {
relative: (tValue * standardError / stats.mean) * 100,
absolute: tValue * standardError
};
}
// 打印结果
printResults(results) {
console.log(`\n=== ${results.suiteName} ===`);
results.tests.forEach(test => {
console.log(`${test.name}: ${test.hz.toFixed(0)} ops/sec ±${test.margin.relative.toFixed(2)}% (${test.samples} samples)`);
});
if (results.fastest && results.slowest && results.tests.length > 1) {
const ratio = results.fastest.hz / results.slowest.hz;
console.log(`\nFastest: ${results.fastest.name}`);
console.log(`Slowest: ${results.slowest.name}`);
console.log(`${results.fastest.name} is ${ratio.toFixed(2)}x faster than ${results.slowest.name}`);
}
}
}
// 使用示例
const benchmark = new Benchmark();
// 数组操作基准测试
benchmark.suite('Array Operations')
.add('Array.push', () => {
const arr = [];
for (let i = 0; i < 100; i++) {
arr.push(i);
}
})
.add('Array spread', () => {
let arr = [];
for (let i = 0; i < 100; i++) {
arr = [...arr, i];
}
})
.add('Array.concat', () => {
let arr = [];
for (let i = 0; i < 100; i++) {
arr = arr.concat([i]);
}
});
// 对象操作基准测试
benchmark.suite('Object Operations')
.add('Object.assign', () => {
const obj = { a: 1, b: 2 };
Object.assign({}, obj, { c: 3 });
})
.add('Spread operator', () => {
const obj = { a: 1, b: 2 };
({ ...obj, c: 3 });
})
.add('Manual copy', () => {
const obj = { a: 1, b: 2 };
const newObj = { a: obj.a, b: obj.b, c: 3 };
});
export default benchmark;
7.6 本章总结
学习要点回顾
性能分析
- 关键性能指标的监控
- 性能数据的收集和分析
- 自定义性能监控面板
渲染优化
- 虚拟滚动技术
- 图片懒加载和压缩
- 内存泄漏检测和预防
内存优化
- 对象池模式的应用
- 弱引用管理
- 内存使用监控
调试工具
- 自定义调试面板开发
- 错误监控和上报系统
- 网络请求和存储调试
性能测试
- 自动化性能测试框架
- 基准测试工具
- 测试报告生成
实践练习
性能监控系统
- 实现完整的性能监控系统
- 集成错误上报功能
- 添加性能预警机制
内存优化方案
- 分析应用内存使用情况
- 实现对象池优化
- 解决内存泄漏问题
调试工具开发
- 开发自定义调试面板
- 实现日志管理系统
- 添加性能分析功能
常见问题解答
Q: 如何选择合适的性能优化策略? A: 首先进行性能分析,识别瓶颈所在,然后针对性地选择优化策略。常见的优化点包括渲染性能、内存使用、网络请求等。
Q: 虚拟滚动适用于哪些场景? A: 虚拟滚动适用于需要展示大量数据的列表场景,如商品列表、聊天记录、数据表格等。当列表项数量超过几百个时,建议使用虚拟滚动。
Q: 如何有效地进行内存泄漏检测? A: 可以使用内存监控工具定期检查内存使用情况,结合弱引用管理和对象池模式来预防内存泄漏。同时要注意及时清理事件监听器、定时器等。
Q: 调试面板会影响应用性能吗? A: 调试面板确实会对性能产生一定影响,建议只在开发和测试环境中启用,生产环境应该禁用或使用轻量级的错误监控。
Q: 如何进行跨平台的性能测试? A: 可以使用统一的性能测试框架,在不同平台上运行相同的测试用例,然后比较测试结果。注意不同平台的性能特点和限制。
最佳实践建议
性能监控
- 建立完善的性能监控体系
- 设置合理的性能指标阈值
- 定期分析性能数据
代码优化
- 遵循性能优化最佳实践
- 避免不必要的重复计算
- 合理使用缓存机制
内存管理
- 及时释放不需要的资源
- 使用对象池减少垃圾回收
- 监控内存使用情况
调试效率
- 使用专业的调试工具
- 建立错误监控和上报机制
- 保持良好的日志记录习惯
测试策略
- 制定全面的性能测试计划
- 自动化性能测试流程
- 持续监控性能变化
下一章预告
在下一章中,我们将学习 UniApp 的打包发布,包括: - 多平台打包配置 - 应用签名和证书 - 发布流程和注意事项 - 版本管理和更新机制 - 应用商店上架指南
通过本章的学习,你已经掌握了 UniApp 应用的性能优化和调试技巧。这些技能将帮助你开发出高性能、稳定可靠的跨平台应用。
7.2 渲染优化
7.2.1 列表渲染优化
虚拟滚动实现
<!-- components/VirtualList.vue -->
<template>
<scroll-view
class="virtual-list"
:scroll-y="true"
:scroll-top="scrollTop"
@scroll="onScroll"
:style="{ height: containerHeight + 'px' }"
>
<!-- 占位元素,用于撑开滚动区域 -->
<view :style="{ height: totalHeight + 'px', position: 'relative' }">
<!-- 可见区域的列表项 -->
<view
v-for="item in visibleItems"
:key="getItemKey(item)"
class="virtual-item"
:style="getItemStyle(item)"
>
<slot :item="item.data" :index="item.index"></slot>
</view>
</view>
</scroll-view>
</template>
<script>
export default {
name: 'VirtualList',
props: {
// 数据列表
items: {
type: Array,
required: true
},
// 每项的高度
itemHeight: {
type: Number,
default: 50
},
// 容器高度
containerHeight: {
type: Number,
default: 400
},
// 缓冲区大小(额外渲染的项目数)
bufferSize: {
type: Number,
default: 5
},
// 获取项目唯一标识的函数
keyField: {
type: String,
default: 'id'
}
},
data() {
return {
scrollTop: 0,
startIndex: 0,
endIndex: 0
};
},
computed: {
// 总高度
totalHeight() {
return this.items.length * this.itemHeight;
},
// 可见区域可容纳的项目数
visibleCount() {
return Math.ceil(this.containerHeight / this.itemHeight);
},
// 可见的项目列表
visibleItems() {
const items = [];
for (let i = this.startIndex; i <= this.endIndex; i++) {
if (i >= 0 && i < this.items.length) {
items.push({
index: i,
data: this.items[i]
});
}
}
return items;
}
},
watch: {
items: {
handler() {
this.updateVisibleRange();
},
immediate: true
}
},
methods: {
// 滚动事件处理
onScroll(e) {
this.scrollTop = e.detail.scrollTop;
this.updateVisibleRange();
},
// 更新可见范围
updateVisibleRange() {
const scrollTop = this.scrollTop;
const startIndex = Math.floor(scrollTop / this.itemHeight);
// 添加缓冲区
this.startIndex = Math.max(0, startIndex - this.bufferSize);
this.endIndex = Math.min(
this.items.length - 1,
startIndex + this.visibleCount + this.bufferSize
);
},
// 获取项目的样式
getItemStyle(item) {
return {
position: 'absolute',
top: item.index * this.itemHeight + 'px',
left: '0',
right: '0',
height: this.itemHeight + 'px'
};
},
// 获取项目的唯一标识
getItemKey(item) {
return item.data[this.keyField] || item.index;
},
// 滚动到指定索引
scrollToIndex(index) {
const scrollTop = index * this.itemHeight;
this.scrollTop = scrollTop;
this.updateVisibleRange();
},
// 滚动到顶部
scrollToTop() {
this.scrollToIndex(0);
},
// 滚动到底部
scrollToBottom() {
this.scrollToIndex(this.items.length - 1);
}
}
};
</script>
<style scoped>
.virtual-list {
position: relative;
overflow: hidden;
}
.virtual-item {
box-sizing: border-box;
}
</style>
使用虚拟滚动
<!-- pages/list/virtual-demo.vue -->
<template>
<view class="page">
<view class="header">
<text class="title">虚拟滚动演示</text>
<text class="subtitle">{{ items.length }} 条数据</text>
</view>
<VirtualList
:items="items"
:item-height="80"
:container-height="600"
:buffer-size="3"
key-field="id"
>
<template #default="{ item, index }">
<view class="list-item">
<image class="avatar" :src="item.avatar" mode="aspectFill"></image>
<view class="content">
<text class="name">{{ item.name }}</text>
<text class="description">{{ item.description }}</text>
<text class="index">索引: {{ index }}</text>
</view>
<view class="actions">
<button class="btn" @click="onItemClick(item)">操作</button>
</view>
</view>
</template>
</VirtualList>
<view class="controls">
<button @click="addItems">添加数据</button>
<button @click="scrollToTop">回到顶部</button>
<button @click="scrollToBottom">滚动到底部</button>
</view>
</view>
</template>
<script>
import VirtualList from '@/components/VirtualList.vue';
export default {
components: {
VirtualList
},
data() {
return {
items: []
};
},
onLoad() {
this.generateItems(1000); // 生成 1000 条数据
},
methods: {
// 生成测试数据
generateItems(count) {
const items = [];
for (let i = 0; i < count; i++) {
items.push({
id: i,
name: `用户 ${i + 1}`,
description: `这是第 ${i + 1} 个用户的描述信息,包含一些详细内容。`,
avatar: `https://picsum.photos/60/60?random=${i}`
});
}
this.items = items;
},
// 添加更多数据
addItems() {
const startIndex = this.items.length;
const newItems = [];
for (let i = 0; i < 100; i++) {
const index = startIndex + i;
newItems.push({
id: index,
name: `用户 ${index + 1}`,
description: `这是第 ${index + 1} 个用户的描述信息。`,
avatar: `https://picsum.photos/60/60?random=${index}`
});
}
this.items.push(...newItems);
},
// 项目点击事件
onItemClick(item) {
uni.showToast({
title: `点击了 ${item.name}`,
icon: 'none'
});
},
// 滚动到顶部
scrollToTop() {
this.$refs.virtualList?.scrollToTop();
},
// 滚动到底部
scrollToBottom() {
this.$refs.virtualList?.scrollToBottom();
}
}
};
</script>
<style scoped>
.page {
padding: 16px;
}
.header {
margin-bottom: 16px;
text-align: center;
}
.title {
display: block;
font-size: 20px;
font-weight: bold;
margin-bottom: 8px;
}
.subtitle {
display: block;
font-size: 14px;
color: #666;
}
.list-item {
display: flex;
align-items: center;
padding: 12px;
border-bottom: 1px solid #eee;
background-color: #fff;
}
.avatar {
width: 60px;
height: 60px;
border-radius: 30px;
margin-right: 12px;
}
.content {
flex: 1;
display: flex;
flex-direction: column;
}
.name {
font-size: 16px;
font-weight: bold;
margin-bottom: 4px;
}
.description {
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
.index {
font-size: 12px;
color: #999;
}
.actions {
margin-left: 12px;
}
.btn {
padding: 8px 16px;
background-color: #007aff;
color: #fff;
border: none;
border-radius: 4px;
font-size: 14px;
}
.controls {
display: flex;
justify-content: space-around;
margin-top: 16px;
padding: 16px;
background-color: #f5f5f5;
border-radius: 8px;
}
.controls button {
padding: 8px 16px;
background-color: #007aff;
color: #fff;
border: none;
border-radius: 4px;
font-size: 14px;
}
</style>
7.2.2 图片优化
图片懒加载组件
<!-- components/LazyImage.vue -->
<template>
<view class="lazy-image" :style="containerStyle">
<image
v-if="shouldLoad"
:src="currentSrc"
:mode="mode"
:lazy-load="true"
:fade-show="fadeShow"
:webp="webp"
:show-menu-by-longpress="showMenuByLongpress"
:class="imageClass"
:style="imageStyle"
@load="onLoad"
@error="onError"
@click="onClick"
/>
<!-- 占位符 -->
<view v-else-if="placeholder" class="placeholder" :style="placeholderStyle">
<image v-if="placeholderSrc" :src="placeholderSrc" :mode="mode" class="placeholder-image" />
<view v-else class="placeholder-content">
<text class="placeholder-text">{{ placeholderText }}</text>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-overlay">
<view class="loading-spinner"></view>
</view>
<!-- 错误状态 -->
<view v-if="error" class="error-overlay" @click="retry">
<text class="error-text">{{ errorText }}</text>
<text class="retry-text">点击重试</text>
</view>
</view>
</template>
<script>
export default {
name: 'LazyImage',
props: {
// 图片地址
src: {
type: String,
required: true
},
// 占位图片
placeholderSrc: {
type: String,
default: ''
},
// 占位文本
placeholderText: {
type: String,
default: '加载中...'
},
// 错误文本
errorText: {
type: String,
default: '加载失败'
},
// 图片模式
mode: {
type: String,
default: 'aspectFill'
},
// 宽度
width: {
type: [String, Number],
default: '100%'
},
// 高度
height: {
type: [String, Number],
default: 'auto'
},
// 是否显示占位符
placeholder: {
type: Boolean,
default: true
},
// 是否启用淡入效果
fadeShow: {
type: Boolean,
default: true
},
// 是否支持 WebP
webp: {
type: Boolean,
default: true
},
// 是否支持长按菜单
showMenuByLongpress: {
type: Boolean,
default: false
},
// 懒加载阈值(距离可视区域的距离)
threshold: {
type: Number,
default: 100
},
// 是否立即加载
immediate: {
type: Boolean,
default: false
},
// 重试次数
retryCount: {
type: Number,
default: 3
}
},
data() {
return {
shouldLoad: false,
loading: false,
loaded: false,
error: false,
currentRetryCount: 0,
observer: null
};
},
computed: {
containerStyle() {
return {
width: this.formatSize(this.width),
height: this.formatSize(this.height),
position: 'relative',
overflow: 'hidden'
};
},
imageStyle() {
return {
width: '100%',
height: '100%',
transition: this.fadeShow ? 'opacity 0.3s' : 'none',
opacity: this.loaded ? 1 : 0
};
},
imageClass() {
return {
'lazy-image__img': true,
'lazy-image__img--loaded': this.loaded,
'lazy-image__img--loading': this.loading
};
},
placeholderStyle() {
return {
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f5f5f5'
};
},
currentSrc() {
// 根据设备像素比选择合适的图片
const dpr = uni.getSystemInfoSync().pixelRatio || 1;
if (dpr >= 3) {
return this.src.replace(/\.(jpg|jpeg|png)$/i, '@3x.$1');
} else if (dpr >= 2) {
return this.src.replace(/\.(jpg|jpeg|png)$/i, '@2x.$1');
}
return this.src;
}
},
mounted() {
if (this.immediate) {
this.loadImage();
} else {
this.observeVisibility();
}
},
beforeDestroy() {
if (this.observer) {
this.observer.disconnect();
}
},
methods: {
// 观察元素可见性
observeVisibility() {
// #ifdef H5
if (typeof IntersectionObserver !== 'undefined') {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.loadImage();
this.observer.disconnect();
}
});
},
{
rootMargin: `${this.threshold}px`
}
);
this.observer.observe(this.$el);
} else {
// 降级方案:立即加载
this.loadImage();
}
// #endif
// #ifndef H5
// 在非 H5 平台使用滚动监听
this.loadImage();
// #endif
},
// 加载图片
loadImage() {
if (this.shouldLoad || this.loading) {
return;
}
this.shouldLoad = true;
this.loading = true;
this.error = false;
},
// 图片加载成功
onLoad(e) {
this.loading = false;
this.loaded = true;
this.error = false;
this.currentRetryCount = 0;
this.$emit('load', e);
},
// 图片加载失败
onError(e) {
this.loading = false;
this.loaded = false;
this.error = true;
this.$emit('error', e);
// 自动重试
if (this.currentRetryCount < this.retryCount) {
setTimeout(() => {
this.retry();
}, 1000 * Math.pow(2, this.currentRetryCount)); // 指数退避
}
},
// 重试加载
retry() {
if (this.currentRetryCount >= this.retryCount) {
return;
}
this.currentRetryCount++;
this.error = false;
this.loading = true;
// 强制重新加载
this.$nextTick(() => {
this.shouldLoad = false;
this.$nextTick(() => {
this.shouldLoad = true;
});
});
},
// 图片点击事件
onClick(e) {
this.$emit('click', e);
},
// 格式化尺寸
formatSize(size) {
if (typeof size === 'number') {
return size + 'px';
}
return size;
}
}
};
</script>
<style scoped>
.lazy-image {
display: inline-block;
background-color: #f5f5f5;
}
.lazy-image__img {
display: block;
}
.placeholder {
background-color: #f5f5f5;
color: #999;
}
.placeholder-image {
width: 100%;
height: 100%;
}
.placeholder-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.placeholder-text {
font-size: 14px;
color: #999;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.1);
}
.loading-spinner {
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #007aff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.1);
cursor: pointer;
}
.error-text {
font-size: 14px;
color: #ff4444;
margin-bottom: 4px;
}
.retry-text {
font-size: 12px;
color: #007aff;
}
</style>
图片压缩工具
// utils/imageCompress.js
class ImageCompressor {
constructor() {
this.defaultOptions = {
quality: 0.8,
maxWidth: 1920,
maxHeight: 1080,
format: 'jpeg',
enableResize: true
};
}
// 压缩图片
async compress(filePath, options = {}) {
const opts = { ...this.defaultOptions, ...options };
try {
// #ifdef H5
return await this.compressH5(filePath, opts);
// #endif
// #ifdef APP-PLUS
return await this.compressApp(filePath, opts);
// #endif
// #ifdef MP
return await this.compressMp(filePath, opts);
// #endif
} catch (error) {
console.error('Image compression failed:', error);
throw error;
}
}
// H5 平台图片压缩
compressH5(filePath, options) {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
// 计算压缩后的尺寸
const { width, height } = this.calculateSize(
img.width,
img.height,
options.maxWidth,
options.maxHeight
);
canvas.width = width;
canvas.height = height;
// 绘制图片
ctx.drawImage(img, 0, 0, width, height);
// 转换为 Blob
canvas.toBlob(
(blob) => {
if (blob) {
const compressedFile = new File([blob], 'compressed.jpg', {
type: `image/${options.format}`
});
resolve({
file: compressedFile,
size: blob.size,
width,
height
});
} else {
reject(new Error('Canvas to blob conversion failed'));
}
},
`image/${options.format}`,
options.quality
);
};
img.onerror = () => {
reject(new Error('Image load failed'));
};
img.src = filePath;
});
}
// App 平台图片压缩
compressApp(filePath, options) {
return new Promise((resolve, reject) => {
plus.zip.compressImage(
{
src: filePath,
dst: `_doc/compressed_${Date.now()}.jpg`,
quality: Math.round(options.quality * 100),
width: options.maxWidth,
height: options.maxHeight,
format: options.format
},
(event) => {
resolve({
filePath: event.target,
size: event.size,
width: event.width,
height: event.height
});
},
(error) => {
reject(error);
}
);
});
}
// 小程序平台图片压缩
compressMp(filePath, options) {
return new Promise((resolve, reject) => {
uni.compressImage({
src: filePath,
quality: Math.round(options.quality * 100),
success: (res) => {
resolve({
filePath: res.tempFilePath,
size: res.size || 0
});
},
fail: reject
});
});
}
// 计算压缩后的尺寸
calculateSize(originalWidth, originalHeight, maxWidth, maxHeight) {
let { width, height } = { width: originalWidth, height: originalHeight };
// 按比例缩放
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
if (height > maxHeight) {
width = (width * maxHeight) / height;
height = maxHeight;
}
return {
width: Math.round(width),
height: Math.round(height)
};
}
// 批量压缩
async compressBatch(filePaths, options = {}) {
const results = [];
for (const filePath of filePaths) {
try {
const result = await this.compress(filePath, options);
results.push({ success: true, data: result });
} catch (error) {
results.push({ success: false, error });
}
}
return results;
}
// 获取图片信息
getImageInfo(filePath) {
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: filePath,
success: resolve,
fail: reject
});
});
}
// 计算压缩率
calculateCompressionRatio(originalSize, compressedSize) {
return ((originalSize - compressedSize) / originalSize * 100).toFixed(2);
}
}
export default new ImageCompressor();
7.3 内存优化
7.3.1 内存泄漏检测
内存监控工具
// utils/memoryMonitor.js
class MemoryMonitor {
constructor() {
this.isMonitoring = false;
this.memoryData = [];
this.leakDetectors = [];
this.thresholds = {
warning: 50 * 1024 * 1024, // 50MB
critical: 100 * 1024 * 1024 // 100MB
};
this.init();
}
init() {
// 注册内存泄漏检测器
this.registerLeakDetectors();
// 监听页面生命周期
this.observePageLifecycle();
}
// 开始监控
startMonitoring(interval = 5000) {
if (this.isMonitoring) {
return;
}
this.isMonitoring = true;
this.monitorTimer = setInterval(() => {
this.collectMemoryData();
}, interval);
console.log('Memory monitoring started');
}
// 停止监控
stopMonitoring() {
if (!this.isMonitoring) {
return;
}
this.isMonitoring = false;
if (this.monitorTimer) {
clearInterval(this.monitorTimer);
this.monitorTimer = null;
}
console.log('Memory monitoring stopped');
}
// 收集内存数据
async collectMemoryData() {
try {
const memoryInfo = await this.getMemoryInfo();
const data = {
timestamp: Date.now(),
...memoryInfo
};
this.memoryData.push(data);
// 只保留最近 100 个数据点
if (this.memoryData.length > 100) {
this.memoryData = this.memoryData.slice(-100);
}
// 检查内存使用情况
this.checkMemoryUsage(memoryInfo);
// 运行泄漏检测
this.runLeakDetection();
} catch (error) {
console.error('Failed to collect memory data:', error);
}
}
// 获取内存信息
getMemoryInfo() {
return new Promise((resolve) => {
// #ifdef H5
if (performance.memory) {
resolve({
used: performance.memory.usedJSHeapSize,
total: performance.memory.totalJSHeapSize,
limit: performance.memory.jsHeapSizeLimit
});
} else {
resolve({ used: 0, total: 0, limit: 0 });
}
// #endif
// #ifdef APP-PLUS
plus.runtime.getProperty(plus.runtime.appid, (info) => {
resolve({
used: info.memory * 1024 * 1024,
total: info.memory * 1024 * 1024,
limit: 0
});
});
// #endif
// #ifndef H5 || APP-PLUS
resolve({ used: 0, total: 0, limit: 0 });
// #endif
});
}
// 检查内存使用情况
checkMemoryUsage(memoryInfo) {
const { used } = memoryInfo;
if (used > this.thresholds.critical) {
this.onMemoryAlert('critical', used);
} else if (used > this.thresholds.warning) {
this.onMemoryAlert('warning', used);
}
}
// 内存警告处理
onMemoryAlert(level, usage) {
const message = `Memory usage ${level}: ${this.formatBytes(usage)}`;
console.warn(message);
// 触发垃圾回收(如果支持)
if (level === 'critical' && typeof gc === 'function') {
gc();
}
// 发送警告事件
uni.$emit('memory-alert', { level, usage });
}
// 注册内存泄漏检测器
registerLeakDetectors() {
// DOM 节点泄漏检测
this.leakDetectors.push(new DOMLeakDetector());
// 事件监听器泄漏检测
this.leakDetectors.push(new EventLeakDetector());
// 定时器泄漏检测
this.leakDetectors.push(new TimerLeakDetector());
// 闭包泄漏检测
this.leakDetectors.push(new ClosureLeakDetector());
}
// 运行泄漏检测
runLeakDetection() {
this.leakDetectors.forEach(detector => {
try {
const leaks = detector.detect();
if (leaks.length > 0) {
console.warn(`${detector.name} detected leaks:`, leaks);
}
} catch (error) {
console.error(`Leak detector ${detector.name} failed:`, error);
}
});
}
// 监听页面生命周期
observePageLifecycle() {
// 页面显示时开始监控
uni.onPageShow(() => {
this.startMonitoring();
});
// 页面隐藏时停止监控
uni.onPageHide(() => {
this.stopMonitoring();
});
}
// 格式化字节数
formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 获取内存报告
getMemoryReport() {
return {
current: this.memoryData[this.memoryData.length - 1],
history: this.memoryData,
leaks: this.leakDetectors.map(detector => ({
name: detector.name,
leaks: detector.detect()
}))
};
}
}
// DOM 节点泄漏检测器
class DOMLeakDetector {
constructor() {
this.name = 'DOM Leak Detector';
this.nodeCount = 0;
this.threshold = 1000;
}
detect() {
// #ifdef H5
const currentNodeCount = document.querySelectorAll('*').length;
const growth = currentNodeCount - this.nodeCount;
this.nodeCount = currentNodeCount;
if (growth > this.threshold) {
return [{
type: 'dom-growth',
growth,
total: currentNodeCount
}];
}
// #endif
return [];
}
}
// 事件监听器泄漏检测器
class EventLeakDetector {
constructor() {
this.name = 'Event Leak Detector';
this.listeners = new Map();
}
detect() {
const leaks = [];
// 检查未清理的事件监听器
this.listeners.forEach((count, event) => {
if (count > 10) { // 阈值
leaks.push({
type: 'event-listener',
event,
count
});
}
});
return leaks;
}
// 记录事件监听器
recordListener(event) {
const count = this.listeners.get(event) || 0;
this.listeners.set(event, count + 1);
}
// 移除事件监听器记录
removeListener(event) {
const count = this.listeners.get(event) || 0;
if (count > 0) {
this.listeners.set(event, count - 1);
}
}
}
// 定时器泄漏检测器
class TimerLeakDetector {
constructor() {
this.name = 'Timer Leak Detector';
this.timers = new Set();
this.threshold = 20;
}
detect() {
if (this.timers.size > this.threshold) {
return [{
type: 'timer-leak',
count: this.timers.size
}];
}
return [];
}
// 记录定时器
recordTimer(id) {
this.timers.add(id);
}
// 移除定时器记录
removeTimer(id) {
this.timers.delete(id);
}
}
// 闭包泄漏检测器
class ClosureLeakDetector {
constructor() {
this.name = 'Closure Leak Detector';
this.closures = new WeakSet();
}
detect() {
// 闭包泄漏较难检测,这里提供基础框架
return [];
}
}
export default new MemoryMonitor();
7.3.2 内存优化策略
对象池模式
// utils/objectPool.js
class ObjectPool {
constructor(createFn, resetFn, initialSize = 10) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
this.used = new Set();
// 预创建对象
for (let i = 0; i < initialSize; i++) {
this.pool.push(this.createFn());
}
}
// 获取对象
acquire() {
let obj;
if (this.pool.length > 0) {
obj = this.pool.pop();
} else {
obj = this.createFn();
}
this.used.add(obj);
return obj;
}
// 释放对象
release(obj) {
if (!this.used.has(obj)) {
return false;
}
this.used.delete(obj);
// 重置对象状态
if (this.resetFn) {
this.resetFn(obj);
}
this.pool.push(obj);
return true;
}
// 清空池
clear() {
this.pool = [];
this.used.clear();
}
// 获取统计信息
getStats() {
return {
poolSize: this.pool.length,
usedCount: this.used.size,
totalCreated: this.pool.length + this.used.size
};
}
}
// 示例:DOM 元素池
class DOMElementPool extends ObjectPool {
constructor(tagName, initialSize = 10) {
super(
() => document.createElement(tagName),
(element) => {
element.innerHTML = '';
element.className = '';
element.removeAttribute('style');
},
initialSize
);
}
}
// 示例:数据对象池
class DataObjectPool extends ObjectPool {
constructor(initialSize = 10) {
super(
() => ({}),
(obj) => {
// 清空对象属性
Object.keys(obj).forEach(key => {
delete obj[key];
});
},
initialSize
);
}
}
export { ObjectPool, DOMElementPool, DataObjectPool };
弱引用管理
// utils/weakRefManager.js
class WeakRefManager {
constructor() {
this.refs = new Set();
this.cleanupTimer = null;
this.cleanupInterval = 30000; // 30秒清理一次
this.startCleanup();
}
// 创建弱引用
createWeakRef(target, callback) {
const weakRef = new WeakRef(target);
const refInfo = {
ref: weakRef,
callback,
created: Date.now()
};
this.refs.add(refInfo);
return weakRef;
}
// 开始清理
startCleanup() {
this.cleanupTimer = setInterval(() => {
this.cleanup();
}, this.cleanupInterval);
}
// 停止清理
stopCleanup() {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
}
}
// 清理已失效的弱引用
cleanup() {
const toRemove = [];
this.refs.forEach(refInfo => {
const target = refInfo.ref.deref();
if (target === undefined) {
// 对象已被垃圾回收
toRemove.push(refInfo);
if (refInfo.callback) {
refInfo.callback();
}
}
});
toRemove.forEach(refInfo => {
this.refs.delete(refInfo);
});
if (toRemove.length > 0) {
console.log(`Cleaned up ${toRemove.length} weak references`);
}
}
// 获取统计信息
getStats() {
let activeRefs = 0;
let deadRefs = 0;
this.refs.forEach(refInfo => {
if (refInfo.ref.deref() !== undefined) {
activeRefs++;
} else {
deadRefs++;
}
});
return {
total: this.refs.size,
active: activeRefs,
dead: deadRefs
};
}
// 销毁管理器
destroy() {
this.stopCleanup();
this.refs.clear();
}
}
export default new WeakRefManager();
7.4 调试工具
7.4.1 自定义调试面板
<!-- components/DebugPanel.vue -->
<template>
<view v-if="visible" class="debug-panel">
<!-- 调试面板头部 -->
<view class="debug-header">
<text class="debug-title">调试面板</text>
<view class="debug-controls">
<button @click="toggleConsole" :class="consoleButtonClass">
控制台
</button>
<button @click="toggleNetwork" :class="networkButtonClass">
网络
</button>
<button @click="toggleStorage" :class="storageButtonClass">
存储
</button>
<button @click="close" class="close-btn">关闭</button>
</view>
</view>
<!-- 调试内容 -->
<scroll-view class="debug-content" scroll-y>
<!-- 控制台面板 -->
<view v-if="activeTab === 'console'" class="console-panel">
<view class="console-controls">
<button @click="clearConsole">清除</button>
<button @click="exportLogs">导出</button>
<select @change="onLogLevelChange" :value="logLevel">
<option value="all">全部</option>
<option value="error">错误</option>
<option value="warn">警告</option>
<option value="info">信息</option>
<option value="debug">调试</option>
</select>
</view>
<view class="console-logs">
<view
v-for="log in filteredLogs"
:key="log.id"
:class="getLogClass(log)"
class="console-log"
>
<text class="log-time">{{ formatTime(log.timestamp) }}</text>
<text class="log-level">{{ log.level.toUpperCase() }}</text>
<text class="log-message">{{ log.message }}</text>
<view v-if="log.data" class="log-data">
<pre>{{ JSON.stringify(log.data, null, 2) }}</pre>
</view>
</view>
</view>
</view>
<!-- 网络面板 -->
<view v-if="activeTab === 'network'" class="network-panel">
<view class="network-controls">
<button @click="clearNetwork">清除</button>
<button @click="exportNetwork">导出</button>
</view>
<view class="network-requests">
<view
v-for="request in networkRequests"
:key="request.id"
class="network-request"
:class="getRequestClass(request)"
@click="showRequestDetails(request)"
>
<view class="request-summary">
<text class="request-method">{{ request.method }}</text>
<text class="request-url">{{ request.url }}</text>
<text class="request-status">{{ request.status }}</text>
<text class="request-time">{{ request.duration }}ms</text>
</view>
</view>
</view>
</view>
<!-- 存储面板 -->
<view v-if="activeTab === 'storage'" class="storage-panel">
<view class="storage-controls">
<button @click="refreshStorage">刷新</button>
<button @click="clearStorage">清除全部</button>
</view>
<view class="storage-sections">
<!-- localStorage -->
<view class="storage-section">
<text class="section-title">本地存储</text>
<view class="storage-items">
<view
v-for="item in localStorageItems"
:key="item.key"
class="storage-item"
>
<text class="item-key">{{ item.key }}</text>
<text class="item-value">{{ item.value }}</text>
<button @click="deleteStorageItem('local', item.key)" class="delete-btn">
删除
</button>
</view>
</view>
</view>
<!-- sessionStorage -->
<view class="storage-section">
<text class="section-title">会话存储</text>
<view class="storage-items">
<view
v-for="item in sessionStorageItems"
:key="item.key"
class="storage-item"
>
<text class="item-key">{{ item.key }}</text>
<text class="item-value">{{ item.value }}</text>
<button @click="deleteStorageItem('session', item.key)" class="delete-btn">
删除
</button>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 请求详情弹窗 -->
<view v-if="selectedRequest" class="request-modal" @click="closeRequestDetails">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">请求详情</text>
<button @click="closeRequestDetails" class="modal-close">×</button>
</view>
<scroll-view class="modal-body" scroll-y>
<view class="detail-section">
<text class="detail-title">基本信息</text>
<view class="detail-item">
<text class="detail-label">URL:</text>
<text class="detail-value">{{ selectedRequest.url }}</text>
</view>
<view class="detail-item">
<text class="detail-label">方法:</text>
<text class="detail-value">{{ selectedRequest.method }}</text>
</view>
<view class="detail-item">
<text class="detail-label">状态:</text>
<text class="detail-value">{{ selectedRequest.status }}</text>
</view>
<view class="detail-item">
<text class="detail-label">耗时:</text>
<text class="detail-value">{{ selectedRequest.duration }}ms</text>
</view>
</view>
<view v-if="selectedRequest.headers" class="detail-section">
<text class="detail-title">请求头</text>
<pre class="detail-code">{{ JSON.stringify(selectedRequest.headers, null, 2) }}</pre>
</view>
<view v-if="selectedRequest.data" class="detail-section">
<text class="detail-title">请求数据</text>
<pre class="detail-code">{{ JSON.stringify(selectedRequest.data, null, 2) }}</pre>
</view>
<view v-if="selectedRequest.response" class="detail-section">
<text class="detail-title">响应数据</text>
<pre class="detail-code">{{ JSON.stringify(selectedRequest.response, null, 2) }}</pre>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'DebugPanel',
data() {
return {
visible: false,
activeTab: 'console',
logLevel: 'all',
logs: [],
networkRequests: [],
localStorageItems: [],
sessionStorageItems: [],
selectedRequest: null
};
},
computed: {
consoleButtonClass() {
return {
'tab-btn': true,
'tab-btn--active': this.activeTab === 'console'
};
},
networkButtonClass() {
return {
'tab-btn': true,
'tab-btn--active': this.activeTab === 'network'
};
},
storageButtonClass() {
return {
'tab-btn': true,
'tab-btn--active': this.activeTab === 'storage'
};
},
filteredLogs() {
if (this.logLevel === 'all') {
return this.logs;
}
return this.logs.filter(log => log.level === this.logLevel);
}
},
mounted() {
this.interceptConsole();
this.interceptNetwork();
this.loadStorageData();
},
methods: {
// 显示调试面板
show() {
this.visible = true;
},
// 隐藏调试面板
close() {
this.visible = false;
},
// 切换控制台
toggleConsole() {
this.activeTab = 'console';
},
// 切换网络
toggleNetwork() {
this.activeTab = 'network';
},
// 切换存储
toggleStorage() {
this.activeTab = 'storage';
this.loadStorageData();
},
// 拦截控制台输出
interceptConsole() {
const originalConsole = {
log: console.log,
warn: console.warn,
error: console.error,
info: console.info,
debug: console.debug
};
['log', 'warn', 'error', 'info', 'debug'].forEach(level => {
console[level] = (...args) => {
// 调用原始方法
originalConsole[level](...args);
// 记录到调试面板
this.addLog(level, args.join(' '), args.length > 1 ? args.slice(1) : null);
};
});
},
// 添加日志
addLog(level, message, data = null) {
const log = {
id: Date.now() + Math.random(),
level,
message,
data,
timestamp: Date.now()
};
this.logs.unshift(log);
// 只保留最近 1000 条日志
if (this.logs.length > 1000) {
this.logs = this.logs.slice(0, 1000);
}
},
// 拦截网络请求
interceptNetwork() {
const originalRequest = uni.request;
uni.request = (options) => {
const startTime = Date.now();
const requestId = Date.now() + Math.random();
const request = {
id: requestId,
url: options.url,
method: options.method || 'GET',
headers: options.header,
data: options.data,
startTime,
status: 'pending'
};
this.networkRequests.unshift(request);
const originalSuccess = options.success;
const originalFail = options.fail;
options.success = (res) => {
const endTime = Date.now();
request.status = res.statusCode;
request.duration = endTime - startTime;
request.response = res.data;
originalSuccess && originalSuccess(res);
};
options.fail = (err) => {
const endTime = Date.now();
request.status = 'error';
request.duration = endTime - startTime;
request.error = err;
originalFail && originalFail(err);
};
return originalRequest(options);
};
},
// 加载存储数据
loadStorageData() {
// 加载本地存储
this.localStorageItems = [];
try {
const info = uni.getStorageInfoSync();
info.keys.forEach(key => {
try {
const value = uni.getStorageSync(key);
this.localStorageItems.push({
key,
value: typeof value === 'object' ? JSON.stringify(value) : String(value)
});
} catch (e) {
console.error('Failed to load storage item:', key, e);
}
});
} catch (e) {
console.error('Failed to load storage info:', e);
}
},
// 清除控制台
clearConsole() {
this.logs = [];
},
// 清除网络记录
clearNetwork() {
this.networkRequests = [];
},
// 清除存储
clearStorage() {
uni.showModal({
title: '确认清除',
content: '确定要清除所有存储数据吗?',
success: (res) => {
if (res.confirm) {
uni.clearStorageSync();
this.loadStorageData();
}
}
});
},
// 删除存储项
deleteStorageItem(type, key) {
uni.removeStorageSync(key);
this.loadStorageData();
},
// 刷新存储
refreshStorage() {
this.loadStorageData();
},
// 显示请求详情
showRequestDetails(request) {
this.selectedRequest = request;
},
// 关闭请求详情
closeRequestDetails() {
this.selectedRequest = null;
},
// 日志级别变化
onLogLevelChange(e) {
this.logLevel = e.target.value;
},
// 导出日志
exportLogs() {
const data = JSON.stringify(this.logs, null, 2);
this.downloadData(data, 'console-logs.json');
},
// 导出网络记录
exportNetwork() {
const data = JSON.stringify(this.networkRequests, null, 2);
this.downloadData(data, 'network-requests.json');
},
// 下载数据
downloadData(data, filename) {
// #ifdef H5
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
// #endif
// #ifndef H5
uni.showToast({
title: '导出功能仅在H5平台支持',
icon: 'none'
});
// #endif
},
// 格式化时间
formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString();
},
// 获取日志样式类
getLogClass(log) {
return {
'console-log--error': log.level === 'error',
'console-log--warn': log.level === 'warn',
'console-log--info': log.level === 'info',
'console-log--debug': log.level === 'debug'
};
},
// 获取请求样式类
getRequestClass(request) {
return {
'network-request--success': request.status >= 200 && request.status < 300,
'network-request--error': request.status >= 400 || request.status === 'error',
'network-request--pending': request.status === 'pending'
};
}
}
};
</script>
<style scoped>
.debug-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.95);
z-index: 10000;
display: flex;
flex-direction: column;
font-family: 'Courier New', monospace;
}
.debug-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #333;
border-bottom: 1px solid #555;
}
.debug-title {
color: #fff;
font-size: 16px;
font-weight: bold;
}
.debug-controls {
display: flex;
gap: 8px;
}
.tab-btn {
padding: 6px 12px;
border: 1px solid #555;
background-color: #444;
color: #ccc;
border-radius: 4px;
font-size: 12px;
}
.tab-btn--active {
background-color: #007aff;
color: #fff;
border-color: #007aff;
}
.close-btn {
padding: 6px 12px;
border: 1px solid #ff4444;
background-color: #ff4444;
color: #fff;
border-radius: 4px;
font-size: 12px;
}
.debug-content {
flex: 1;
padding: 16px;
color: #fff;
}
.console-panel,
.network-panel,
.storage-panel {
height: 100%;
}
.console-controls,
.network-controls,
.storage-controls {
display: flex;
gap: 8px;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #555;
}
.console-controls button,
.network-controls button,
.storage-controls button {
padding: 4px 8px;
border: 1px solid #555;
background-color: #444;
color: #ccc;
border-radius: 4px;
font-size: 12px;
}
.console-logs {
max-height: 400px;
overflow-y: auto;
}
.console-log {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 4px 0;
border-bottom: 1px solid #333;
font-size: 12px;
}
.console-log--error {
color: #ff6b6b;
}
.console-log--warn {
color: #ffd93d;
}
.console-log--info {
color: #74c0fc;
}
.console-log--debug {
color: #b197fc;
}
.log-time {
color: #666;
min-width: 80px;
}
.log-level {
color: #999;
min-width: 60px;
font-weight: bold;
}
.log-message {
flex: 1;
}
.log-data {
margin-top: 4px;
padding: 8px;
background-color: #222;
border-radius: 4px;
font-size: 10px;
}
.network-requests {
max-height: 400px;
overflow-y: auto;
}
.network-request {
padding: 8px;
margin-bottom: 4px;
background-color: #333;
border-radius: 4px;
cursor: pointer;
}
.network-request--success {
border-left: 4px solid #51cf66;
}
.network-request--error {
border-left: 4px solid #ff6b6b;
}
.network-request--pending {
border-left: 4px solid #ffd93d;
}
.request-summary {
display: flex;
gap: 12px;
font-size: 12px;
}
.request-method {
color: #51cf66;
font-weight: bold;
min-width: 60px;
}
.request-url {
flex: 1;
color: #ccc;
}
.request-status {
color: #74c0fc;
min-width: 60px;
}
.request-time {
color: #ffd93d;
min-width: 60px;
}
.storage-sections {
display: flex;
flex-direction: column;
gap: 16px;
}
.storage-section {
background-color: #333;
border-radius: 8px;
padding: 12px;
}
.section-title {
display: block;
font-size: 14px;
font-weight: bold;
margin-bottom: 8px;
color: #74c0fc;
}
.storage-items {
max-height: 200px;
overflow-y: auto;
}
.storage-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
border-bottom: 1px solid #444;
font-size: 12px;
}
.item-key {
color: #51cf66;
min-width: 120px;
font-weight: bold;
}
.item-value {
flex: 1;
color: #ccc;
word-break: break-all;
}
.delete-btn {
padding: 2px 6px;
border: 1px solid #ff6b6b;
background-color: #ff6b6b;
color: #fff;
border-radius: 2px;
font-size: 10px;
}
.request-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10001;
}
.modal-content {
width: 90%;
max-width: 600px;
height: 80%;
background-color: #222;
border-radius: 8px;
display: flex;
flex-direction: column;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #444;
}
.modal-title {
color: #fff;
font-size: 16px;
font-weight: bold;
}
.modal-close {
width: 24px;
height: 24px;
border: none;
background-color: #ff6b6b;
color: #fff;
border-radius: 50%;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
flex: 1;
padding: 16px;
color: #ccc;
}
.detail-section {
margin-bottom: 16px;
}
.detail-title {
display: block;
font-size: 14px;
font-weight: bold;
margin-bottom: 8px;
color: #74c0fc;
}
.detail-item {
display: flex;
margin-bottom: 4px;
font-size: 12px;
}
.detail-label {
min-width: 60px;
color: #999;
font-weight: bold;
}
.detail-value {
flex: 1;
color: #ccc;
word-break: break-all;
}
.detail-code {
background-color: #111;
padding: 8px;
border-radius: 4px;
font-size: 10px;
color: #ccc;
white-space: pre-wrap;
word-break: break-all;
}
</style>