掌握File与Blob黑科技:从浏览器操控本地文件夹到现代应用实战指南(中篇)

5 阅读17分钟

6.2 结合Service Worker实现离线文件处理

Service Worker使Web应用能够在离线状态下运行,结合File/Blob API可以创建功能完善的离线文件处理系统:

// 离线文件处理器(Service Worker端)
class OfflineFileProcessor {
  constructor() {
    this.cacheName = 'file-processor-v1';
    this.supportedFormats = {
      'image/jpeg': true,
      'image/png': true,
      'image/webp': true,
      'application/pdf': true
    };
    this.setupEventListeners();
  }

  setupEventListeners() {
    // 安装Service Worker
    self.addEventListener('install', (event) => {
      event.waitUntil(this.install());
    });

    // 激活Service Worker
    self.addEventListener('activate', (event) => {
      event.waitUntil(this.activate());
    });

    // 拦截网络请求
    self.addEventListener('fetch', (event) => {
      event.respondWith(this.handleFetch(event.request));
    });

    // 监听消息事件
    self.addEventListener('message', (event) => {
      this.handleMessage(event);
    });
  }

  async install() {
    console.log('OfflineFileProcessor installing...');
    
    // 缓存核心资源
    const cache = await caches.open(this.cacheName);
    return cache.addAll([
      '/',
      '/index.html',
      '/styles.css',
      '/app.js',
      '/offline-icon.svg'
    ]);
  }

  async activate() {
    console.log('OfflineFileProcessor activating...');
    
    // 清理旧缓存
    const cacheNames = await caches.keys();
    for (const name of cacheNames) {
      if (name !== this.cacheName) {
        await caches.delete(name);
      }
    }
    
    // 激活后立即控制所有客户端
    return self.clients.claim();
  }

  async handleFetch(request) {
    // 处理文件处理请求
    if (request.url.includes('/process-file')) {
      return this.handleFileProcessingRequest(request);
    }

    // 常规请求使用网络优先策略
    try {
      const response = await fetch(request);
      
      // 缓存成功的GET请求
      if (request.method === 'GET' && response.status === 200) {
        const cache = await caches.open(this.cacheName);
        cache.put(request, response.clone());
      }
      
      return response;
    } catch (error) {
      // 网络失败时使用缓存
      const cachedResponse = await caches.match(request);
      return cachedResponse || this.getOfflineResponse(request);
    }
  }

  async handleFileProcessingRequest(request) {
    const formData = await request.formData();
    const file = formData.get('file');
    const operation = formData.get('operation');
    
    if (!file || !operation) {
      return new Response(JSON.stringify({ error: '缺少文件或操作参数' }), {
        headers: { 'Content-Type': 'application/json' },
        status: 400
      });
    }

    try {
      // 读取文件内容
      const arrayBuffer = await  file.arrayBuffer();
      const blob = new Blob([arrayBuffer], { type: file.type });

      // 根据操作类型处理文件
      let processedBlob, outputType;
      
      switch (operation) {
        case 'compress-image':
          // 图片压缩处理
          const quality = parseFloat(formData.get('quality') || '0.8');
          [processedBlob, outputType] = await this.compressImage(blob, file.type, quality);
          break;
          
        case 'convert-to-webp':
          // 转换为WebP格式
          [processedBlob, outputType] = await this.convertToWebp(blob);
          break;
          
        case 'pdf-to-images':
          // PDF转图片(需要PDF.js库支持)
          return this.pdfToImages(blob, formData);
          
        case 'resize-image':
          // 图片尺寸调整
          const width = parseInt(formData.get('width') || '0');
          const height = parseInt(formData.get('height') || '0');
          [processedBlob, outputType] = await this.resizeImage(blob, file.type, width, height);
          break;
          
        default:
          return new Response(JSON.stringify({ error: `不支持的操作: ${operation}` }), {
            headers: { 'Content-Type': 'application/json' },
            status: 400
          });
      }

      // 创建处理结果响应
      const fileName = file.name.replace(/\.[^/.]+$/, "") + `.${outputType.split('/')[1] || 'bin'}`;
      
      return new Response(processedBlob, {
        headers: {
          'Content-Type': outputType,
          'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`
        }
      });
      
    } catch (error) {
      console.error('文件处理失败:', error);
      return new Response(JSON.stringify({ error: `文件处理失败: ${error.message}` }), {
        headers: { 'Content-Type': 'application/json' },
        status: 500
      });
    }
  }

  // 图片压缩
  async compressImage(blob, mimeType, quality = 0.8) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => {
        // 创建画布
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        
        // 设置画布尺寸(保持原始比例)
        canvas.width = img.width;
        canvas.height = img.height;
        
        // 绘制图片
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        
        // 转换为压缩后的Blob
        canvas.toBlob(
          (compressedBlob) => {
            if (!compressedBlob) {
              reject(new Error('图片压缩失败'));
              return;
            }
            resolve([compressedBlob, mimeType]);
          },
          mimeType,
          quality
        );
      };
      
      img.onerror = () => reject(new Error('无法加载图片'));
      img.src = URL.createObjectURL(blob);
    });
  }

  // 转换为WebP格式
  async convertToWebp(blob) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => {
        const canvas = document.createElement('canvas');
        canvas.width = img.width;
        canvas.height = img.height;
        
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0);
        
        // 转换为WebP
        canvas.toBlob(
          (webpBlob) => {
            if (!webpBlob) {
              reject(new Error('转换为WebP失败'));
              return;
            }
            resolve([webpBlob, 'image/webp']);
          },
          'image/webp',
          0.85 // WebP压缩质量
        );
      };
      
      img.onerror = () => reject(new Error('无法加载图片'));
      img.src = URL.createObjectURL(blob);
    });
  }

  // 调整图片尺寸
  async resizeImage(blob, mimeType, targetWidth, targetHeight) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => {
        // 计算调整后的尺寸(保持比例)
        let width = img.width;
        let height = img.height;
        
        // 如果只指定了一个维度,计算另一个维度
        if (targetWidth && !targetHeight) {
          const ratio = targetWidth / width;
          height *= ratio;
          width = targetWidth;
        } else if (targetHeight && !targetWidth) {
          const ratio = targetHeight / height;
          width *= ratio;
          height = targetHeight;
        } else if (targetWidth && targetHeight) {
          // 同时指定了宽高,直接使用
          width = targetWidth;
          height = targetHeight;
        }
        
        // 创建画布并绘制调整后的图片
        const canvas = document.createElement('canvas');
        canvas.width = Math.round(width);
        canvas.height = Math.round(height);
        
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        
        // 转换为Blob
        canvas.toBlob(
          (resizedBlob) => {
            if (!resizedBlob) {
              reject(new Error('调整图片尺寸失败'));
              return;
            }
            resolve([resizedBlob, mimeType]);
          },
          mimeType,
          0.9 // 质量
        );
      };
      
      img.onerror = () => reject(new Error('无法加载图片'));
      img.src = URL.createObjectURL(blob);
    });
  }

  // PDF转图片(需要引入PDF.js库)
  async pdfToImages(blob, formData) {
    // 检查是否加载了PDF.js
    if (typeof pdfjsLib === 'undefined') {
      return new Response(JSON.stringify({ 
        error: 'PDF处理功能未加载,请引入PDF.js库' 
      }), {
        headers: { 'Content-Type': 'application/json' },
        status: 500
      });
    }
    
    try {
      // 读取PDF数据
      const data = await blob.arrayBuffer();
      const loadingTask = pdfjsLib.getDocument({ data });
      const pdf = await loadingTask.promise;
      
      const pageNumbers = formData.get('pages') || 'all';
      const scale = parseFloat(formData.get('scale') || '1.5');
      const outputFormat = formData.get('format') || 'image/png';
      
      // 准备响应数据
      const images = [];
      
      // 确定要处理的页面范围
      let startPage = 1;
      let endPage = pdf.numPages;
      
      if (pageNumbers !== 'all' && pageNumbers.includes('-')) {
        const [start, end] = pageNumbers.split('-').map(Number);
        startPage = Math.max(1, start);
        endPage = Math.min(pdf.numPages, end);
      } else if (pageNumbers !== 'all') {
        startPage = endPage = Math.min(Math.max(1, parseInt(pageNumbers)), pdf.numPages);
      }
      
      // 处理每一页
      for (let pageNum = startPage; pageNum <= endPage; pageNum++) {
        const page = await pdf.getPage(pageNum);
        const viewport = page.getViewport({ scale });
        
        // 创建画布
        const canvas = document.createElement('canvas');
        const context = canvas.getContext('2d');
        canvas.height = viewport.height;
        canvas.width = viewport.width;
        
        // 渲染PDF页面到画布
        const renderContext = {
          canvasContext: context,
          viewport: viewport
        };
        
        await page.render(renderContext).promise;
        
        // 转换为Blob
        const pageBlob = await new Promise(resolve => 
          canvas.toBlob(resolve, outputFormat, 0.9)
        );
        
        images.push({
          page: pageNum,
          blob: pageBlob,
          type: outputFormat
        });
      }
      
      // 如果只有一页,直接返回图片
      if (images.length === 1) {
        const { blob, type } = images[0];
        return new Response(blob, {
          headers: {
            'Content-Type': type,
            'Content-Disposition': 'attachment; filename="page-1.png"'
          }
        });
      }
      
      // 多页PDF返回ZIP文件(需要JSZip库支持)
      if (typeof JSZip === 'undefined') {
        return new Response(JSON.stringify({ 
          error: '多页PDF处理需要JSZip库支持' 
        }), {
          headers: { 'Content-Type': 'application/json' },
          status: 500
        });
      }
      
      // 创建ZIP文件
      const zip = new JSZip();
      const imageFolder = zip.folder('pdf-images');
      
      for (const { page, blob, type } of images) {
        const ext = type.split('/')[1];
        imageFolder.file(`page-${page}.${ext}`, blob);
      }
      
      // 生成ZIP Blob
      const zipBlob = await zip.generateAsync({ type: 'blob' });
      
      return new Response(zipBlob, {
        headers: {
          'Content-Type': 'application/zip',
          'Content-Disposition': 'attachment; filename="pdf-images.zip"'
        }
      });
      
    } catch (error) {
      console.error('PDF处理失败:', error);
      return new Response(JSON.stringify({ error: `PDF处理失败: ${error.message}` }), {
        headers: { 'Content-Type': 'application/json' },
        status: 500
      });
    }
  }

  // 获取离线响应
  getOfflineResponse(request) {
    // 根据请求类型返回不同的离线响应
    if (request.mode === 'navigate') {
      return caches.match('/index.html');
    }
    
    return new Response(JSON.stringify({ 
      error: '离线模式下无法完成请求' 
    }), {
      headers: { 'Content-Type': 'application/json' },
      status: 503
    });
  }

  // 处理来自客户端的消息
  async handleMessage(event) {
    const { type, data } = event.data;
    
    switch (type) {
      case 'cache-files':
        // 缓存指定文件
        await this.cacheAdditionalFiles(data.files);
        event.source.postMessage({ 
          type: 'cache-complete', 
          success: true 
        }, '*');
        break;
        
      case 'clear-cache':
        // 清除缓存
        await this.clearCache();
        event.source.postMessage({ 
          type: 'cache-cleared', 
          success: true 
        }, '*');
        break;
        
      case 'list-cached-files':
        // 列出缓存文件
        const files = await this.listCachedFiles();
        event.source.postMessage({ 
          type: 'cached-files', 
          files 
        }, '*');
        break;
    }
  }

  // 缓存额外文件
  async cacheAdditionalFiles(fileUrls) {
    if (!Array.isArray(fileUrls) || fileUrls.length === 0) return;
    
    const cache = await caches.open(this.cacheName);
    const requests = fileUrls.map(url => new Request(url));
    
    // 只缓存不存在的文件
    for (const request of requests) {
      const cached = await caches.match(request);
      if (!cached) {
        try {
          const response = await fetch(request);
          if (response.status === 200) {
            await cache.put(request, response);
          }
        } catch (error) {
          console.warn(`缓存文件失败: ${request.url}`, error);
        }
      }
    }
  }

  // 清除缓存
  async clearCache() {
    const cache = await caches.open(this.cacheName);
    const keys = await cache.keys();
    await Promise.all(keys.map(key => cache.delete(key)));
  }

  // 列出缓存文件
  async listCachedFiles() {
    const cache = await caches.open(this.cacheName);
    const keys = await cache.keys();
    return keys.map(key => key.url);
  }

  // 获取离线响应
  getOfflineResponse(request) {
    // 返回离线页面或资源
    if (request.headers.get('Accept').includes('text/html')) {
      return caches.match('/offline.html') || new Response(
        '<!DOCTYPE html><html><body><h1>离线模式</h1><p>无法连接到网络,请检查您的连接。</p></body></html>',
        { headers: { 'Content-Type': 'text/html' } }
      );
    }
    
    return new Response(JSON.stringify({ offline: true }), {
      headers: { 'Content-Type': 'application/json' }
    });
  }
}

