性能优化是 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 本章总结

学习要点回顾

  1. 性能分析

    • 关键性能指标的监控
    • 性能数据的收集和分析
    • 自定义性能监控面板
  2. 渲染优化

    • 虚拟滚动技术
    • 图片懒加载和压缩
    • 内存泄漏检测和预防
  3. 内存优化

    • 对象池模式的应用
    • 弱引用管理
    • 内存使用监控
  4. 调试工具

    • 自定义调试面板开发
    • 错误监控和上报系统
    • 网络请求和存储调试
  5. 性能测试

    • 自动化性能测试框架
    • 基准测试工具
    • 测试报告生成

实践练习

  1. 性能监控系统

    • 实现完整的性能监控系统
    • 集成错误上报功能
    • 添加性能预警机制
  2. 内存优化方案

    • 分析应用内存使用情况
    • 实现对象池优化
    • 解决内存泄漏问题
  3. 调试工具开发

    • 开发自定义调试面板
    • 实现日志管理系统
    • 添加性能分析功能

常见问题解答

Q: 如何选择合适的性能优化策略? A: 首先进行性能分析,识别瓶颈所在,然后针对性地选择优化策略。常见的优化点包括渲染性能、内存使用、网络请求等。

Q: 虚拟滚动适用于哪些场景? A: 虚拟滚动适用于需要展示大量数据的列表场景,如商品列表、聊天记录、数据表格等。当列表项数量超过几百个时,建议使用虚拟滚动。

Q: 如何有效地进行内存泄漏检测? A: 可以使用内存监控工具定期检查内存使用情况,结合弱引用管理和对象池模式来预防内存泄漏。同时要注意及时清理事件监听器、定时器等。

Q: 调试面板会影响应用性能吗? A: 调试面板确实会对性能产生一定影响,建议只在开发和测试环境中启用,生产环境应该禁用或使用轻量级的错误监控。

Q: 如何进行跨平台的性能测试? A: 可以使用统一的性能测试框架,在不同平台上运行相同的测试用例,然后比较测试结果。注意不同平台的性能特点和限制。

最佳实践建议

  1. 性能监控

    • 建立完善的性能监控体系
    • 设置合理的性能指标阈值
    • 定期分析性能数据
  2. 代码优化

    • 遵循性能优化最佳实践
    • 避免不必要的重复计算
    • 合理使用缓存机制
  3. 内存管理

    • 及时释放不需要的资源
    • 使用对象池减少垃圾回收
    • 监控内存使用情况
  4. 调试效率

    • 使用专业的调试工具
    • 建立错误监控和上报机制
    • 保持良好的日志记录习惯
  5. 测试策略

    • 制定全面的性能测试计划
    • 自动化性能测试流程
    • 持续监控性能变化

下一章预告

在下一章中,我们将学习 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>