// 初始化离线文件处理器
if ('serviceWorker' in self) {
  const processor = new OfflineFileProcessor();
}

6.3 结合WebCodecs API实现高级媒体处理

WebCodecs API提供了对音频和视频编码/解码的直接访问,结合File/Blob API可以构建强大的客户端媒体处理应用:

// 基于WebCodecs的媒体文件处理器
class MediaFileProcessor {
  constructor() {
    this.supported = this.checkSupport();
    this.codecs = {
      video: {
        'avc1.42001E': 'H.264 Baseline',
        'avc1.4D401E': 'H.264 Main',
        'avc1.64001F': 'H.264 High',
        'vp09.00.10.08': 'VP9',
        'av01.0.05M.08': 'AV1'
      },
      audio: {
        'mp4a.40.2': 'AAC LC',
        'mp4a.40.5': 'AAC HE',
        'opus': 'Opus'
      }
    };
  }

  // 检查浏览器支持情况
  checkSupport() {
    return 'VideoDecoder' in window && 
           'VideoEncoder' in window && 
           'AudioDecoder' in window && 
           'AudioEncoder' in window && 
           'MediaSource' in window;
  }

  // 解码视频文件
  async decodeVideo(blob, progressCallback) {
    if (!this.supported) {
      throw new Error('您的浏览器不支持WebCodecs API');
    }
    
    const start = performance.now();
    const tracks = [];
    let videoTrack, audioTrack;
    
    try {
      // 创建媒体源
      const mediaSource = new MediaSource();
      const url = URL.createObjectURL(blob);
      
      // 使用VideoFrame和AudioData解码
      const demuxer = await this.demuxFile(blob);
      
      // 检查是否有视频轨道
      if (demuxer.videoTracks.length > 0) {
        videoTrack = demuxer.videoTracks[0];
        tracks.push(...await this.decodeVideoTrack(videoTrack, progressCallback));
      }
      
      // 检查是否有音频轨道
      if (demuxer.audioTracks.length > 0) {
        audioTrack = demuxer.audioTracks[0];
        tracks.push(...await this.decodeAudioTrack(audioTrack, progressCallback));
      }
      
      URL.revokeObjectURL(url);
      
      const duration = (performance.now() - start) / 1000;
      console.log(`媒体解码完成,耗时: ${duration.toFixed(2)}s`);
      
      return {
        tracks,
        duration,
        videoTrack,
        audioTrack
      };
    } catch (error) {
      console.error('媒体解码失败:', error);
      throw error;
    }
  }

  // 解复用文件(简化实现,实际应用需使用更完善的解复用库)
  async demuxFile(blob) {
    // 在实际应用中,这里会使用如mp4box.js等库进行文件解复用
    // 这里返回模拟的轨道信息
    return {
      videoTracks: [{
        type: 'video',
        codec: 'avc1.42001E',
        width: 1920,
        height: 1080,
        duration: 10, // 秒
        frames: []
      }],
      audioTracks: [{
        type: 'audio',
        codec: 'mp4a.40.2',
        sampleRate: 44100,
        channels: 2,
        duration: 10 // 秒
      }]
    };
  }

  // 解码视频轨道
  async decodeVideoTrack(track, progressCallback) {
    const frames = [];
    const decoderConfig = {
      codec: track.codec,
      width: track.width,
      height: track.height,
      hardwareAcceleration: 'prefer-software',
      optimizeForLatency: false
    };
     // 创建视频解码器
    const decoder = new VideoDecoder({
      output: (frame) => {
        frames.push(frame);
        progressCallback && progressCallback({
          type: 'video-decode',
          progress: (frames.length / track.frames.length) * 100,
          framesDecoded: frames.length,
          totalFrames: track.frames.length
        });
      },
      error: (error) => {
        console.error('视频解码错误:', error);
        throw error;
      }
    });

    // 配置解码器
    await decoder.configure(decoderConfig);

    // 处理视频块(简化实现)
    for (let i = 0; i < track.frames.length; i++) {
      const chunk = track.frames[i];
      
      // 将视频块送入解码器
      decoder.decode(new EncodedVideoChunk({
        type: i === 0 ? 'key' : 'delta',
        data: chunk.data,
        timestamp: chunk.timestamp,
        duration: chunk.duration
      }));
      
      // 防止解码器缓冲区溢出
      if (i % 10 === 0) {
        await new Promise(resolve => setTimeout(resolve, 10));
      }
    }

    // 完成解码并等待所有帧输出
    await decoder.flush();
    decoder.close();

    return [{
      type: 'video',
      codec: track.codec,
      codecName: this.codecs.video[track.codec] || track.codec,
      width: track.width,
      height: track.height,
      frames,
      duration: track.duration
    }];
  }

  // 解码音频轨道
  async decodeAudioTrack(track, progressCallback) {
    const samples = [];
    const decoderConfig = {
      codec: track.codec,
      sampleRate: track.sampleRate,
      numberOfChannels: track.channels
    };

    // 创建音频解码器
    const decoder = new AudioDecoder({
      output: (data) => {
        samples.push(data);
        progressCallback && progressCallback({
          type: 'audio-decode',
          progress: (samples.length / track.samples.length) * 100,
          samplesDecoded: samples.length,
          totalSamples: track.samples.length
        });
      },
      error: (error) => {
        console.error('音频解码错误:', error);
        throw error;
      }
    });

    // 配置解码器
    await decoder.configure(decoderConfig);

    // 处理音频块
    for (let i = 0; i < track.samples.length; i++) {
      const chunk = track.samples[i];
      
      decoder.decode(new EncodedAudioChunk({
        type: i === 0 ? 'key' : 'delta',
        data: chunk.data,
        timestamp: chunk.timestamp,
        duration: chunk.duration
      }));
    }

    // 完成解码
    await decoder.flush();
    decoder.close();

    return [{
      type: 'audio',
      codec: track.codec,
      codecName: this.codecs.audio[track.codec] || track.codec,
      sampleRate: track.sampleRate,
      channels: track.channels,
      samples,
      duration: track.duration
    }];
  }

  // 编码视频文件
  async encodeVideo(frames, config, progressCallback) {
    if (!this.supported) {
      throw new Error('您的浏览器不支持WebCodecs API');
    }

    const encodedChunks = [];
    const startTime = performance.now();
    
    // 默认编码配置
    const encodeConfig = {
      codec: config.codec || 'avc1.42001E', // H.264 Baseline
      width: config.width,
      height: config.height,
      bitrate: config.bitrate || 5_000_000, // 5 Mbps
      framerate: config.framerate || 30,
      hardwareAcceleration: config.hardwareAcceleration || 'prefer-hardware',
      latencyMode: 'quality'
    };

    // 创建视频编码器
    const encoder = new VideoEncoder({
      output: (chunk) => {
        encodedChunks.push(chunk);
        progressCallback && progressCallback({
          type: 'video-encode',
          progress: (encodedChunks.length / frames.length) * 100,
          chunksEncoded: encodedChunks.length,
          totalFrames: frames.length
        });
      },
      error: (error) => {
        console.error('视频编码错误:', error);
        throw error;
      }
    });

    // 配置编码器
    await encoder.configure(encodeConfig);

    // 编码每一帧
    const frameDuration = Math.floor(1_000_000 / encodeConfig.framerate); // 微秒
    let timestamp = 0;

    for (let i = 0; i < frames.length; i++) {
      const frame = frames[i];
      
      // 调整帧大小以匹配编码配置
      const resizedFrame = this.resizeVideoFrame(frame, encodeConfig.width, encodeConfig.height);
      
      // 设置时间戳
      resizedFrame.timestamp = timestamp;
      timestamp += frameDuration;
      
      // 编码帧
      encoder.encode(resizedFrame);
      
      // 释放原始帧
      frame.close();
      resizedFrame.close();
      
      // 防止编码器缓冲区溢出
      if (i % 10 === 0) {
        await new Promise(resolve => setTimeout(resolve, 10));
      }
    }

    // 完成编码
    await encoder.flush();
    encoder.close();

    // 计算编码统计信息
    const duration = (performance.now() - startTime) / 1000;
    const totalSize = encodedChunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
    const bitrate = Math.round((totalSize * 8) / duration / 1000); // kbps

    // 将编码后的块转换为Blob
    const data = new Uint8Array(totalSize);
    let offset = 0;
    
    encodedChunks.forEach(chunk => {
      data.set(new Uint8Array(chunk.data), offset);
      offset += chunk.data.byteLength;
    });

    return {
      blob: new Blob([data], { type: 'video/mp4' }),
      duration,
      size: totalSize,
      bitrate,
      codec: encodeConfig.codec,
      codecName: this.codecs.video[encodeConfig.codec] || encodeConfig.codec,
      width: encodeConfig.width,
      height: encodeConfig.height
    };
  }

  // 调整视频帧大小
  resizeVideoFrame(frame, targetWidth, targetHeight) {
    // 如果尺寸已经匹配,直接返回
    if (frame.width === targetWidth && frame.height === targetHeight) {
      return frame;
    }

    // 创建调整大小的画布
    const canvas = new OffscreenCanvas(targetWidth, targetHeight);
    const ctx = canvas.getContext('2d');
    
    // 绘制并缩放原始帧
    ctx.drawImage(frame, 0, 0, targetWidth, targetHeight);
    
    // 从画布创建新的视频帧
    return new VideoFrame(canvas, {
      timestamp: frame.timestamp,
      duration: frame.duration
    });
  }

  // 从视频帧创建GIF动画
  async createGifFromFrames(frames, options = {}) {
    const {
      delay = 100, // 每帧延迟(毫秒)
      loop = 0,    // 0 = 无限循环
      width = frames[0].width,
      height = frames[0].height,
      quality = 10 // 1-20,越低质量越高
    } = options;

    // 检查是否有GIF编码器库(如gif.js)
    if (typeof GIFEncoder === 'undefined') {
      throw new Error('GIF编码需要gif.js库支持');
    }

    // 创建GIF编码器
    const encoder = new GIFEncoder();
    const chunks = [];
    
    // 收集编码后的GIF数据
    encoder.on('data', (chunk) => chunks.push(chunk));
    
    // 配置编码器
    encoder.setRepeat(loop);
    encoder.setDelay(delay);
    encoder.setQuality(quality);
    encoder.setSize(width, height);
    encoder.start();

    // 将视频帧添加到GIF
    for (let i = 0; i < frames.length; i += Math.max(1, Math.floor(frames.length / 50))) {
      const frame = frames[i];
      
      // 将VideoFrame转换为ImageBitmap
      const imageBitmap = await createImageBitmap(frame);
      
      // 创建临时画布绘制帧
      const canvas = new OffscreenCanvas(frame.width, frame.height);
      const ctx = canvas.getContext('2d');
      ctx.drawImage(imageBitmap, 0, 0);
      
      // 调整大小并添加到GIF
      const resizedCanvas = new OffscreenCanvas(width, height);
      const resizedCtx = resizedCanvas.getContext('2d');
      resizedCtx.drawImage(canvas, 0, 0, width, height);
      
      // 将像素数据添加到GIF编码器
      const imageData = resizedCtx.getImageData(0, 0, width, height);
      encoder.addFrame(imageData.data, true);
      
      // 释放资源
      frame.close();
      imageBitmap.close();
    }

    // 完成GIF编码
    encoder.finish();

    // 创建Blob
    const blob = new Blob(chunks, { type: 'image/gif' });
    
    return {
      blob,
      width,
      height,
      frameCount: chunks.length,
      size: blob.size
    };
  }
}

// WebCodecs媒体处理器应用示例
class MediaEditor {
  constructor() {
    this.processor = new MediaFileProcessor();
    this.mediaData = null;
    this.supported = this.processor.supported;
  }

  // 加载媒体文件
  async loadFile(file, progressCallback) {
    if (!this.supported) {
      throw new Error('您的浏览器不支持WebCodecs API');
    }

    this.mediaData = await this.processor.decodeVideo(file, progressCallback);
    return this.mediaData;
  }

  // 视频转GIF
  async convertToGif(options = {}, progressCallback) {
    if (!this.mediaData || !this.mediaData.tracks || this.mediaData.tracks.length === 0) {
      throw new Error('没有加载媒体文件');
    }

    // 找到视频轨道
    const videoTrack = this.mediaData.tracks.find(track => track.type === 'video');
    if (!videoTrack) {
      throw new Error('媒体文件中没有视频轨道');
    }

    progressCallback && progressCallback({
      type: 'gif-conversion',
      status: 'started',
      message: '开始视频转GIF处理'
    });

    // 创建GIF
    const gifResult = await this.processor.createGifFromFrames(
      videoTrack.frames,
      options,
      (progress) => {
        progressCallback && progressCallback({
          type: 'gif-conversion',
          ...progress
        });
      }
    );

    progressCallback && progressCallback({
      type: 'gif-conversion',
      status: 'complete',
      message: 'GIF转换完成',
      result: gifResult
    });

    return gifResult;
  }

  // 压缩视频文件
  async compressVideo(options = {}, progressCallback) {
    if (!this.mediaData || !this.mediaData.tracks || this.mediaData.tracks.length === 0) {
      throw new Error('没有加载媒体文件');
    }

    // 找到视频轨道
    const videoTrack = this.mediaData.tracks.find(track => track.type === 'video');
    if (!videoTrack) {
      throw new Error('媒体文件中没有视频轨道');
    }

    progressCallback && progressCallback({
      type: 'video-compression',
      status: 'started',
      message: '开始视频压缩处理'
    });

    // 编码压缩后的视频
    const compressedVideo = await this.processor.encodeVideo(
      videoTrack.frames,
      options,
      (progress) => {
        progressCallback && progressCallback({
          type: 'video-compression',
          ...progress
        });
      }
    );

    progressCallback && progressCallback({
      type: 'video-compression',
      status: 'complete',
      message: '视频压缩完成',
      result: compressedVideo
    });

    return compressedVideo;
  }

  // 提取视频帧
  extractFrames(count = 10, progressCallback) {
    if (!this.mediaData || !this.mediaData.tracks || this.mediaData.tracks.length === 0) {
      throw new Error('没有加载媒体文件');
    }

    // 找到视频轨道
    const videoTrack = this.mediaData.tracks.find(track => track.type === 'video');
    if (!videoTrack) {
      throw new Error('媒体文件中没有视频轨道');
    }

    const totalFrames = videoTrack.frames.length;
    const step = Math.max(1, Math.floor(totalFrames / count));
    const extractedFrames = [];
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    canvas.width = videoTrack.width;
    canvas.height = videoTrack.height;

    // 提取帧
    for (let i = 0; i < totalFrames && extractedFrames.length < count; i += step) {
      const frame = videoTrack.frames[i];
      
      // 绘制帧到画布
      ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
      
      // 转换为Blob
      const blob = await new Promise(resolve => {
        canvas.toBlob(resolve, 'image/png');
      });
      
      extractedFrames.push({
        index: i,
        timestamp: frame.timestamp,
        blob,
        width: canvas.width,
        height: canvas.height
      });

      progressCallback && progressCallback({
        type: 'frame-extraction',
        progress: (extractedFrames.length / count) * 100,
        extracted: extractedFrames.length,
        total: count,
        currentFrame: i
      });
    }

    return extractedFrames;
  }

  // 释放资源
  dispose() {
    if (this.mediaData && this.mediaData.tracks) {
      // 释放所有帧资源
      this.mediaData.tracks.forEach(track => {
        if (track.frames && track.frames.length > 0) {
          track.frames.forEach(frame => {
            try {
              frame.close();
            } catch (error) {
              console.warn('释放帧资源失败:', error);
            }
          });
          track.frames = [];
        }
      });
    }
    this.mediaData = null;
  }
}

七、性能优化与最佳实践

7.1 大文件处理的内存管理策略

处理大型File和Blob对象时,内存管理至关重要。不当的处理方式可能导致浏览器崩溃或严重的性能问题:

// 大文件处理内存管理器
class LargeFileMemoryManager {
  constructor() {
    this.activeObjects = new Map();
    this.memoryLimit = this.calculateMemoryLimit();
    this.memoryUsage = 0;
    this.cleanupInterval = setInterval(() => this.cleanupUnusedObjects(), 30000); // 每30秒清理一次
    this.setupMemoryMonitoring();
  }

  // 计算内存限制(基于浏览器和系统)
  calculateMemoryLimit() {
    // 检测浏览器环境
    const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
    const isChrome = /Chrome/i.test(navigator.userAgent) && !/Edge/i.test(navigator.userAgent);
    
    // 根据环境设置不同的内存限制(MB)
    if (isMobile) {
      return 256; // 移动设备限制256MB
    } else if (isChrome) {
      return 1024; // Chrome限制1GB
    } else {
      return 512; // 其他浏览器限制512MB
    }
  }

  // 设置内存监控
  setupMemoryMonitoring() {
    if (performance.memory) {
      setInterval(() => {
        const memory = performance.memory;
        this.memoryUsage = memory.usedJSHeapSize / (1024 * 1024); // MB
        
        // 内存使用超过限制的80%时触发警告
        if (this.memoryUsage > this.memoryLimit * 0.8) {
          console.warn(`内存使用警告: ${this.memoryUsage.toFixed(2)}MB / ${this.memoryLimit}MB`);
          this.cleanupUnusedObjects(true); // 强制清理
        }
      }, 5000);
    }
  }

  // 跟踪File/Blob对象
  trackObject(id, object, metadata = {}) {
    if (!(object instanceof Blob || object instanceof File)) {
      throw new Error('只能跟踪Blob或File对象');
    }

     // 计算对象大小(近似值)
    const size = object.size || object.byteLength;
    
    // 如果添加此对象会超出内存限制,则先清理
    if (this.memoryUsage + size / (1024 * 1024) > this.memoryLimit) {
      console.warn('即将超出内存限制,尝试清理未使用对象');
      this.cleanupUnusedObjects(true);
    }
    
    // 记录对象信息
    this.activeObjects.set(id, {
      object,
      metadata,
      size,
      lastAccessed: Date.now(),
      references: 1,
      isPersistent: metadata.persistent || false
    });
    
    // 更新内存使用量
    this.memoryUsage += size / (1024 * 1024);
    
    return {
      id,
      release: () => this.releaseObject(id),
      get: () => this.getObject(id)
    };
  }

  // 获取跟踪的对象
  getObject(id) {
    const entry = this.activeObjects.get(id);
    if (!entry) {
      throw new Error(`未找到ID为 ${id} 的对象`);
    }
    
    // 更新最后访问时间
    entry.lastAccessed = Date.now();
    entry.references++;
    
    return entry.object;
  }

  // 释放对象引用
  releaseObject(id) {
    const entry = this.activeObjects.get(id);
    if (!entry) return false;
    
    entry.references--;
    console.log(`释放对象引用: ${id} (剩余引用: ${entry.references})`);
    
    // 如果引用计数为0且不是持久对象,则可以清理
    if (entry.references <= 0 && !entry.isPersistent) {
      this.activeObjects.delete(id);
      
      // 更新内存使用量
      this.memoryUsage -= entry.size / (1024 * 1024);
      
      console.log(`对象 ${id} 已从内存中清理 (大小: ${(entry.size / (1024 * 1024)).toFixed(2)}MB)`);
      return true;
    }
    
    return false;
  }

  // 清理未使用的对象
  cleanupUnusedObjects(force = false) {
    const now = Date.now();
    const threshold = force ? 0 : 60000; // 非强制清理时,清理60秒未使用的对象
    let cleanedCount = 0;
    let cleanedSize = 0;
    
    for (const [id, entry] of this.activeObjects.entries()) {
      // 跳过持久对象和最近使用的对象
      if (entry.isPersistent) continue;
      
      if (force || now - entry.lastAccessed > threshold) {
        // 如果引用计数为0或强制清理
        if (entry.references <= 0 || force) {
          this.activeObjects.delete(id);
          cleanedCount++;
          cleanedSize += entry.size;
          this.memoryUsage -= entry.size / (1024 * 1024);
        }
      }
    }
    
    if (cleanedCount > 0) {
      console.log(`清理了 ${cleanedCount} 个未使用对象,释放了 ${(cleanedSize / (1024 * 1024)).toFixed(2)}MB 内存`);
    }
    
    return { cleanedCount, cleanedSize };
  }

  // 强制清理所有可清理对象
  forceCleanup() {
    return this.cleanupUnusedObjects(true);
  }

  // 持久化存储对象(不会被自动清理)
  persistObject(id) {
    const entry = this.activeObjects.get(id);
    if (!entry) return false;
    
    entry.isPersistent = true;
    return true;
  }

  // 取消对象持久化
  unpersistObject(id) {
    const entry = this.activeObjects.get(id);
    if (!entry) return false;
    
    entry.isPersistent = false;
    entry.lastAccessed = Date.now();
    return true;
  }

  // 获取当前内存状态
  getMemoryStatus() {
    return {
      usage: this.memoryUsage,
      limit: this.memoryLimit,
      usagePercentage: (this.memoryUsage / this.memoryLimit) * 100,
      trackedObjects: this.activeObjects.size,
      totalTrackedSize: Array.from(this.activeObjects.values())
        .reduce((sum, entry) => sum + entry.size, 0) / (1024 * 1024)
    };
  }

  // 销毁管理器
  destroy() {
    clearInterval(this.cleanupInterval);
    this.activeObjects.clear();
    this.memoryUsage = 0;
  }
}

// 大文件分块处理工具
class LargeFileProcessor {
  constructor(memoryManager) {
    this.memoryManager = memoryManager || new LargeFileMemoryManager();
    this.chunkSize = 64 * 1024; // 默认64KB块大小
    this.maxParallelChunks = navigator.hardwareConcurrency || 4; // 基于CPU核心数的并行处理限制
  }

  // 设置块大小(根据文件类型和处理需求调整)
  setChunkSize(size) {
    if (size < 1024 || size > 10 * 1024 * 1024) {
      throw new Error('块大小必须在1KB到10MB之间');
    }
    this.chunkSize = size;
    return this;
  }

  // 处理大文件(带进度和取消支持)
  async processFile(file, processor, options = {}) {
    const {
      progressCallback,
      cancelSignal,
      chunkSize = this.chunkSize,
      parallel = this.maxParallelChunks,
      metadata = {}
    } = options;

    // 跟踪文件处理
    const fileId = `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    const fileHandle = this.memoryManager.trackObject(fileId, file, {
      ...metadata,
      type: 'source-file',
      processing: true
    });

    try {
      const totalSize = file.size;
      const totalChunks = Math.ceil(totalSize / chunkSize);
      const results = new Array(totalChunks);
      let processedChunks = 0;
      let startTime = Date.now();
      let lastProgressTime = 0;

      // 检查取消信号
      if (cancelSignal && cancelSignal.aborted) {
        throw new Error('处理已被取消');
      }

      // 创建取消处理函数
      const onCancel = () => {
        throw new Error('处理已被取消');
      };
      cancelSignal?.addEventListener('abort', onCancel);

      // 更新进度的辅助函数
      const updateProgress = (chunkIndex, chunkResult) => {
        processedChunks++;
        const progress = (processedChunks / totalChunks) * 100;
        const now = Date.now();
        const elapsed = now - startTime;
        const speed = processedChunks * chunkSize / (elapsed / 1000); // B/s
        
        // 限制进度更新频率(最多每秒10次)
        if (now - lastProgressTime > 100 || progress >= 100) {
          lastProgressTime = now;
          progressCallback?.({
            percent: progress,
            processed: processedChunks,
            total: totalChunks,
            chunkIndex,
            chunkResult,
            speed,
            estimatedTime: progress > 0 ? (elapsed / progress) * (100 - progress) : 0
          });
        }
      };

      // 创建块处理函数
      const processChunk = async (chunkIndex) => {
        // 检查取消信号
        if (cancelSignal && cancelSignal.aborted) {
          return;
        }

        try {
          const start = chunkIndex * chunkSize;
          const end = Math.min(start + chunkSize, totalSize);
          const chunk = file.slice(start, end);
          
          // 跟踪块对象
          const chunkId = `${fileId}-chunk-${chunkIndex}`;
          const chunkHandle = this.memoryManager.trackObject(chunkId, chunk, {
            type: 'file-chunk',
            fileId,
            chunkIndex,
            start,
            end
          });

          // 处理块
          const result = await processor({
            chunk: chunkHandle.get(),
            index: chunkIndex,
            totalChunks,
            start,
            end,
            file
          });

          // 存储结果并更新进度
          results[chunkIndex] = result;
          updateProgress(chunkIndex, result);

          // 释放块资源
          chunkHandle.release();
          
          return result;
        } catch (error) {
          console.error(`处理块 ${chunkIndex} 失败:`, error);
          
          // 如果是可恢复错误,尝试重试
          if (options.maxRetries && error.retryable !== false) {
            const retryCount = (error.retryCount || 0) + 1;
            if (retryCount <= options.maxRetries) {
              console.log(`重试处理块 ${chunkIndex} (第 ${retryCount} 次)`);
              error.retryCount = retryCount;
              return processChunk(chunkIndex); // 递归重试
            }
          }
          
          // 传播错误
          throw error;
        }
      };

      // 创建块索引数组
      const chunkIndices = Array.from({ length: totalChunks }, (_, i) => i);
      
      // 并行处理块(控制并发数量)
      const parallelBatches = [];
      for (let i = 0; i < chunkIndices.length; i += parallel) {
        parallelBatches.push(chunkIndices.slice(i, i + parallel));
      }

      // 按批次处理
      for (const batch of parallelBatches) {
        await Promise.all(batch.map(index => processChunk(index)));
      }

      // 处理完成,移除取消监听
      cancelSignal?.removeEventListener('abort', onCancel);

      // 整理结果
      const finalResult = {
        file,
        totalSize,
        totalChunks,
        processedChunks,
        duration: Date.now() - startTime,
        results: results.filter(Boolean) // 过滤空结果
      };

      progressCallback?.({
        percent: 100,
        processed: processedChunks,
        total: totalChunks,
        complete: true,
        result: finalResult
      });

      return finalResult;
    } finally {
      // 释放文件资源
      fileHandle.release();
      this.memoryManager.persistObject(fileId, false);
    }
  }

  // 合并处理结果
  async mergeResults(results, merger, options = {}) {
    const {
      progressCallback,
      cancelSignal,
      metadata = {}
    } = options;

    // 检查取消信号
    if (cancelSignal && cancelSignal.aborted) {
      throw new Error('合并已被取消');
    }

    const mergeId = `merge-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    let progress = 0;
    const totalResults = results.length;

    // 更新进度的辅助函数
    const updateProgress = (step, message, data = {}) => {
      progress = Math.min(100, (step / totalResults) * 100);
      progressCallback?.({
        percent: progress,
        step,
        total: totalResults,
        message,
        ...data
      });
    };

    try {
      updateProgress(0, '开始合并结果');
      
      // 执行合并器函数
      const mergedResult = await merger(results, {
        updateProgress: (step, message, data) => {
          updateProgress(step, message, data);
        },
        cancelSignal
      });

      // 如果合并结果是Blob/File对象,进行跟踪
      if (mergedResult instanceof Blob || mergedResult instanceof File) {
        const resultHandle = this.memoryManager.trackObject(mergeId, mergedResult, {
          ...metadata,
          type: 'merged-result',
          timestamp: Date.now()
        });
        
        updateProgress(totalResults, '合并完成', {
          resultId: mergeId,
          size: mergedResult.size,
          type: mergedResult.type
        });
        
        return {
          ...mergedResult,
          manager: {
            id: mergeId,
            release: () => resultHandle.release(),
            persist: () => this.memoryManager.persistObject(mergeId)
          }
        };
      }

      updateProgress(totalResults, '合并完成');
      return mergedResult;
    } catch (error) {
      if (cancelSignal && cancelSignal.aborted) {
        updateProgress(progress, '合并已取消');
      } else {
        updateProgress(progress, `合并失败: ${error.message}`, { error });
        console.error('结果合并失败:', error);
      }
      throw error;
    }
  }
}

7.2 浏览器兼容性处理与降级方案

File和Blob API虽然已被现代浏览器广泛支持,但在处理高级特性时仍需考虑兼容性问题:

// File/Blob API兼容性处理工具
class FileApiCompat {
  constructor() {
    this.support = {
      blobConstructor: 'Blob' in window && typeof Blob === 'function',
      blobStream: 'stream' in Blob.prototype,
      fileSystemAccess: 'showOpenFilePicker' in window,
      readableStream: 'ReadableStream' in window,
      webCodecs: 'VideoDecoder' in window && 'VideoEncoder' in window,
      arrayBuffer: 'ArrayBuffer' in window,
      textEncoder: 'TextEncoder' in window,
      textDecoder: 'TextDecoder' in window,
      fileConstructor: 'File' in window && typeof File === 'function',
      urlObject: 'URL' in window && 'createObjectURL' in URL,
      directoryPicker: 'showDirectoryPicker' in window,
      structuredClone: 'structuredClone' in window
    };
    
    // 记录兼容性检测结果
    console.log('File/Blob API兼容性检测结果:', this.support);
  }

  // 创建Blob对象(兼容旧浏览器)
  createBlob(data, options = {}) {
    if (this.support.blobConstructor) {
      return new Blob(data, options);
    }
    
    // 旧浏览器降级方案(IE10-)
    const BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MSBlobBuilder;
    if (!BlobBuilder) {
      throw new Error('您的浏览器不支持Blob API');
    }
    
    const builder = new BlobBuilder();
    for (const chunk of data) {
      builder.append(chunk);
    }
    
    return options.type ? builder.getBlob(options.type) : builder.getBlob();
  }

  // 将Blob转换为ArrayBuffer(提供多种降级方案)
  async blobToArrayBuffer(blob) {
    // 优先使用现代API
    if (blob.arrayBuffer) {
      return blob.arrayBuffer();
    }
    
    // 降级方案: FileReader
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result);
      reader.onerror = () => reject(reader.error);
      reader.readAsArrayBuffer(blob);
    });
  }

  // 将Blob转换为文本(提供多种降级方案)
  async blobToText(blob, encoding = 'utf-8') {
    // 优先使用现代API
    if (blob.text) {
      return blob.text();
    }
    
    // 降级方案: FileReader
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result);
      reader.onerror = () => reject(reader.error);
      reader.readAsText(blob, encoding);
    });
  }

  // 创建Blob URL(带兼容性处理)
  createObjectURL(blob) {
    if (!this.support.urlObject) {
      throw new Error('您的浏览器不支持URL.createObjectURL');
    }
    
    return URL.createObjectURL(blob);
  }

  // 释放Blob URL(带兼容性处理)
  revokeObjectURL(url) {
    if (this.support.urlObject) {
      URL.revokeObjectURL(url);
    }
  }

  // 打开文件选择器(提供文件系统访问API和传统input的降级方案)
  async openFilePicker(options = {}) {
    const {
      multiple = false,
      types = [],
      excludeAcceptAllOption = true,
      legacyInput = null
    } = options;
    
    // 优先使用文件系统访问API
    if (this.support.fileSystemAccess && window.showOpenFilePicker) {
      try {
        const fileHandles = await window.showOpenFilePicker({
          multiple,
          types,
          excludeAcceptAllOption
        });
        
        // 获取File对象数组
        return Promise.all(
          fileHandles.map(handle => handle.getFile())
        );
      } catch (error) {
        // 如果用户取消选择或发生错误,尝试降级方案
        console.warn('文件系统访问API访问失败,尝试降级方案:', error);
      }
    }
    
    // 降级方案: 使用<input type="file">元素
    return new Promise((resolve, reject) => {
      // 创建或复用input元素
      let input = legacyInput;
      if (!input) {
        input = document.createElement('input');
        input.type = 'file';
        input.style.display = 'none';
        
        // 添加到文档以确保能触发事件
        document.body.appendChild(input);
        
        // 注册清理函数
        const cleanup = () => {
          document.body.removeChild(input);
        };
      }
      
      // 配置input元素
      input.multiple = multiple;
      input.accept = types.map(type => type.accept).join(',') || '';
      
      // 处理文件选择事件
      const handleChange = (e) => {
        const files = Array.from(e.target.files);
        
        // 移除事件监听器
        input.removeEventListener('change', handleChange);
        input.removeEventListener('cancel', handleCancel);
        
        // 如果没有提供外部input元素,清理创建的元素
        if (!legacyInput) {
          cleanup();
        }
        
        resolve(files);
      };
      
      const handleCancel = () => {
        input.removeEventListener('change', handleChange);
        input.removeEventListener('cancel', handleCancel);
        
        if (!legacyInput) {
          cleanup();
        }
        
        resolve([]);
      };
      
      // 添加事件监听器
      input.addEventListener('change', handleChange);
      input.addEventListener('cancel', handleCancel);
      
      // 触发文件选择对话框
      input.click();
    });
  }

  // 保存Blob到文件(提供多种方案)
  async saveBlobToFile(blob, suggestedName) {
    // 方案1: 使用File System Access API的文件保存功能
    if (this.support.fileSystemAccess && window.showSaveFilePicker) {
      try {
        // 提取文件扩展名
        const ext = suggestedName.split('.').pop() || '';
        const mimeType = blob.type || '';
        
        // 配置文件选择器选项
        const options = {
          suggestedName,
          types: [{
            description: `${mimeType.split('/')[0]} file`,
            accept: {
              [mimeType]: ext ? [`.${ext}`] : []
            }
          }]
        };
        
        // 显示保存对话框
        const handle = await window.showSaveFilePicker(options);
        
        // 写入文件内容
        const writable = await handle.createWritable();
        await writable.write(blob);
        await writable.close();
        
        return {
          success: true,
          handle,
          name: handle.name,
          using: 'file-system-access-api'
        };
      } catch (error) {
        console.warn('文件系统访问API保存失败,尝试降级方案:', error);
      }
    }
    
    // 方案2: 使用a标签下载(传统方法)
    if (this.support.urlObject) {
      try {
        const url = this.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = suggestedName;
        a.style.display = 'none';
        
        document.body.appendChild(a);
        a.click();
        
        // 清理
        setTimeout(() => {
          document.body.removeChild(a);
          this.revokeObjectURL(url);
        }, 100);
        
        return {
          success: true,
          using: 'a-tag-download'
        };
      } catch (error) {
        console.warn('a标签下载失败,尝试降级方案:', error);
      }
    }
    
    // 方案3: 仅提供Blob URL(最低级降级方案)
    if (this.support.urlObject) {
      const url = this.createObjectURL(blob);
      console.warn('无法自动保存文件,请复制以下链接到新窗口手动保存:', url);
      
      return {
        success: true,
        url,
        using: 'blob-url-only',
        message: '无法自动保存文件,请复制链接到新窗口手动保存'
      };
    }
    
    // 所有方案均失败
    throw new Error('您的浏览器不支持文件保存功能');
  }

  // 检测特定MIME类型的支持情况
  async isMimeTypeSupported(mimeType) {
    // 对于图像类型,使用Image构造函数检测
    if (mimeType.startsWith('image/')) {
      return new Promise(resolve => {
        const img = new Image();
        img.onload = () => resolve(true);
        img.onerror = () => resolve(false);
        
        // 使用数据URL测试支持情况
        img.src = `data:${mimeType};base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==`;
      });
    }
    
    // 对于视频/音频类型,使用MediaSource检测
    if (mimeType.startsWith('video/') || mimeType.startsWith('audio/')) {
      if ('MediaSource' in window) {
        return MediaSource.isTypeSupported(mimeType);
      }
      return false;
    }
    
    // 其他类型无法直接检测,返回true表示乐观支持
    return true;
  }

  // 获取最佳兼容的MIME类型
  async getBestCompatibleMimeType(candidates) {
    if (!Array.isArray(candidates) || candidates.length === 0) {
      throw new Error('候选MIME类型数组不能为空');
    }
    
    // 测试每个候选类型
    for (const mimeType of candidates) {
      if (await this.isMimeTypeSupported(mimeType)) {
        return mimeType;
      }
    }
    
    // 如果没有找到支持的类型,返回第一个候选
    console.warn('没有找到支持的MIME类型,返回第一个候选类型');
    return candidates[0];
  }
}

7.3 安全考量与权限管理

在处理本地文件时,安全性是首要考虑因素。现代浏览器实施了严格的安全策略,我们的应用必须在这些限制下工作:

// 文件操作安全管理器
class FileSecurityManager {
  constructor() {
    this.permissionStates = new Map();
    this.allowedOrigins = new Set();
    this.setupPermissionMonitoring();
  }

  // 初始化权限监控
  setupPermissionMonitoring() {
    // 监控权限变化(如果浏览器支持)
    if ('permissions' in navigator) {
      navigator.permissions.query({ name: 'readily-available' })
        .then(permission => {
          this.permissionStates.set('readily-available', permission.state);
          permission.onchange = () => {
            this.permissionStates.set('readily-available', permission.state);
            this.onPermissionChange('readily-available', permission.state);
          };
        })
        .catch(error => console.warn('无法监控权限状态:', error));
    }
  }

  // 权限变化回调(可被子类重写或通过事件监听)
  onPermissionChange(permissionName, state) {
    console.log(`权限变化: ${permissionName} -> ${state}`);
    // 可以在这里触发自定义事件
  }

  // 验证文件访问权限
  async verifyFileAccess(file) {
    // 对于通过File System Access API获取的文件,检查权限状态
    if (file && file.handle && 'queryPermission' in file.handle) {
      try {
        const permission = await file.handle.queryPermission({ mode: 'read' });
        if (permission !== 'granted') {
          // 尝试请求权限
          const requestResult = await file.handle.requestPermission({ mode: 'read' });
          if (requestResult !== 'granted') {
            throw new Error('没有文件读取权限');
          }
        }
        return true;
      } catch (error) {
        console.error('文件权限验证失败:', error);
        return false;
      }
    }
    
    // 对于传统File对象,无法直接验证权限,但它们的存在本身意味着用户已授予访问权
    if (file instanceof File) {
      return true;
    }
    
    return false;
  }

  // 验证目录访问权限
  async verifyDirectoryAccess(directoryHandle) {
    if (!directoryHandle || !('queryPermission' in directoryHandle)) {
      return false;
    }
    
    try {
      const permission = await directoryHandle.queryPermission({ mode: 'read' });
      if (permission !== 'granted') {
        const requestResult = await directoryHandle.requestPermission({ mode: 'read' });
        if (requestResult !== 'granted') {
          throw new Error('没有目录读取权限');
        }
      }
      return true;
    } catch (error) {
      console.error('目录权限验证失败:', error);
      return false;
    }
  }

  // 验证文件类型安全性
  validateFileType(file, allowedTypes = []) {
    if (!file || !allowedTypes.length) return true;
    
    // 检查MIME类型
    const mimeType = file.type.toLowerCase();
    const extension = file.name.split('.').pop()?.toLowerCase() || '';
    
    // 检查是否在允许列表中
    const isAllowed = allowedTypes.some(type => {
      if (type.startsWith('.')) {
        // 扩展名匹配
        return type.toLowerCase() === `.${extension}`;
      } else if (type.includes('/')) {
        // MIME类型匹配
        const [mainType, subType] = type.split('/');
        if (subType === '*') {
          return mimeType.startsWith(`${mainType}/`);
        }
        return mimeType === type.toLowerCase();
      }
      return false;
    });
    
    if (!isAllowed) {
      console.warn(`不允许的文件类型: ${mimeType} (.${extension})`);
      return false;
    }
    
    // 额外的安全检查:验证文件内容(简单检查)
    if (allowedTypes.some(type => type.startsWith('image/')) && mimeType.startsWith('image/')) {
      return this.validateImageFile(file);
    }
    
    return true;
  }

  // 验证图像文件内容(防止伪装文件)
  async validateImageFile(file) {
    return new Promise(resolve => {
      const img = new Image();
      const url = URL.createObjectURL(file);
      
      img.onload = () => {
        // 基本尺寸检查
        const isValid = img.width > 0 && img.height > 0;
        URL.revokeObjectURL(url);
        resolve(isValid);
      };
      
      img.onerror = () => {
        console.warn('图像文件验证失败:文件可能不是有效的图像');
        URL.revokeObjectURL(url);
        resolve(false);
      };
      
      img.src = url;
    });
  }

  // 验证文件大小
  validateFileSize(file, maxSizeBytes) {
    if (!file || !maxSizeBytes) return true;
    
    if (file.size > maxSizeBytes) {
      console.warn(`文件过大: ${file.name} (${file.size} bytes),超过限制 ${maxSizeBytes} bytes`);
      return false;
    }
    
    return true;
  }

  // 注册可信来源(用于跨域文件访问)
  registerTrustedOrigin(origin) {
    if (typeof origin === 'string') {
      this.allowedOrigins.add(origin);
      return true;
    }
    return false;
  }

  // 验证URL来源是否可信
  isTrustedOrigin(url) {
    try {
      const origin = new URL(url).origin;
      return this.allowedOrigins.has(origin) || origin === window.location.origin;
    } catch (error) {
      console.error('解析URL失败:', error);
      return false;
    }
  }

  // 安全的文件内容处理(防止XSS等攻击)
  sanitizeFileContent(content, mimeType) {
    // 根据MIME类型应用不同的清理策略
    if (mimeType.includes('html') || mimeType.includes('xml')) {
      // 对于HTML/XML内容,使用DOMPurify等库进行清理
      if (typeof DOMPurify !== 'undefined') {
        return DOMPurify.sanitize(content);
      } else {
        console.warn('DOMPurify未加载,无法安全清理HTML/XML内容');
        return content.replace(/<script.*?>.*?<\/script>/gi, ''); // 简单的脚本标签移除
      }
    }
    
    // 对于文本内容,转义HTML特殊字符
    if (mimeType.startsWith('text/')) {
      return content
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
    }
    
    // 二进制内容无需清理
    return content;
  }
}

八、实际应用案例与架构设计

8.1 客户端CSV/Excel数据处理工具

利用File和Blob API构建的客户端CSV/Excel处理工具,无需服务器即可实现数据导入、解析和可视化:

// 客户端CSV/Excel处理工具
class ClientSideDataProcessor {
  constructor(options = {}) {
    this.securityManager = new FileSecurityManager();
    this.memoryManager = new LargeFileMemoryManager();
    this.compat = new FileApiCompat();
    this.csvParser = options.csvParser || this.defaultCsvParser;
    this.maxFileSize = options.maxFileSize || 100 * 1024 * 1024; // 默认100MB
    this.supportedFormats = options.supportedFormats || [
      '.csv', '.tsv', '.txt', 
      'text/csv', 'text/tab-separated-values', 'text/plain'
    ];
  }

  // 默认CSV解析器
  defaultCsvParser(text, delimiter = ',') {
    const lines = text.split(/\r?\n/).filter(line => line.trim() !== '');
    if (lines.length === 0) return { headers: [], rows: [] };
    
    // 检测分隔符(如果未指定)
    if (!delimiter) {
      const commaCount = (text.match(/,/g) || []).length;
      const tabCount = (text.match(/\t/g) || []).length;
      delimiter = commaCount > tabCount ? ',' : '\t';
    }
    
    // 解析表头
    const headers = lines[0].split(delimiter).map(header => header.trim());
    
    // 解析数据行
    const rows = [];
    for (let i = 1; i < lines.length; i++) {
      const line = lines[i];
      if (!line) continue;
      
      const values = line.split(delimiter).map(value => {
        // 尝试转换为适当的类型
        const trimmed = value.trim();
        
        // 数字检测
        if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
          return parseFloat(trimmed);
        }
        
        // 布尔值检测
        if (trimmed.toLowerCase() === 'true') return true;
        if (trimmed.toLowerCase() === 'false') return false;
        
        // 日期检测
        if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
          const date = new Date(trimmed);
          if (!isNaN(date.getTime())) return date;
        }
        
        return trimmed;
      });
      
      // 创建对象
      const row = {};
      headers.forEach((header, index) => {
        row[header] = values[index] !== undefined ? values[index] : '';
      });
      
      rows.push(row);
    }
    
    return { headers, rows, delimiter };
  }

  // 加载并解析数据文件
  async loadAndParseFile(file, options = {}) {
    const {
      delimiter,
      progressCallback,
      maxRows = Infinity,
      skipValidation = false
    } = options;

    // 安全检查
    if (!skipValidation) {
      // 验证文件类型
      if (!this.securityManager.validateFileType(file, this.supportedFormats)) {
        throw new Error(`不支持的文件类型: ${file.name}`);
      }
      
      // 验证文件大小
      if (!this.securityManager.validateFileSize(file, this.maxFileSize)) {
        throw new Error(`文件过大: ${file.name},最大支持 ${this.formatFileSize(this.maxFileSize)}`);
      }
      
      // 验证访问权限
      if (!await this.securityManager.verifyFileAccess(file)) {
        throw new Error('没有文件访问权限');
      }
    }

    // 跟踪文件
    const fileId = `data-file-${Date.now()}`;
    const fileHandle = this.memoryManager.trackObject(fileId, file, {
      type: 'data-file',
      parser: 'csv',
      timestamp: Date.now()
    });

    try {
      progressCallback?.({
        stage: 'loading',
        percent: 10,
        message: `正在读取文件: ${file.name}`
      });

      // 读取文件内容
      const text = await this.compat.blobToText(fileHandle.get());

      progressCallback?.({
        stage: 'parsing',
        percent: 40,
        message: `正在解析文件: ${file.name}`
      });

      // 解析CSV内容
      const result = this.csvParser(text, delimiter);
      
      // 如果设置了最大行数限制,截断结果
      if (maxRows && result.rows.length > maxRows) {
        result.rows = result.rows.slice(0, maxRows);
        result.truncated = true;
        result.totalRows = result.rows.length;
      }

      // 生成统计信息
      result.stats = {
        fileSize: file.size,
        fileType: file.type,
        fileName: file.name,
        parseTime: Date.now() - fileHandle.metadata.timestamp,
        rowCount: result.rows.length,
        columnCount: result.headers.length,
        memoryUsage: this.memoryManager.getMemoryStatus().usage
      };

      progressCallback?.({
        stage: 'complete',
        percent: 100,
        message: `解析完成: ${result.rows.length} 行数据`,
        stats: result.stats
      });

      return {
        ...result,
        fileInfo: {
          name: file.name,
          size: file.size,
          type: file.type,
          lastModified: file.lastModified
        },
        export: (format = 'json') => this.exportData(result, format),
        analyze: () => this.analyzeData(result)
      };
    } finally {
      // 释放文件句柄,但保留数据
      fileHandle.release();
    }
  }

  // 数据分析功能
  analyzeData(parsedResult) {
    const { headers, rows } = parsedResult;
    const analysis = {
      columns: {},
      rowCount: rows.length,
      stats: {
        numericColumns: 0,
        stringColumns: 0,
        dateColumns: 0,
        booleanColumns: 0,
        emptyValues: 0,
        totalValues: rows.length * headers.length
      }
    };

    // 分析每一列
    headers.forEach(header => {
      const columnData = rows.map(row => row[header]);
      const columnAnalysis = {
        name: header,
        type: 'unknown',
        values: {
          unique: new Set(),
          missing: 0,
          total: columnData.length
        },
        stats: {}
      };

      // 检测列类型和统计信息
      columnData.forEach(value => {
        if (value === null || value === undefined || value === '') {
          columnAnalysis.values.missing++;
          analysis.stats.emptyValues++;
          return;
        }

        columnAnalysis.values.unique.add(value);

        // 检测值类型
        if (typeof value === 'number') {
          if (!columnAnalysis.type || columnAnalysis.type === 'number') {
            columnAnalysis.type = 'number';
            
            // 数值统计
            if (isNaN(columnAnalysis.stats.sum)) columnAnalysis.stats.sum = 0;
            if (isNaN(columnAnalysis.stats.min)) columnAnalysis.stats.min = value;
            if (isNaN(columnAnalysis.stats.max)) columnAnalysis.stats.max = value;
            
            columnAnalysis.stats.sum += value;
            columnAnalysis.stats.min = Math.min(columnAnalysis.stats.min, value);
            columnAnalysis.stats.max = Math.max(columnAnalysis.stats.max, value);
          }
        } else if (typeof value === 'boolean') {
          if (!columnAnalysis.type || columnAnalysis.type === 'boolean') {
            columnAnalysis.type = 'boolean';
            
            // 布尔值统计
            if (isNaN(columnAnalysis.stats.trueCount)) columnAnalysis.stats.trueCount = 0;
            if (isNaN(columnAnalysis.stats.falseCount)) columnAnalysis.stats.falseCount = 0;
            
            value ? columnAnalysis.stats.trueCount++ : columnAnalysis.stats.falseCount++;
          }
        } else if (value instanceof Date) {
          if (!columnAnalysis.type || columnAnalysis.type === 'date') {
            columnAnalysis.type = 'date';
            
            // 日期统计
            if (!columnAnalysis.stats.earliest || value < columnAnalysis.stats.earliest) {
              columnAnalysis.stats.earliest = value;
            }
            if (!columnAnalysis.stats.latest || value > columnAnalysis.stats.latest) {
              columnAnalysis.stats.latest = value;
            }
          }
        } else {
          // 默认视为字符串
          if (!columnAnalysis.type) {
            columnAnalysis.type = 'string';
          }
        }
      });

      // 计算数值列的平均值
      if (columnAnalysis.type === 'number' && columnAnalysis.values.total > 0) {
        columnAnalysis.stats.mean = columnAnalysis.stats.sum / columnAnalysis.values.total;
        
        // 计算方差和标准差
        const squaredDiffs = columnData.map(value => {
          if (value === null || value === undefined || value === '') return 0;
          const diff = value - columnAnalysis.stats.mean;
          return diff * diff;
        }).filter(v => !isNaN(v));
        
        columnAnalysis.stats.variance = squaredDiffs.length > 0 
          ? squaredDiffs.reduce((sum, diff) => sum + diff, 0) / squaredDiffs.length 
          : 0;
        columnAnalysis.stats.standardDeviation = Math.sqrt(columnAnalysis.stats.variance);
      }

      // 更新全局统计
      if (columnAnalysis.type === 'number') analysis.stats.numericColumns++;
      else if (columnAnalysis.type === 'string') analysis.stats.stringColumns++;
      else if (columnAnalysis.type === 'date') analysis.stats.dateColumns++;
      else if (columnAnalysis.type === 'boolean') analysis.stats.booleanColumns++;

      // 完成列分析
      columnAnalysis.values.uniqueCount = columnAnalysis.values.unique.size;
      columnAnalysis.values.missingPercentage = (columnAnalysis.values.missing / columnAnalysis.values.total) * 100;
      columnAnalysis.values.uniquePercentage = (columnAnalysis.values.uniqueCount / columnAnalysis.values.total) * 100;

      analysis.columns[header] = columnAnalysis;
    });

    // 计算空值百分比
    analysis.stats.emptyValuePercentage = (analysis.stats.emptyValues / analysis.stats.totalValues) * 100;

    return analysis;
  }

  // 导出数据为不同格式
  async exportData(parsedResult, format = 'json') {
    const { headers, rows } = parsedResult;
    let blob, fileName = `export-${new Date().toISOString().slice(0,10)}`;

    switch (format.toLowerCase()) {
      case 'json':
        // 导出为JSON
        const jsonData = JSON.stringify(rows, null, 2);
        blob = this.compat.createBlob([jsonData], { type: 'application/json' });
        fileName += '.json';
        break;

      case 'csv':
        // 导出为CSV
        const csvRows = [headers.join(',')];
        
        // 添加数据行
        rows.forEach(row => {
          const csvRow = headers.map(header => {
            const value = row[header];
            if (value === null || value === undefined) return '';
            
            // 处理CSV特殊字符
            let strValue = typeof value === 'object' && value instanceof Date 
              ? value.toISOString() 
              : String(value);
            
            if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
              strValue = `"${strValue.replace(/"/g, '""')}"`;
            }
            return strValue;
          });
          csvRows.push(csvRow.join(','));
        });
        
        blob = this.compat.createBlob([csvRows.join('\n')], { type: 'text/csv' });
        fileName += '.csv';
        break;

      case 'tsv':
        // 导出为TSV
        const tsvRows = [headers.join('\t')];
        
        rows.forEach(row => {
          const tsvRow = headers.map(header => {
            const value = row[header];
            if (value === null || value === undefined) return '';
            
            let strValue = typeof value === 'object' && value instanceof Date 
              ? value.toISOString() 
              : String(value);
            
            // 处理TSV特殊字符
            if (strValue.includes('\t') || strValue.includes('\n')) {
              strValue = strValue.replace(/\t/g, '\\t').replace(/\n/g, '\\n');
            }
            return strValue;
          });
          tsvRows.push(tsvRow.join('\t'));
        });
        
        blob = this.compat.createBlob([tsvRows.join('\n')], { type: 'text/tab-separated-values' });
        fileName += '.tsv';
        break;

      case 'html':
        // 导出为HTML表格
        const htmlRows = rows.map(row => `
          <tr>
            ${headers.map(header => `<td>${row[header] !== null && row[header] !== undefined ? row[header] : ''}</td>`).join('')}
          </tr>
        `).join('\n');
        
        const htmlContent = `
          <!DOCTYPE html>
          <html>
          <head>
            <meta charset="UTF-8">
            <title>导出数据表格</title>
            <style>
              table { border-collapse: collapse; width: 100%; }
              th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
              th { background-color: #f2f2f2; }
              tr:nth-child(even) { background-color: #f9f9f9; }
            </style>
          </head>
          <body>
            <h1>导出数据表格</h1>
            <p>导出时间: ${new Date().toLocaleString()}</p>
            <table>
              <thead>
                <tr>${headers.map(header => `<th>${header}</th>`).join('')}</tr>
              </thead>
              <tbody>
                ${htmlRows}
              </tbody>
            </table>
          </body>
          </html>
        `;
        
        blob = this.compat.createBlob([htmlContent], { type: 'text/html' });
        fileName += '.html';
        break;

      default:
        throw new Error(`不支持的导出格式: ${format}`);
    }

    // 保存文件
    return this.compat.saveBlobToFile(blob, fileName);
  }

  // 格式化文件大小显示
  formatFileSize(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }
}

8.2 客户端图像编辑与处理应用

结合File/Blob API与Canvas实现的客户端图像编辑工具,支持基本编辑功能而无需服务器:

// 客户端图像编辑器
class ClientImageEditor {
  constructor(canvasId, options = {}) {
    this.canvas = document.getElementById(canvasId);
    if (!this.canvas) {
      throw new Error(`找不到ID为 ${canvasId} 的Canvas元素`);
    }
    
    this.ctx = this.canvas.getContext('2d');
    if (!this.ctx) {
      throw new Error('无法获取Canvas 2D上下文');
    }
    
    // 初始化管理器
    this.memoryManager = new LargeFileMemoryManager();
    this.compat = new FileApiCompat();
    this.securityManager = new FileSecurityManager();
    
    // 配置
    this.options = {
      maxImageSize: 8000, // 最大图像尺寸(像素)
      defaultQuality: 0.92, // 默认图像质量
      supportedFormats: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
      ...options
    };
    
    // 状态管理
    this.state = {
      originalImage: null,
      currentImage: null,
      history: [],
      historyIndex: -1,
      transformations: [],
      imageInfo: {}
    };
    
    // 初始化画布大小
    this.resizeCanvas(this.options.initialWidth || 800, this.options.initialHeight || 600);
  }
  
  // 调整画布大小
  resizeCanvas(width, height) {
    // 保存当前内容
    const tempCanvas = document.createElement('canvas');
    const tempCtx = tempCanvas.getContext('2d');
    tempCanvas.width = this.canvas.width;
    tempCanvas.height = this.canvas.height;
    tempCtx.drawImage(this.canvas, 0, 0);
    
    // 调整大小
    this.canvas.width = width;
    this.canvas.height = height;
    
    // 恢复内容(如果需要)
    if (this.state.currentImage && this.options.preserveOnResize) {
      this.ctx.drawImage(tempCanvas, 0, 0, width, height);
    }
    
    return this;
  }
  
  // 加载图像文件
  async loadImageFile(file, progressCallback) {
    // 安全验证
    if (!this.securityManager.validateFileType(file, this.options.supportedFormats)) {
      throw new Error(`不支持的图像格式: ${file.type}`);
    }
    
    if (!this.securityManager.validateFileSize(file, this.options.maxFileSize)) {
      throw new Error(`文件过大,最大支持 ${this.formatFileSize(this.options.maxFileSize)}`);
    }
    
    progressCallback?.({ stage: 'loading', percent: 20, message: '正在加载图像' });
    
    // 跟踪文件
    const fileId = `image-file-${Date.now()}`;
    const fileHandle = this.memoryManager.trackObject(fileId, file, {
      type: 'image-file',
      timestamp: Date.now()
    });
    
    try {
      // 创建图像对象
      const img = new Image();
      const url = this.compat.createObjectURL(fileHandle.get());
      
      // 等待图像加载
      await new Promise((resolve, reject) => {
        img.onload = resolve;
        img.onerror = reject;
        img.src = url;
      });
      
      progressCallback?.({ stage: 'processing', percent: 50, message: '正在处理图像' });
      
      // 检查图像尺寸
      let { width, height } = img;
      
      // 如果图像过大,调整大小
      const maxSize = this.options.maxImageSize;
      if (width > maxSize || height > maxSize) {
        const scale = Math.min(maxSize / width, maxSize / height);
        width *= scale;
        height *= scale;
      }
      
      // 调整画布大小并绘制图像
      this.resizeCanvas(width, height);
      this.ctx.clearRect(0, 0, width, height);
      this.ctx.drawImage(img, 0, 0, width, height);
      
      // 保存原始图像数据
      this.state.originalImage = img;
      this.state.currentImage = this.canvas.toDataURL(file.type, this.options.defaultQuality);
      this.state.imageInfo = {
        originalWidth: img.naturalWidth,
        originalHeight: img.naturalHeight,
        displayWidth: width,
        displayHeight: height,
        format: file.type,
        size: file.size,
        name: file.name,
        lastModified: file.lastModified
      };
      
      // 重置历史记录
      this.resetHistory();
      this.addToHistory();
      
      progressCallback?.({ 
        stage: 'complete', 
        percent: 100, 
        message: '图像加载完成',
        imageInfo: this.state.imageInfo
      });
      
      return this.state.imageInfo;
    } finally {
      // 清理URL对象,但保留文件引用
      this.compat.revokeObjectURL(img.src);
      fileHandle.release();
    }
  }
  
  // 应用滤镜效果
  applyFilter(filterName, options = {}) {
    if (!this.state.originalImage) {
      throw new Error('没有加载图像');
    }
    
    // 保存当前状态
    this.addToHistory();
    
    // 获取滤镜函数
    const filterFunction = this.getFilterFunction(filterName);
    if (!filterFunction) {
      throw new Error(`不支持的滤镜: ${filterName}`);
    }
    
    // 获取图像数据
    const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
    
    // 应用滤镜
    filterFunction(imageData.data, options);
    
    // 将处理后的数据放回画布
    this.ctx.putImageData(imageData, 0, 0);
    
    // 更新当前图像状态
    this.state.currentImage = this.canvas.toDataURL(this.state.imageInfo.format, this.options.defaultQuality);
    this.state.transformations.push({
      type: 'filter',
      name: filterName,
      options
    });
    
    return this;
  }
  
  // 获取滤镜函数
  getFilterFunction(filterName) {
    const filters = {
      // 灰度滤镜
      grayscale: (data) => {
        for (let i = 0; i < data.length; i += 4) {
          const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
          data[i] = avg;     // R
          data[i + 1] = avg; // G
          data[i + 2] = avg; // B
        }
      },
      
      // 亮度调整
      brightness: (data, { level = 0 }) => {
        const adjustment = level * 255 / 100;
        for (let i = 0; i < data.length; i += 4) {
          data[i] = Math.max(0, Math.min(255, data[i] + adjustment));     // R
          data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + adjustment)); // G
          data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + adjustment)); // B
        }
      },
      
      // 对比度调整
      contrast: (data, { level = 0 }) => {
        const factor = (259 * (level + 255)) / (255 * (259 - level));
        for (let i = 0; i < data.length; i += 4) {
          data[i] = Math.max(0, Math.min(255, factor * (data[i] - 128) + 128));     // R
          data[i + 1] = Math.max(0, Math.min(255, factor * (data[i + 1] - 128) + 128)); // G
          data[i + 2] = Math.max(0, Math.min(255, factor * (data[i + 2] - 128) + 128)); // B
        }
      },
      
      // 饱和度调整
      saturation: (data, { level = 0 }) => {
        const saturationFactor = level / 100;
        for (let i = 0; i < data.length; i += 4) {
          const r = data[i];
          const g = data[i + 1];
          const b = data[i + 2];
          
          // 计算灰度值
          const gray = 0.299 * r + 0.587 * g + 0.114 * b;
          
          // 应用饱和度调整
          data[i] = Math.max(0, Math.min(255, gray + saturationFactor * (r - gray)));
          data[i + 1] = Math.max(0, Math.min(255, gray + saturationFactor * (g - gray)));
          data[i + 2] = Math.max(0, Math.min(255, gray + saturationFactor * (b - gray)));
        }
      },
      
      // 反转颜色
      invert: (data) => {
        for (let i = 0; i < data.length; i += 4) {
          data[i] = 255 - data[i];     // R
          data[i + 1] = 255 - data[i + 1]; // G
          data[i + 2] = 255 - data[i + 2]; // B
        }
      },
      
      //  sepia滤镜
      sepia: (data) => {
        for (let i = 0; i < data.length; i += 4) {
          const r = data[i];
          const g = data[i + 1];
          const b = data[i + 2];
          
          data[i] = Math.min(255, 0.393 * r + 0.769 * g + 0.189 * b);
          data[i + 1] = Math.min(255, 0.349 * r + 0.686 * g + 0.168 * b);
          data[i + 2] = Math.min(255, 0.272 * r + 0.534 * g + 0.131 * b);
        }
      },
      
      // 锐化滤镜
      sharpen: (data, { strength = 1 }) => {
        // 简单锐化实现(3x3卷积核)
        const width = this.canvas.width;
        const height = this.canvas.height;
        const pixels = new Uint8ClampedArray(data);
        
        const kernel = [
          0, -strength, 0,
          -strength, 4*strength + 1, -strength,
          0, -strength, 0
        ];
        
        // 创建临时数组存储结果
        const result = new Uint8ClampedArray(data.length);
        
        // 应用卷积
        for (let y = 1; y < height - 1; y++) {
          for (let x = 1; x < width - 1; x++) {
            for (let c = 0; c < 3; c++) { // 对RGB通道应用滤镜
              const index = (y * width + x) * 4 + c;
              let sum = 0;
              
              // 应用卷积核
              for (let ky = -1; ky <= 1; ky++) {
                for (let kx = -1; kx <= 1; kx++) {
                  const kernelIndex = (ky + 1) * 3 + (kx + 1);
                  const neighborIndex = ((y + ky) * width + (x + kx)) * 4 + c;
                  sum += pixels[neighborIndex] * kernel[kernelIndex];
                }
              }
              
              // 确保值在有效范围内
              result[index] = Math.max(0, Math.min(255, sum));
            }
            // 保留alpha通道
            result[(y * width + x) * 4 + 3] = pixels[(y * width + x) * 4 + 3];
          }
        }
        
        // 将结果复制回原始数据
        for (let i = 0; i < data.length; i++) {
          data[i] = result[i];
        }
      }
    };
    
    return filters[filterName];
  }
  
  // 添加到历史记录
  addToHistory() {
    // 如果在历史记录中间添加新状态,删除后面的历史
    if (this.state.historyIndex < this.state.history.length - 1) {
      this.state.history = this.state.history.slice(0, this.state.historyIndex + 1);
    }
    
    // 保存当前画布状态
    const stateUrl = this.canvas.toDataURL(this.state.imageInfo.format);
    this.state.history.push(stateUrl);
    this.state.historyIndex = this.state.history.length - 1;
    
    // 限制历史记录数量
    if (this.state.history.length > this.options.maxHistoryStates || 20) {
      this.state.history.shift();
      this.state.historyIndex--;
    }
    
    return this;
  }
  
  // 重置历史记录
  resetHistory() {
    this.state.history = [];
    this.state.historyIndex = -1;
    this.state.transformations = [];
    return this;
  }
  
  // 撤销操作
  undo() {
    if (this.state.historyIndex <= 0) return this;
    
    this.state.historyIndex--;
    this.restoreHistoryState(this.state.historyIndex);
    
    return this;
  }
  
  // 重做操作
  redo() {
    if (this.state.historyIndex >= this.state.history.length - 1) return this;
    
    this.state.historyIndex++;
    this.restoreHistoryState(this.state.historyIndex);
    
    return this;
  }
  
  // 恢复历史状态
  restoreHistoryState(index) {
    if (index < 0 || index >= this.state.history.length) return;
    
    const img = new Image();
    img.onload = () => {
      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
      this.ctx.drawImage(img, 0, 0);
      this.state.currentImage = this.state.history[index];
    };
    img.src = this.state.history[index];
    
    return this;
  }
  
  // 旋转图像
  rotate(degrees) {
    this.addToHistory();
    
    // 创建临时画布
    const tempCanvas = document.createElement('canvas');
    const tempCtx = tempCanvas.getContext('2d');
    
    // 计算旋转后的尺寸
    const radians = degrees * Math.PI / 180;
    const rotatedWidth = Math.abs(this.canvas.width * Math.cos(radians)) + Math.abs(this.canvas.height * Math.sin(radians));
    const rotatedHeight = Math.abs(this.canvas.width * Math.sin(radians)) + Math.abs(this.canvas.height * Math.cos(radians));
    
    // 设置临时画布尺寸
    tempCanvas.width = rotatedWidth;
    tempCanvas.height = rotatedHeight;
    
    // 旋转并绘制图像
    tempCtx.translate(rotatedWidth / 2, rotatedHeight / 2);
    tempCtx.rotate(radians);
    tempCtx.drawImage(this.canvas, -this.canvas.width / 2, -this.canvas.height / 2);
    
    // 更新主画布
    this.resizeCanvas(rotatedWidth, rotatedHeight);
    this.ctx.clearRect(0, 0, rotatedWidth, rotatedHeight);
    this.ctx.drawImage(tempCanvas, 0, 0);
    
    // 更新状态
    this.state.currentImage = this.canvas.toDataURL(this.state.imageInfo.format);
    this.state.transformations.push({
      type: 'rotate',
      degrees
    });
    
    return this;
  }
  
  // 裁剪图像
  crop(x, y, width, height) {
    this.addToHistory();
    
    // 创建临时画布
    const tempCanvas = document.createElement('canvas');
    const tempCtx = tempCanvas.getContext('2d');
    
    // 设置临时画布尺寸
    tempCanvas.width = width;
    tempCanvas.height = height;
    
    // 绘制裁剪区域
    tempCtx.drawImage(
      this.canvas, 
      x, y, width, height,  // 源区域
      0, 0, width, height   // 目标区域
    );
    
    // 更新主画布
    this.resizeCanvas(width, height);
    this.ctx.clearRect(0, 0, width, height);
    this.ctx.drawImage(tempCanvas, 0, 0);
    
    // 更新状态
    this.state.currentImage = this.canvas.toDataURL(this.state.imageInfo.format);
    this.state.transformations.push({
      type: 'crop',
      x, y, width, height
    });
    
    return this;
  }
  
  // 调整图像大小
  resize(newWidth, newHeight) {
    this.addToHistory();
    
    // 创建临时画布
    const tempCanvas = document.createElement('canvas');
    const tempCtx = tempCanvas.getContext('2d');
    
    // 设置临时画布尺寸
    tempCanvas.width = newWidth;
    tempCanvas.height = newHeight;
    
    // 绘制调整后的图像
    tempCtx.drawImage(this.canvas, 0, 0, newWidth, newHeight);
    
    // 更新主画布
    this.resizeCanvas(newWidth, newHeight);
    this.ctx.clearRect(0, 0, newWidth, newHeight);
    this.ctx.drawImage(tempCanvas, 0, 0);
    
    // 更新状态
    this.state.currentImage = this.canvas.toDataURL(this.state.imageInfo.format);
    this.state.transformations.push({
      type: 'resize',
      width: newWidth,
      height: newHeight
    });
    
    return this;
  }
  
  // 保存图像到文件
  async saveImage(options = {}) {
    if (!this.state.currentImage) {
      throw new Error('没有可保存的图像');
    }
    
    const {
      format,
      quality = this.options.defaultQuality,
      fileName = `edited-image-${new Date().toISOString().slice(0,10)}`,
      progressCallback
    } = options;
    
    progressCallback?.({ stage: 'processing', percent: 30, message: '正在处理图像' });
    
    // 确定图像格式
    const imageFormat = format || this.state.imageInfo.format || 'image/jpeg';
    const extension = imageFormat.split('/')[1] || 'jpg';
    
    // 从画布获取图像数据
    const dataUrl = this.canvas.toDataURL(imageFormat, quality);
    const byteString = atob(dataUrl.split(',')[1]);
    const ab = new ArrayBuffer(byteString.length);
    const ia = new Uint8Array(ab);
    
    for (let i = 0; i < byteString.length; i++) {
      ia[i] = byteString.charCodeAt(i);
    }
    
    // 创建Blob
    const blob = new Blob([ab], { type: imageFormat });
    
    progressCallback?.({ stage: 'saving', percent: 70, message: '正在准备保存' });
    
    // 保存到文件
    const result = await this.compat.saveBlobToFile(blob, `${fileName}.${extension}`);
    
    progressCallback?.({ stage: 'complete', percent: 100, message: '图像保存完成' });
    
    return {
      ...result,
      blob,
      format: imageFormat,
      size: blob.size,
      quality
    };
  }
  
  // 获取当前图像Blob
  async getImageBlob(format, quality = this.options.defaultQuality) {
    const dataUrl = this.canvas.toDataURL(format || this.state.imageInfo.format, quality);
    const response = await fetch(dataUrl);
    return response.blob();
  }
  
  // 格式化文件大小
  formatFileSize(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }
  
  // 销毁编辑器实例
  destroy() {
    this.resetHistory();
    this.memoryManager.destroy();
    this.state = {
      originalImage: null,
      currentImage: null,
      history: [],
      historyIndex: -1,
      transformations: []
    };
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
  }
}

掌握File与Blob黑科技:从浏览器操控本地文件夹到现代应用实战指南(上篇)

掌握File与Blob黑科技:从浏览器操控本地文件夹到现代应用实战指南(中篇)

掌握File与Blob黑科技:从浏览器操控本地文件夹到现代应用实战指南(下篇)