实现浏览器插件:图片抓取与下载工具

引言:浏览器插件的无限可能

在当今信息爆炸的时代,网络上的图片资源已成为数字内容的重要组成部分。无论是设计师寻找灵感、研究人员收集数据,还是普通用户保存喜爱的图像,高效地抓取和下载网络图片都是一项极具价值的技术。浏览器插件作为连接用户与网页内容的桥梁,正成为实现这一功能的理想平台。

本文将深入探讨如何开发一个功能全面的浏览器图片抓取与下载工具,从基础原理到高级功能实现,涵盖技术细节、性能优化和用户体验设计。通过万字详解,您将掌握构建此类插件的完整知识体系,并能够在此基础上进行自定义扩展。

一、浏览器插件基础架构

1.1 插件核心组件解析

现代浏览器插件通常遵循MV2(Manifest V2)或MV3(Manifest V3)规范,由多个相互协作的组件构成:

// manifest.json (Manifest V3)
{
  "manifest_version": 3,
  "name": "高级图片抓取工具",
  "version": "1.0.0",
  "description": "强大的网页图片检测与批量下载解决方案",
  "permissions": [
    "activeTab",
    "scripting",
    "downloads",
    "storage"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_title": "图片抓取工具"
  },
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "css": ["content.css"]
    }
  ],
  "web_accessible_resources": [{
    "resources": ["images/*.png", "styles/*.css"],
    "matches": ["<all_urls>"]
  }],
  "icons": {
    "16": "images/icon16.png",
    "48": "images/icon48.png",
    "128": "images/icon128.png"
  }
}

1.2 组件通信机制

浏览器插件各组件间通过消息传递实现通信,下图展示了核心架构:

发送消息
发送消息
存储管理
下载管理
DOM操作
用户交互
内容脚本
content.js
后台脚本
background.js
弹出页面
popup.html
存储API
下载API
网页DOM
用户界面

内容脚本与后台脚本间的消息传递示例:

// content.js - 发送消息到后台
chrome.runtime.sendMessage({
  type: "imageListUpdate",
  data: {
    url: window.location.href,
    images: extractedImages
  }
}, response => {
  console.log("收到响应:", response);
});

// background.js - 监听消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === "imageListUpdate") {
    console.log("收到图片列表更新:", message.data);
    // 处理图片数据
    processImages(message.data.images);
    sendResponse({status: "success"});
  }
  return true; // 保持消息通道开放,用于异步响应
});

二、图片检测与提取技术

2.1 DOM遍历与图片识别算法

高效检测网页中的所有图片资源需要多种策略结合,以下是核心实现:

// content.js - 高级图片检测功能
class AdvancedImageDetector {
  constructor() {
    this.images = new Map();
    this.observers = [];
    this.lazyLoadingThreshold = 0.8; // 视窗阈值用于延迟加载检测
  }

  // 主检测方法
  detectAllImages() {
    this.detectStandardImages();
    this.detectBackgroundImages();
    this.detectLazyLoadedImages();
    this.detectCanvasElements();
    this.detectSVGContents();
    this.setupMutationObserver();
    this.setupIntersectionObserver();
  }

  // 检测标准img标签
  detectStandardImages() {
    const imgElements = document.querySelectorAll('img');
    imgElements.forEach(img => {
      this.processImageElement(img, 'standard');
    });
  }

  // 检测CSS背景图片
  detectBackgroundImages() {
    const allElements = document.querySelectorAll('*');
    allElements.forEach(element => {
      const backgroundImage = this.getBackgroundImage(element);
      if (backgroundImage) {
        this.processBackgroundImage(element, backgroundImage);
      }
    });
  }

  // 获取元素的背景图片
  getBackgroundImage(element) {
    const style = window.getComputedStyle(element);
    const backgroundImage = style.backgroundImage || 
                           style.background || 
                           style.getPropertyValue('background-image');
    
    if (backgroundImage && backgroundImage !== 'none') {
      // 提取URL内容
      const urlMatch = backgroundImage.match(/url\(["']?(.*?)["']?\)/);
      if (urlMatch && urlMatch[1]) {
        return {
          url: urlMatch[1],
          element: element,
          size: this.getElementSize(element)
        };
      }
    }
    return null;
  }

  // 检测延迟加载图片
  detectLazyLoadedImages() {
    const lazyElements = document.querySelectorAll('[data-src], [data-srcset], [loading="lazy"]');
    lazyElements.forEach(element => {
      const src = element.getAttribute('data-src') || element.src;
      const srcset = element.getAttribute('data-srcset') || element.srcset;
      
      if (src || srcset) {
        this.processImageElement({
          src: src,
          srcset: srcset,
          tagName: element.tagName,
          naturalWidth: element.naturalWidth,
          naturalHeight: element.naturalHeight,
          offsetWidth: element.offsetWidth,
          offsetHeight: element.offsetHeight
        }, 'lazy');
      }
    });
  }

  // 处理图片元素
  processImageElement(img, type) {
    try {
      // 跳过空白或无效URL
      if (!img.src || img.src.trim() === '') return;
      
      // 解析URL,处理相对路径
      const fullUrl = this.resolveUrl(img.src);
      
      // 获取图片尺寸信息
      const dimensions = this.getImageDimensions(img);
      
      // 创建图片信息对象
      const imageInfo = {
        url: fullUrl,
        type: type,
        element: img,
        dimensions: dimensions,
        naturalSize: {
          width: img.naturalWidth,
          height: img.naturalHeight
        },
        displayedSize: {
          width: img.offsetWidth,
          height: img.offsetHeight
        },
        srcset: img.srcset || '',
        alt: img.alt || '',
        title: img.title || '',
        // 添加更多元数据
        timestamp: Date.now(),
        pageUrl: window.location.href
      };
      
      // 添加到图片集合
      if (!this.images.has(fullUrl)) {
        this.images.set(fullUrl, imageInfo);
        this.notifyImageAdded(imageInfo);
      }
    } catch (error) {
      console.warn('处理图片元素时出错:', error, img);
    }
  }

  // 解析URL为绝对路径
  resolveUrl(url) {
    try {
      // 处理data URL
      if (url.startsWith('data:')) return url;
      
      // 使用URL构造函数解析相对路径
      return new URL(url, window.location.href).href;
    } catch (e) {
      console.warn('解析URL失败:', url, e);
      return url;
    }
  }

  // 获取图片尺寸
  getImageDimensions(img) {
    // 实现多种尺寸获取策略
    if (img.naturalWidth && img.naturalHeight) {
      return { width: img.naturalWidth, height: img.naturalHeight };
    }
    
    if (img.offsetWidth && img.offsetHeight) {
      return { width: img.offsetWidth, height: img.offsetHeight };
    }
    
    // 对于未加载的图片,尝试预加载获取尺寸
    return this.getDimensionsViaPreload(img.src);
  }

  // 通过预加载获取图片尺寸
  async getDimensionsViaPreload(url) {
    return new Promise((resolve) => {
      if (url.startsWith('data:')) {
        // 处理data URL
        const match = url.match(/data:image\/(\w+);base64,/);
        if (match) {
          // 可以解析常见格式的data URL获取尺寸
          resolve({ width: 0, height: 0 });
          return;
        }
      }
      
      const img = new Image();
      img.onload = () => {
        resolve({ width: img.width, height: img.height });
      };
      img.onerror = () => {
        resolve({ width: 0, height: 0 });
      };
      img.src = url;
      
      // 设置超时
      setTimeout(() => {
        resolve({ width: 0, height: 0 });
      }, 2000);
    });
  }

  // 设置Mutation Observer监听DOM变化
  setupMutationObserver() {
    const observer = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        if (mutation.type === 'childList') {
          mutation.addedNodes.forEach(node => {
            if (node.nodeType === 1) { // Element node
              if (node.tagName === 'IMG') {
                this.processImageElement(node, 'dynamic');
              } else {
                const images = node.querySelectorAll('img');
                images.forEach(img => this.processImageElement(img, 'dynamic'));
                
                // 检查新元素的背景图片
                this.checkElementForBackgroundImages(node);
              }
            }
          });
        } else if (mutation.type === 'attributes') {
          // 处理属性变化,如src、srcset更新
          if (mutation.target.tagName === 'IMG' && 
              (mutation.attributeName === 'src' || 
               mutation.attributeName === 'srcset' ||
               mutation.attributeName === 'data-src')) {
            this.processImageElement(mutation.target, 'dynamic-update');
          }
        }
      });
    });
    
    observer.observe(document.documentElement, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['src', 'srcset', 'data-src', 'style']
    });
    
    this.observers.push(observer);
  }

  // 设置Intersection Observer监听图片进入视图
  setupIntersectionObserver() {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          // 图片进入视口,可能触发延迟加载
          if (entry.target.tagName === 'IMG') {
            this.processImageElement(entry.target, 'lazy-loaded');
          }
        }
      });
    }, { threshold: this.lazyLoadingThreshold });
    
    // 观察所有图片元素
    document.querySelectorAll('img').forEach(img => {
      observer.observe(img);
    });
    
    this.observers.push(observer);
  }

  // 通知系统有新图片添加
  notifyImageAdded(imageInfo) {
    chrome.runtime.sendMessage({
      type: 'imageDiscovered',
      data: imageInfo
    });
  }

  // 获取所有检测到的图片
  getAllImages() {
    return Array.from(this.images.values());
  }

  // 清理观察器
  destroy() {
    this.observers.forEach(observer => {
      try {
        observer.disconnect();
      } catch (e) {
        console.warn('清理观察器时出错:', e);
      }
    });
    this.observers = [];
  }
}

// 初始化检测器
const imageDetector = new AdvancedImageDetector();
document.addEventListener('DOMContentLoaded', () => {
  // 延迟执行以确保页面充分加载
  setTimeout(() => {
    imageDetector.detectAllImages();
  }, 1000);
});

// 处理页面完全加载后的资源
window.addEventListener('load', () => {
  imageDetector.detectAllImages();
});

2.2 高级图片识别技术

对于现代网页中复杂的图片展示方式,需要更高级的识别技术:

// advanced-detection.js - 高级图片识别功能
class AdvancedImageRecognition {
  // 检测Canvas中的图片内容
  detectCanvasImages() {
    const canvases = document.querySelectorAll('canvas');
    canvases.forEach(canvas => {
      // 检查canvas是否包含图片内容
      if (this.isCanvasImage(canvas)) {
        this.extractImageFromCanvas(canvas);
      }
    });
  }

  // 判断Canvas是否包含图片内容
  isCanvasImage(canvas) {
    // 实现检测逻辑
    try {
      // 方法1: 检查canvas数据
      const context = canvas.getContext('2d');
      const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
      
      // 简单的非空检查
      const isEmpty = imageData.data.every(value => value === 0);
      return !isEmpty;
    } catch (e) {
      return false;
    }
  }

  // 从Canvas提取图片
  extractImageFromCanvas(canvas) {
    try {
      // 将canvas转换为Data URL
      const dataUrl = canvas.toDataURL('image/png');
      if (dataUrl && !dataUrl.startsWith('data:,')) {
        // 创建图片信息
        const imageInfo = {
          url: dataUrl,
          type: 'canvas',
          dimensions: {
            width: canvas.width,
            height: canvas.height
          },
          element: canvas,
          timestamp: Date.now()
        };
        
        // 发送到后台处理
        chrome.runtime.sendMessage({
          type: 'canvasImageExtracted',
          data: imageInfo
        });
      }
    } catch (e) {
      console.warn('从canvas提取图片失败:', e);
    }
  }

  // 检测SVG中的图片内容
  detectSVGImages() {
    const svgs = document.querySelectorAll('svg');
    svgs.forEach(svg => {
      this.extractImagesFromSVG(svg);
    });
  }

  // 从SVG提取图片
  extractImagesFromSVG(svg) {
    // 查找SVG中的image元素
    const svgImages = svg.querySelectorAll('image');
    svgImages.forEach(svgImage => {
      const href = svgImage.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || 
                   svgImage.getAttribute('href');
      if (href) {
        const imageInfo = {
          url: this.resolveUrl(href),
          type: 'svg-image',
          dimensions: {
            width: parseFloat(svgImage.getAttribute('width') || 0),
            height: parseFloat(svgImage.getAttribute('height') || 0)
          },
          element: svgImage,
          timestamp: Date.now()
        };
        
        chrome.runtime.sendMessage({
          type: 'svgImageDiscovered',
          data: imageInfo
        });
      }
    });
  }

  // 检测图片库和轮播组件
  detectGalleryComponents() {
    // 常见图片库选择器
    const gallerySelectors = [
      '.gallery',
      '.carousel',
      '.slider',
      '[data-gallery]',
      '[data-slider]',
      '.slick-slider',
      '.swiper-container'
    ];
    
    gallerySelectors.forEach(selector => {
      const galleries = document.querySelectorAll(selector);
      galleries.forEach(gallery => {
        this.processGalleryComponent(gallery);
      });
    });
  }

  // 处理图片库组件
  processGalleryComponent(gallery) {
    // 在库内查找图片
    const images = gallery.querySelectorAll('img');
    images.forEach(img => {
      // 标记为库图片以便特殊处理
      img.setAttribute('data-in-gallery', 'true');
    });
    
    // 发送库信息到后台
    chrome.runtime.sendMessage({
      type: 'galleryDetected',
      data: {
        element: gallery.tagName,
        imageCount: images.length,
        url: window.location.href
      }
    });
  }
}

三、图片过滤与排序系统

3.1 多维度过滤算法

实现强大的图片过滤功能,帮助用户快速找到所需图片:

// filtering.js - 高级图片过滤系统
class ImageFilterSystem {
  constructor() {
    this.filters = {
      minWidth: 0,
      minHeight: 0,
      maxWidth: Infinity,
      maxHeight: Infinity,
      minAspectRatio: 0,
      maxAspectRatio: Infinity,
      types: ['all'], // all, standard, background, canvas, etc.
      domains: [],
      minSize: 0, // 最小文件大小(估算)
      maxSize: Infinity // 最大文件大小(估算)
    };
    
    this.sortBy = 'size-desc'; // 默认排序方式
  }

  // 应用过滤条件
  applyFilters(images) {
    return images.filter(image => {
      // 尺寸过滤
      if (image.dimensions.width < this.filters.minWidth) return false;
      if (image.dimensions.height < this.filters.minHeight) return false;
      if (image.dimensions.width > this.filters.maxWidth) return false;
      if (image.dimensions.height > this.filters.maxHeight) return false;
      
      // 宽高比过滤
      const aspectRatio = image.dimensions.width / image.dimensions.height;
      if (aspectRatio < this.filters.minAspectRatio) return false;
      if (aspectRatio > this.filters.maxAspectRatio) return false;
      
      // 类型过滤
      if (this.filters.types[0] !== 'all' && !this.filters.types.includes(image.type)) {
        return false;
      }
      
      // 域名过滤
      if (this.filters.domains.length > 0) {
        try {
          const url = new URL(image.url);
          if (!this.filters.domains.includes(url.hostname)) {
            return false;
          }
        } catch (e) {
          // URL解析失败,跳过域名检查
        }
      }
      
      // 文件大小估算过滤
      const estimatedSize = this.estimateImageSize(image);
      if (estimatedSize < this.filters.minSize) return false;
      if (estimatedSize > this.filters.maxSize) return false;
      
      return true;
    });
  }

  // 估算图片大小
  estimateImageSize(image) {
    // 简单估算:宽 × 高 × 4(RGBA) × 压缩系数
    if (image.dimensions.width && image.dimensions.height) {
      const rawSize = image.dimensions.width * image.dimensions.height * 4;
      
      // 根据格式应用压缩比率估算
      let compressionRatio = 0.5; // 默认JPEG压缩比
      
      if (image.url.includes('.png')) compressionRatio = 0.9;
      if (image.url.includes('.webp')) compressionRatio = 0.7;
      if (image.url.includes('.gif')) compressionRatio = 0.8;
      if (image.url.startsWith('data:')) {
        // 对data URL进行更精确的估算
        return this.estimateDataUrlSize(image.url);
      }
      
      return Math.round(rawSize * compressionRatio);
    }
    
    return 0;
  }

  // 估算Data URL大小
  estimateDataUrlSize(dataUrl) {
    // data:image/png;base64, 前缀长度约22字符
    if (dataUrl.length < 22) return 0;
    
    // Base64编码数据大小约为原始数据的4/3
    const base64Data = dataUrl.substring(22);
    const base64Length = base64Data.length;
    
    // 减去可能的填充字符
    const padding = base64Data.endsWith('==') ? 2 : base64Data.endsWith('=') ? 1 : 0;
    
    // 计算原始数据大小
    return Math.round((base64Length * 3) / 4 - padding);
  }

  // 排序图片
  sortImages(images) {
    return images.sort((a, b) => {
      switch (this.sortBy) {
        case 'size-asc':
          return this.estimateImageSize(a) - this.estimateImageSize(b);
        case 'size-desc':
          return this.estimateImageSize(b) - this.estimateImageSize(a);
        case 'width-asc':
          return a.dimensions.width - b.dimensions.width;
        case 'width-desc':
          return b.dimensions.width - a.dimensions.width;
        case 'height-asc':
          return a.dimensions.height - b.dimensions.height;
        case 'height-desc':
          return b.dimensions.height - a.dimensions.height;
        case 'area-asc':
          return (a.dimensions.width * a.dimensions.height) - (b.dimensions.width * b.dimensions.height);
        case 'area-desc':
          return (b.dimensions.width * b.dimensions.height) - (a.dimensions.width * a.dimensions.height);
        default:
          return 0;
      }
    });
  }

  // 设置过滤条件
  setFilter(key, value) {
    this.filters[key] = value;
  }

  // 设置排序方式
  setSort(sortBy) {
    this.sortBy = sortBy;
  }

  // 获取过滤后的图片
  getFilteredImages(images) {
    const filtered = this.applyFilters(images);
    return this.sortImages(filtered);
  }
}

3.2 图片去重算法

避免重复下载相同或高度相似的图片:

// deduplication.js - 图片去重系统
class ImageDeduplication {
  constructor() {
    this.hashCache = new Map(); // URL到哈希值的映射
  }

  // 基于内容的简单哈希生成
  async generateImageHash(imageUrl) {
    // 如果已有缓存,直接返回
    if (this.hashCache.has(imageUrl)) {
      return this.hashCache.get(imageUrl);
    }

    try {
      // 获取图片数据
      const response = await fetch(imageUrl, { mode: 'cors' });
      const blob = await response.blob();
      
      // 创建图片对象
      return new Promise((resolve) => {
        const img = new Image();
        img.onload = () => {
          // 创建canvas计算哈希
          const canvas = document.createElement('canvas');
          const ctx = canvas.getContext('2d');
          
          // 缩放到小尺寸以计算感知哈希
          const width = 8;
          const height = 8;
          canvas.width = width;
          canvas.height = height;
          
          ctx.drawImage(img, 0, 0, width, height);
          const imageData = ctx.getImageData(0, 0, width, height);
          
          // 计算平均亮度
          let total = 0;
          for (let i = 0; i < imageData.data.length; i += 4) {
            total += (imageData.data[i] + imageData.data[i+1] + imageData.data[i+2]) / 3;
          }
          const average = total / (width * height);
          
          // 生成哈希(每个像素与平均值比较)
          let hash = '';
          for (let i = 0; i < imageData.data.length; i += 4) {
            const brightness = (imageData.data[i] + imageData.data[i+1] + imageData.data[i+2]) / 3;
            hash += brightness > average ? '1' : '0';
          }
          
          // 缓存结果
          this.hashCache.set(imageUrl, hash);
          resolve(hash);
        };
        
        img.onerror = () => {
          // 使用URL作为后备哈希
          const fallbackHash = this.generateFallbackHash(imageUrl);
          this.hashCache.set(imageUrl, fallbackHash);
          resolve(fallbackHash);
        };
        
        img.src = URL.createObjectURL(blob);
      });
    } catch (error) {
      console.warn('生成图片哈希失败:', error);
      const fallbackHash = this.generateFallbackHash(imageUrl);
      this.hashCache.set(imageUrl, fallbackHash);
      return fallbackHash;
    }
  }

  // 生成后备哈希(基于URL)
  generateFallbackHash(url) {
    // 简单哈希函数
    let hash = 0;
    for (let i = 0; i < url.length; i++) {
      const char = url.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash; // 转换为32位整数
    }
    return hash.toString();
  }

  // 计算汉明距离(衡量哈希相似度)
  hammingDistance(hash1, hash2) {
    if (hash1.length !== hash2.length) {
      return Math.max(hash1.length, hash2.length);
    }
    
    let distance = 0;
    for (let i = 0; i < hash1.length; i++) {
      if (hash1[i] !== hash2[i]) {
        distance++;
      }
    }
    return distance;
  }

  // 检测并移除重复图片
  async removeDuplicates(images, similarityThreshold = 5) {
    const uniqueImages = [];
    const seenHashes = new Set();
    
    for (const image of images) {
      try {
        const hash = await this.generateImageHash(image.url);
        
        // 检查是否有相似哈希
        let isDuplicate = false;
        for (const seenHash of seenHashes) {
          if (this.hammingDistance(hash, seenHash) <= similarityThreshold) {
            isDuplicate = true;
            break;
          }
        }
        
        if (!isDuplicate) {
          uniqueImages.push(image);
          seenHashes.add(hash);
        }
      } catch (error) {
        // 出错时保留图片
        uniqueImages.push(image);
        console.warn('去重处理失败,保留图片:', image.url, error);
      }
    }
    
    return uniqueImages;
  }

  // 基于URL模式的简单去重
  removeURLDuplicates(images) {
    const seenUrls = new Set();
    return images.filter(image => {
      // 规范化URL(移除查询参数等)
      const normalizedUrl = this.normalizeUrl(image.url);
      if (seenUrls.has(normalizedUrl)) {
        return false;
      }
      seenUrls.add(normalizedUrl);
      return true;
    });
  }

  // 规范化URL
  normalizeUrl(url) {
    try {
      const urlObj = new URL(url);
      // 移除查询参数和片段
      return `${urlObj.origin}${urlObj.pathname}`;
    } catch (e) {
      // 如果URL解析失败,返回原URL
      return url;
    }
  }
}

四、批量下载管理系统

4.1 下载队列与并发控制

实现高效可靠的批量下载功能,避免浏览器限制和性能问题:

// download-manager.js - 高级下载管理系统
class DownloadManager {
  constructor() {
    this.queue = [];
    this.activeDownloads = new Map();
    this.maxConcurrentDownloads = 3; // 最大并发下载数
    this.downloadFolder = 'images'; // 默认下载文件夹
    this.namingConvention = '{name}_{index}{extension}'; // 文件名模板
    this.downloadHistory = [];
    this.paused = false;
    
    // 恢复之前的下载状态
    this.loadState();
  }

  // 添加到下载队列
  addToQueue(imageInfo, options = {}) {
    const downloadItem = {
      id: this.generateId(),
      url: imageInfo.url,
      filename: this.generateFilename(imageInfo, options),
      referrer: imageInfo.pageUrl,
      imageInfo: imageInfo,
      status: 'queued',
      addedAt: Date.now(),
      startedAt: null,
      completedAt: null,
      attempts: 0,
      maxAttempts: 3,
      error: null
    };
    
    this.queue.push(downloadItem);
    this.saveState();
    this.processQueue();
    
    return downloadItem.id;
  }

  // 生成唯一ID
  generateId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2);
  }

  // 生成文件名
  generateFilename(imageInfo, options) {
    let filename = options.filenameTemplate || this.namingConvention;
    const url = new URL(imageInfo.url);
    const extension = this.getFileExtension(imageInfo.url) || '.jpg';
    const name = this.sanitizeFilename(url.pathname.split('/').pop() || 'image');
    
    // 替换模板变量
    filename = filename.replace('{name}', name)
                      .replace('{index}', options.index || '')
                      .replace('{timestamp}', Date.now())
                      .replace('{width}', imageInfo.dimensions.width)
                      .replace('{height}', imageInfo.dimensions.height)
                      .replace('{extension}', extension);
    
    // 确保文件名安全
    return this.sanitizeFilename(filename);
  }

  // 获取文件扩展名
  getFileExtension(url) {
    const match = url.match(/\.(jpe?g|png|gif|bmp|webp|svg)(?:\?|$)/i);
    return match ? '.' + match[1].toLowerCase() : null;
  }

  // 清理文件名中的非法字符
  sanitizeFilename(filename) {
    return filename.replace(/[<>:"/\\|?*]/g, '_')
                   .replace(/\s+/g, '_')
                   .replace(/^\.+/, '')
                   .replace(/\.+$/, '')
                   .substring(0, 200); // 限制长度
  }

  // 处理下载队列
  async processQueue() {
    if (this.paused) return;
    
    // 检查并发限制
    if (this.activeDownloads.size >= this.maxConcurrentDownloads) {
      return;
    }
    
    // 获取下一个待下载项
    const nextItem = this.queue.find(item => item.status === 'queued');
    if (!nextItem) return;
    
    // 更新状态
    nextItem.status = 'downloading';
    nextItem.startedAt = Date.now();
    nextItem.attempts++;
    
    this.activeDownloads.set(nextItem.id, nextItem);
    this.saveState();
    
    try {
      // 开始下载
      const downloadId = await chrome.downloads.download({
        url: nextItem.url,
        filename: `${this.downloadFolder}/${nextItem.filename}`,
        saveAs: false,
        conflictAction: 'uniquify'
      });
      
      // 监听下载状态
      chrome.downloads.onChanged.addListener(function onChanged(delta) {
        if (delta.id === downloadId) {
          if (delta.state && delta.state.current === 'complete') {
            // 下载完成
            chrome.downloads.onChanged.removeListener(onChanged);
            this.downloadComplete(nextItem.id, downloadId);
          } else if (delta.state && delta.state.current === 'interrupted') {
            // 下载中断
            chrome.downloads.onChanged.removeListener(onChanged);
            this.downloadFailed(nextItem.id, '下载中断');
          }
        }
      }.bind(this));
      
    } catch (error) {
      console.error('下载启动失败:', error);
      this.downloadFailed(nextItem.id, error.message);
    }
    
    // 继续处理队列(如果有空位)
    if (this.activeDownloads.size < this.maxConcurrentDownloads) {
      setTimeout(() => this.processQueue(), 100);
    }
  }

  // 下载完成处理
  downloadComplete(itemId, downloadId) {
    const item = this.activeDownloads.get(itemId);
    if (item) {
      item.status = 'completed';
      item.completedAt = Date.now();
      this.downloadHistory.push(item);
      this.activeDownloads.delete(itemId);
      this.saveState();
      
      // 通知UI更新
      chrome.runtime.sendMessage({
        type: 'downloadComplete',
        data: item
      });
    }
    
    // 继续处理队列
    this.processQueue();
  }

  // 下载失败处理
  downloadFailed(itemId, error) {
    const item = this.activeDownloads.get(itemId);
    if (item) {
      if (item.attempts < item.maxAttempts) {
        // 重试
        item.status = 'queued';
        item.error = error;
        this.activeDownloads.delete(itemId);
      } else {
        // 最终失败
        item.status = 'failed';
        item.error = error;
        this.activeDownloads.delete(itemId);
        this.downloadHistory.push(item);
        
        // 通知UI更新
        chrome.runtime.sendMessage({
          type: 'downloadFailed',
          data: item
        });
      }
      this.saveState();
    }
    
    // 继续处理队列
    this.processQueue();
  }

  // 暂停下载
  pauseDownloads() {
    this.paused = true;
  }

  // 继续下载
  resumeDownloads() {
    this.paused = false;
    this.processQueue();
  }

  // 取消下载
  cancelDownload(itemId) {
    // 查找在活动下载或队列中的项目
    let item = this.activeDownloads.get(itemId);
    if (item) {
      // 尝试通过Chrome API取消下载
      chrome.downloads.search({}, (results) => {
        const download = results.find(d => d.filename.includes(item.filename));
        if (download) {
          chrome.downloads.cancel(download.id);
        }
      });
      
      this.activeDownloads.delete(itemId);
      item.status = 'cancelled';
      this.downloadHistory.push(item);
    } else {
      // 从队列中移除
      const index = this.queue.findIndex(i => i.id === itemId);
      if (index !== -1) {
        item = this.queue[index];
        this.queue.splice(index, 1);
        item.status = 'cancelled';
        this.downloadHistory.push(item);
      }
    }
    
    this.saveState();
    
    // 通知UI更新
    chrome.runtime.sendMessage({
      type: 'downloadCancelled',
      data: { id: itemId }
    });
  }

  // 保存状态到存储
  saveState() {
    const state = {
      queue: this.queue,
      downloadHistory: this.downloadHistory.slice(-100), // 保存最近100条记录
      settings: {
        maxConcurrentDownloads: this.maxConcurrentDownloads,
        downloadFolder: this.downloadFolder,
        namingConvention: this.namingConvention
      }
    };
    
    chrome.storage.local.set({ downloadManagerState: state }, () => {
      if (chrome.runtime.lastError) {
        console.warn('保存状态失败:', chrome.runtime.lastError);
      }
    });
  }

  // 从存储加载状态
  async loadState() {
    try {
      const result = await chrome.storage.local.get('downloadManagerState');
      if (result.downloadManagerState) {
        const state = result.downloadManagerState;
        this.queue = state.queue || [];
        this.downloadHistory = state.downloadHistory || [];
        
        if (state.settings) {
          this.maxConcurrentDownloads = state.settings.maxConcurrentDownloads || 3;
          this.downloadFolder = state.settings.downloadFolder || 'images';
          this.namingConvention = state.settings.namingConvention || '{name}_{index}{extension}';
        }
        
        // 恢复队列处理
        this.processQueue();
      }
    } catch (error) {
      console.warn('加载状态失败:', error);
    }
  }

  // 获取下载状态统计
  getStats() {
    const total = this.queue.length + this.activeDownloads.size + this.downloadHistory.length;
    const completed = this.downloadHistory.filter(item => item.status === 'completed').length;
    const failed = this.downloadHistory.filter(item => item.status === 'failed').length;
    const queued = this.queue.filter(item => item.status === 'queued').length;
    const downloading = this.activeDownloads.size;
    
    return {
      total,
      completed,
      failed,
      queued,
      downloading,
      paused: this.paused
    };
  }
}

// 初始化下载管理器
const downloadManager = new DownloadManager();

4.2 断点续传与错误处理

实现可靠的下载恢复机制和全面的错误处理:

// resume-download.js - 断点续传功能
class ResumableDownload {
  constructor() {
    this.chunkSize = 1024 * 1024; // 1MB chunks
    this.maxRetries = 3;
  }

  // 分块下载大型文件
  async downloadInChunks(url, filename, onProgress) {
    let response;
    try {
      // 首先获取文件信息
      response = await fetch(url, { method: 'HEAD' });
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      const totalSize = parseInt(response.headers.get('content-length') || '0', 10);
      const acceptsRanges = response.headers.get('accept-ranges') === 'bytes';

      if (!acceptsRanges || totalSize === 0) {
        // 不支持断点续传,使用普通下载
        return this.downloadNormally(url, filename, onProgress);
      }

      // 检查已有部分下载
      const existingChunks = await this.checkExistingChunks(filename);
      let downloadedSize = existingChunks.size;

      // 创建或继续下载
      while (downloadedSize < totalSize) {
        const start = downloadedSize;
        const end = Math.min(start + this.chunkSize - 1, totalSize - 1);

        try {
          const chunk = await this.downloadChunk(url, start, end, existingChunks.count);
          downloadedSize += chunk.size;

          if (onProgress) {
            onProgress(downloadedSize, totalSize);
          }
        } catch (error) {
          console.error('下载分块失败:', error);
          throw error;
        }
      }

      // 合并所有分块
      await this.mergeChunks(filename, existingChunks.count);
      return true;

    } catch (error) {
      console.error('分块下载失败:', error);
      throw error;
    }
  }

  // 检查已存在的分块
  async checkExistingChunks(filename) {
    // 实现检查本地已下载分块的逻辑
    return { count: 0, size: 0 }; // 简化实现
  }

  // 下载单个分块
  async downloadChunk(url, start, end, chunkIndex) {
    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      try {
        const response = await fetch(url, {
          headers: {
            'Range': `bytes=${start}-${end}`
          }
        });

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }

        const blob = await response.blob();
        await this.saveChunk(blob, chunkIndex);

        return {
          size: blob.size,
          chunkIndex: chunkIndex
        };

      } catch (error) {
        if (attempt === this.maxRetries) {
          throw new Error(`下载分块失败 after ${this.maxRetries} attempts: ${error.message}`);
        }
        await this.delay(1000 * attempt); // 指数退避
      }
    }
  }

  // 保存分块
  async saveChunk(blob, chunkIndex) {
    // 使用File System API或IndexedDB保存分块
    return new Promise((resolve, reject) => {
      // 简化实现 - 实际中需要使用适当的存储API
      resolve();
    });
  }

  // 合并分块
  async mergeChunks(filename, totalChunks) {
    // 实现合并所有分块的逻辑
    console.log(`合并 ${totalChunks} 个分块到 ${filename}`);
  }

  // 普通下载(不支持断点续传时使用)
  async downloadNormally(url, filename, onProgress) {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const totalSize = parseInt(response.headers.get('content-length') || '0', 10);
    const reader = response.body.getReader();
    let receivedSize = 0;
    const chunks = [];

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      chunks.push(value);
      receivedSize += value.length;

      if (onProgress) {
        onProgress(receivedSize, totalSize);
      }
    }

    // 合并所有chunks
    const blob = new Blob(chunks);
    await this.saveFile(blob, filename);
    return true;
  }

  // 保存文件
  async saveFile(blob, filename) {
    // 使用Download API保存文件
    const url = URL.createObjectURL(blob);
    await chrome.downloads.download({
      url: url,
      filename: filename,
      saveAs: false
    });
    URL.revokeObjectURL(url);
  }

  // 延迟函数
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

五、用户界面与交互设计

5.1 弹出页面与选项页面

创建直观易用的用户界面,提供丰富的交互功能:

<!-- popup.html - 主弹出页面 -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    :root {
      --primary-color: #4a6fa5;
      --secondary-color: #6e9cd2;
      --background-color: #f5f7fa;
      --text-color: #333;
      --border-color: #ddd;
      --success-color: #4caf50;
      --error-color: #f44336;
      --warning-color: #ff9800;
    }
    
    body {
      width: 400px;
      min-height: 500px;
      margin: 0;
      padding: 0;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background-color: var(--background-color);
      color: var(--text-color);
    }
    
    .container {
      padding: 15px;
    }
    
    .header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding-bottom: 10px;
      border-bottom: 1px solid var(--border-color);
      margin-bottom: 15px;
    }
    
    .header h1 {
      margin: 0;
      font-size: 18px;
      color: var(--primary-color);
    }
    
    .stats {
      display: flex;
      justify-content: space-around;
      margin-bottom: 15px;
      padding: 10px;
      background: white;
      border-radius: 5px;
      box-shadow: 0 1px 3px rgba(0,0,0,0.1);
    }
    
    .stat-item {
      text-align: center;
    }
    
    .stat-number {
      font-size: 18px;
      font-weight: bold;
      color: var(--primary-color);
    }
    
    .stat-label {
      font-size: 12px;
      color: #666;
    }
    
    .filters {
      margin-bottom: 15px;
    }
    
    .filter-row {
      display: flex;
      margin-bottom: 8px;
      gap: 10px;
    }
    
    .filter-row label {
      min-width: 80px;
      font-size: 12px;
      align-self: center;
    }
    
    .filter-row input, .filter-row select {
      flex: 1;
      padding: 5px;
      border: 1px solid var(--border-color);
      border-radius: 3px;
    }
    
    .actions {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 10px;
      margin-bottom: 15px;
    }
    
    button {
      padding: 8px 12px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-weight: bold;
      transition: background-color 0.2s;
    }
    
    .btn-primary {
      background-color: var(--primary-color);
      color: white;
    }
    
    .btn-primary:hover {
      background-color: var(--secondary-color);
    }
    
    .btn-secondary {
      background-color: #e0e0e0;
      color: #333;
    }
    
    .btn-secondary:hover {
      background-color: #d0d0d0;
    }
    
    .btn-success {
      background-color: var(--success-color);
      color: white;
    }
    
    .btn-warning {
      background-color: var(--warning-color);
      color: white;
    }
    
    .btn-error {
      background-color: var(--error-color);
      color: white;
    }
    
    .image-list {
      max-height: 200px;
      overflow-y: auto;
      border: 1px solid var(--border-color);
      border-radius: 4px;
      background: white;
    }
    
    .image-item {
      display: flex;
      padding: 8px;
      border-bottom: 1px solid var(--border-color);
      align-items: center;
    }
    
    .image-item:last-child {
      border-bottom: none;
    }
    
    .image-preview {
      width: 40px;
      height: 40px;
      object-fit: cover;
      margin-right: 10px;
      border-radius: 3px;
    }
    
    .image-info {
      flex: 1;
    }
    
    .image-name {
      font-size: 12px;
      margin-bottom: 2px;
      word-break: break-all;
    }
    
    .image-details {
      font-size: 10px;
      color: #666;
    }
    
    .progress-bar {
      height: 4px;
      background-color: #e0e0e0;
      border-radius: 2px;
      overflow: hidden;
      margin-top: 5px;
    }
    
    .progress-fill {
      height: 100%;
      background-color: var(--success-color);
      transition: width 0.3s;
    }
    
    .status-indicator {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      margin-right: 5px;
      display: inline-block;
    }
    
    .status-queued { background-color: #ff9800; }
    .status-downloading { background-color: #2196f3; }
    .status-completed { background-color: #4caf50; }
    .status-failed { background-color: #f44336; }
    
    .tab-container {
      margin-top: 15px;
    }
    
    .tabs {
      display: flex;
      border-bottom: 1px solid var(--border-color);
    }
    
    .tab {
      padding: 8px 15px;
      cursor: pointer;
      border-bottom: 2px solid transparent;
    }
    
    .tab.active {
      border-bottom-color: var(--primary-color);
      color: var(--primary-color);
      font-weight: bold;
    }
    
    .tab-content {
      padding: 15px 0;
    }
    
    .hidden {
      display: none;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <h1>图片抓取工具</h1>
      <button id="settings-btn" class="btn-secondary">设置</button>
    </div>
    
    <div class="stats">
      <div class="stat-item">
        <div class="stat-number" id="total-images">0</div>
        <div class="stat-label">检测到</div>
      </div>
      <div class="stat-item">
        <div class="stat-number" id="selected-images">0</div>
        <div class="stat-label">已选择</div>
      </div>
      <div class="stat-item">
        <div class="stat-number" id="download-stats">0/0</div>
        <div class="stat-label">下载进度</div>
      </div>
    </div>
    
    <div class="tabs">
      <div class="tab active" data-tab="images">图片列表</div>
      <div class="tab" data-tab="downloads">下载管理</div>
      <div class="tab" data-tab="filters">筛选设置</div>
    </div>
    
    <div class="tab-content">
      <!-- 图片列表标签 -->
      <div id="tab-images" class="tab-panel">
        <div class="actions">
          <button id="scan-btn" class="btn-primary">扫描图片</button>
          <button id="select-all-btn" class="btn-secondary">全选</button>
        </div>
        
        <div class="filters">
          <div class="filter-row">
            <label for="min-size">最小尺寸:</label>
            <input type="number" id="min-width" placeholder="" min="0">
            <input type="number" id="min-height" placeholder="" min="0">
          </div>
          <div class="filter-row">
            <label for="type-filter">类型:</label>
            <select id="type-filter">
              <option value="all">所有类型</option>
              <option value="standard">标准图片</option>
              <option value="background">背景图片</option>
              <option value="canvas">Canvas</option>
            </select>
          </div>
        </div>
        
        <div class="image-list" id="image-list">
          <!-- 图片列表将由JavaScript动态生成 -->
          <div class="empty-state">点击"扫描图片"开始</div>
        </div>
        
        <div class="actions" style="margin-top: 15px;">
          <button id="download-btn" class="btn-success" disabled>下载选中</button>
          <button id="cancel-btn" class="btn-error" disabled>取消下载</button>
        </div>
      </div>
      
      <!-- 下载管理标签 -->
      <div id="tab-downloads" class="tab-panel hidden">
        <div class="download-controls">
          <button id="pause-btn" class="btn-warning">暂停全部</button>
          <button id="resume-btn" class="btn-success">继续全部</button>
          <button id="clear-completed-btn" class="btn-secondary">清除已完成</button>
        </div>
        
        <div class="download-list" id="download-list">
          <!-- 下载列表将由JavaScript动态生成 -->
          <div class="empty-state">没有活跃的下载任务</div>
        </div>
      </div>
      
      <!-- 筛选设置标签 -->
      <div id="tab-filters" class="tab-panel hidden">
        <div class="filter-settings">
          <div class="filter-row">
            <label for="concurrent-downloads">并发下载:</label>
            <input type="number" id="concurrent-downloads" min="1" max="5" value="3">
          </div>
          <div class="filter-row">
            <label for="download-folder">下载文件夹:</label>
            <input type="text" id="download-folder" value="images">
          </div>
          <div class="filter-row">
            <label for="naming-template">命名模板:</label>
            <input type="text" id="naming-template" value="{name}_{index}{extension}">
          </div>
          <div class="filter-row">
            <label for="auto-scan">自动扫描:</label>
            <input type="checkbox" id="auto-scan">
          </div>
        </div>
        
        <button id="save-settings" class="btn-primary">保存设置</button>
      </div>
    </div>
  </div>

  <script src="popup.js"></script>
</body>
</html>

5.2 响应式UI与实时更新

实现动态交互和实时状态更新的JavaScript代码:

// popup.js - 弹出页面逻辑
class PopupUI {
  constructor() {
    this.images = [];
    this.selectedImages = new Set();
    this.downloads = [];
    this.currentTab = 'images';
    
    this.initializeEventListeners();
    this.loadState();
    this.updateStats();
    
    // 请求当前页面的图片数据
    this.requestImageData();
  }

  // 初始化事件监听器
  initializeEventListeners() {
    // 标签切换
    document.querySelectorAll('.tab').forEach(tab => {
      tab.addEventListener('click', () => {
        this.switchTab(tab.dataset.tab);
      });
    });
    
    // 扫描按钮
    document.getElementById('scan-btn').addEventListener('click', () => {
      this.scanImages();
    });
    
    // 全选按钮
    document.getElementById('select-all-btn').addEventListener('click', () => {
      this.selectAllImages();
    });
    
    // 下载按钮
    document.getElementById('download-btn').addEventListener('click', () => {
      this.downloadSelected();
    });
    
    // 设置按钮
    document.getElementById('settings-btn').addEventListener('click', () => {
      this.switchTab('filters');
    });
    
    // 保存设置
    document.getElementById('save-settings').addEventListener('click', () => {
      this.saveSettings();
    });
    
    // 过滤器变化
    document.getElementById('min-width').addEventListener('change', () => {
      this.applyFilters();
    });
    
    document.getElementById('min-height').addEventListener('change', () => {
      this.applyFilters();
    });
    
    document.getElementById('type-filter').addEventListener('change', () => {
      this.applyFilters();
    });
    
    // 监听来自内容脚本和后台的消息
    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
      this.handleRuntimeMessage(message);
    });
  }

  // 处理运行时消息
  handleRuntimeMessage(message) {
    switch (message.type) {
      case 'imageListUpdate':
        this.images = message.data.images;
        this.renderImageList();
        this.updateStats();
        break;
        
      case 'downloadProgress':
        this.updateDownloadProgress(message.data);
        break;
        
      case 'downloadComplete':
        this.updateDownloadStatus(message.data.id, 'completed');
        break;
        
      case 'downloadFailed':
        this.updateDownloadStatus(message.data.id, 'failed', message.data.error);
        break;
        
      case 'downloadCancelled':
        this.removeDownloadItem(message.data.id);
        break;
    }
  }

  // 请求图片数据
  async requestImageData() {
    try {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
      const response = await chrome.tabs.sendMessage(tab.id, { type: 'getImages' });
      
      if (response && response.images) {
        this.images = response.images;
        this.renderImageList();
        this.updateStats();
      }
    } catch (error) {
      console.warn('无法获取图片数据:', error);
      document.getElementById('image-list').innerHTML = 
        '<div class="empty-state">请刷新页面后重试</div>';
    }
  }

  // 扫描图片
  async scanImages() {
    try {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
      await chrome.tabs.sendMessage(tab.id, { type: 'scanImages' });
      
      // 显示加载状态
      document.getElementById('image-list').innerHTML = 
        '<div class="empty-state">扫描中...</div>';
    } catch (error) {
      console.error('扫描失败:', error);
    }
  }

  // 渲染图片列表
  renderImageList() {
    const container = document.getElementById('image-list');
    
    if (this.images.length === 0) {
      container.innerHTML = '<div class="empty-state">未检测到图片</div>';
      return;
    }
    
    let html = '';
    this.images.forEach((image, index) => {
      const isSelected = this.selectedImages.has(image.url);
      const dimensions = image.dimensions || { width: 0, height: 0 };
      
      html += `
        <div class="image-item" data-url="${image.url}">
          <input type="checkbox" class="image-checkbox" 
                 data-url="${image.url}" 
                 ${isSelected ? 'checked' : ''}>
          <img class="image-preview" src="${image.url}" 
               onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiBmaWxsPSIjRUVFRUVFIi8+CjxwYXRoIGQ9Ik0yMCAxM1YyNyIgc3Ryb2tlPSIjOTk5OTk5IiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iTTEzIDIwSDI3IiBzdHJva2U9IiM5OTk5OTkiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K'" 
               alt="预览">
          <div class="image-info">
            <div class="image-name">${this.getFilenameFromUrl(image.url)}</div>
            <div class="image-details">
              ${dimensions.width} × ${dimensions.height}${this.formatFileSize(image.estimatedSize)}${image.type}
            </div>
          </div>
        </div>
      `;
    });
    
    container.innerHTML = html;
    
    // 添加复选框事件监听
    container.querySelectorAll('.image-checkbox').forEach(checkbox => {
      checkbox.addEventListener('change', (e) => {
        this.toggleImageSelection(e.target.dataset.url, e.target.checked);
      });
    });
  }

  // 切换图片选择状态
  toggleImageSelection(url, selected) {
    if (selected) {
      this.selectedImages.add(url);
    } else {
      this.selectedImages.delete(url);
    }
    
    this.updateDownloadButtonState();
    this.updateStats();
  }

  // 全选图片
  selectAllImages() {
    this.images.forEach(image => {
      this.selectedImages.add(image.url);
    });
    
    this.renderImageList();
    this.updateDownloadButtonState();
    this.updateStats();
  }

  // 应用过滤器
  applyFilters() {
    // 获取过滤条件
    const minWidth = parseInt(document.getElementById('min-width').value) || 0;
    const minHeight = parseInt(document.getElementById('min-height').value) || 0;
    const typeFilter = document.getElementById('type-filter').value;
    
    // 过滤图片
    const filteredImages = this.images.filter(image => {
      const dimensions = image.dimensions || { width: 0, height: 0 };
      
      if (dimensions.width < minWidth) return false;
      if (dimensions.height < minHeight) return false;
      if (typeFilter !== 'all' && image.type !== typeFilter) return false;
      
      return true;
    });
    
    // 重新渲染
    this.images = filteredImages;
    this.renderImageList();
    this.updateStats();
  }

  // 下载选中的图片
  async downloadSelected() {
    if (this.selectedImages.size === 0) return;
    
    try {
      // 获取选中的图片信息
      const imagesToDownload = this.images.filter(image => 
        this.selectedImages.has(image.url)
      );
      
      // 发送下载请求到后台
      chrome.runtime.sendMessage({
        type: 'downloadImages',
        data: {
          images: imagesToDownload,
          options: {
            filenameTemplate: document.getElementById('naming-template').value,
            folder: document.getElementById('download-folder').value
          }
        }
      });
      
      // 切换到下载标签页
      this.switchTab('downloads');
      
      // 清空选择
      this.selectedImages.clear();
      this.updateDownloadButtonState();
      
    } catch (error) {
      console.error('下载启动失败:', error);
    }
  }

  // 更新下载按钮状态
  updateDownloadButtonState() {
    const downloadBtn = document.getElementById('download-btn');
    const cancelBtn = document.getElementById('cancel-btn');
    
    downloadBtn.disabled = this.selectedImages.size === 0;
    cancelBtn.disabled = this.downloads.length === 0;
  }

  // 更新统计信息
  updateStats() {
    document.getElementById('total-images').textContent = this.images.length;
    document.getElementById('selected-images').textContent = this.selectedImages.size;
    
    // 下载统计
    const total = this.downloads.length;
    const completed = this.downloads.filter(d => d.status === 'completed').length;
    document.getElementById('download-stats').textContent = `${completed}/${total}`;
  }

  // 切换标签页
  switchTab(tabName) {
    // 更新标签状态
    document.querySelectorAll('.tab').forEach(tab => {
      tab.classList.toggle('active', tab.dataset.tab === tabName);
    });
    
    // 更新内容可见性
    document.querySelectorAll('.tab-panel').forEach(panel => {
      panel.classList.toggle('hidden', panel.id !== `tab-${tabName}`);
    });
    
    this.currentTab = tabName;
    
    // 如果是下载标签,刷新下载列表
    if (tabName === 'downloads') {
      this.refreshDownloadList();
    }
  }

  // 刷新下载列表
  refreshDownloadList() {
    chrome.runtime.sendMessage({
      type: 'getDownloadStatus'
    }, response => {
      if (response && response.downloads) {
        this.downloads = response.downloads;
        this.renderDownloadList();
        this.updateStats();
      }
    });
  }

  // 渲染下载列表
  renderDownloadList() {
    const container = document.getElementById('download-list');
    
    if (this.downloads.length === 0) {
      container.innerHTML = '<div class="empty-state">没有活跃的下载任务</div>';
      return;
    }
    
    let html = '';
    this.downloads.forEach(download => {
      html += `
        <div class="download-item" data-id="${download.id}">
          <span class="status-indicator status-${download.status}"></span>
          <div class="download-info">
            <div class="download-filename">${download.filename}</div>
            <div class="download-details">
              ${this.formatFileSize(download.size)}${this.formatStatus(download.status)}
              ${download.error ? `${download.error}` : ''}
            </div>
            ${download.progress !== undefined ? `
              <div class="progress-bar">
                <div class="progress-fill" style="width: ${download.progress}%"></div>
              </div>
            ` : ''}
          </div>
          <button class="cancel-download-btn" data-id="${download.id}">取消</button>
        </div>
      `;
    });
    
    container.innerHTML = html;
    
    // 添加取消按钮事件
    container.querySelectorAll('.cancel-download-btn').forEach(btn => {
      btn.addEventListener('click', (e) => {
        this.cancelDownload(e.target.dataset.id);
      });
    });
  }

  // 取消下载
  cancelDownload(downloadId) {
    chrome.runtime.sendMessage({
      type: 'cancelDownload',
      data: { id: downloadId }
    });
  }

  // 更新下载进度
  updateDownloadProgress(data) {
    const item = this.downloads.find(d => d.id === data.id);
    if (item) {
      item.progress = data.progress;
      item.size = data.size;
      this.renderDownloadList();
    }
  }

  // 更新下载状态
  updateDownloadStatus(downloadId, status, error = null) {
    const item = this.downloads.find(d => d.id === downloadId);
    if (item) {
      item.status = status;
      item.error = error;
      this.renderDownloadList();
      this.updateStats();
    }
  }

  // 移除下载项
  removeDownloadItem(downloadId) {
    this.downloads = this.downloads.filter(d => d.id !== downloadId);
    this.renderDownloadList();
    this.updateStats();
  }

  // 保存设置
  saveSettings() {
    const settings = {
      concurrentDownloads: parseInt(document.getElementById('concurrent-downloads').value),
      downloadFolder: document.getElementById('download-folder').value,
      namingTemplate: document.getElementById('naming-template').value,
      autoScan: document.getElementById('auto-scan').checked
    };
    
    chrome.runtime.sendMessage({
      type: 'saveSettings',
      data: settings
    });
    
    alert('设置已保存');
  }

  // 加载状态
  loadState() {
    chrome.storage.local.get(['popupState'], (result) => {
      if (result.popupState) {
        this.selectedImages = new Set(result.popupState.selectedImages || []);
      }
    });
  }

  // 工具函数:从URL提取文件名
  getFilenameFromUrl(url) {
    try {
      const urlObj = new URL(url);
      return urlObj.pathname.split('/').pop() || 'image';
    } catch {
      return url.length > 30 ? url.substring(0, 30) + '...' : url;
    }
  }

  // 工具函数:格式化文件大小
  formatFileSize(bytes) {
    if (!bytes || bytes === 0) return '0 B';
    
    const units = ['B', 'KB', 'MB', 'GB'];
    const exponent = Math.floor(Math.log(bytes) / Math.log(1024));
    const size = (bytes / Math.pow(1024, exponent)).toFixed(1);
    
    return `${size} ${units[exponent]}`;
  }

  // 工具函数:格式化状态文本
  formatStatus(status) {
    const statusMap = {
      'queued': '排队中',
      'downloading': '下载中',
      'completed': '已完成',
      'failed': '失败',
      'cancelled': '已取消'
    };
    
    return statusMap[status] || status;
  }
}

// 初始化UI
document.addEventListener('DOMContentLoaded', () => {
  new PopupUI();
});

六、高级功能与性能优化

6.1 内存管理与性能优化

确保插件在处理大量图片时保持高效和稳定:

// performance.js - 性能优化与内存管理
class PerformanceOptimizer {
  constructor() {
    this.maxImageCacheSize = 100; // 最大缓存图片数
    this.imageCache = new Map();
    this.memoryWarning = false;
    
    // 监听内存警告
    this.setupMemoryMonitoring();
  }

  // 设置内存监控
  setupMemoryMonitoring() {
    // 定期检查内存使用情况
    setInterval(() => {
      this.checkMemoryUsage();
    }, 30000); // 每30秒检查一次
  }

  // 检查内存使用情况
  async checkMemoryUsage() {
    if (chrome.system && chrome.system.memory) {
      try {
        const memoryInfo = await new Promise(resolve => {
          chrome.system.memory.getInfo(resolve);
        });
        
        // 计算可用内存百分比
        const availableMemoryPercent = (memoryInfo.availableCapacity / memoryInfo.capacity) * 100;
        
        if (availableMemoryPercent < 20) {
          this.memoryWarning = true;
          this.freeUpMemory();
          console.warn('内存不足,已启用节能模式');
        } else {
          this.memoryWarning = false;
        }
      } catch (error) {
        // system.memory API可能不可用
        console.debug('无法访问系统内存信息:', error);
      }
    }
  }

  // 释放内存
  freeUpMemory() {
    // 清空图片缓存
    this.imageCache.clear();
    
    // 减少DOM节点引用
    this.cleanupDOMReferences();
    
    // 通知其他组件进入节能模式
    chrome.runtime.sendMessage({
      type: 'memoryWarning',
      data: { severe: this.memoryWarning }
    });
  }

  // 清理DOM引用
  cleanupDOMReferences() {
    // 实现清理不再需要的DOM引用
  }

  // 缓存图片数据
  cacheImageData(url, data) {
    // 如果缓存过大,移除最旧的项
    if (this.imageCache.size >= this.maxImageCacheSize) {
      const oldestKey = this.imageCache.keys().next().value;
      this.imageCache.delete(oldestKey);
    }
    
    this.imageCache.set(url, {
      data: data,
      timestamp: Date.now()
    });
  }

  // 获取缓存的图片数据
  getCachedImageData(url) {
    const cached = this.imageCache.get(url);
    if (cached) {
      // 更新使用时间
      cached.timestamp = Date.now();
      return cached.data;
    }
    return null;
  }

  // 优化图片处理性能
  optimizeImageProcessing(images) {
    if (this.memoryWarning) {
      // 内存警告时,减少处理的数据量
      return images.slice(0, 50); // 只处理前50张图片
    }
    
    // 正常情况下的优化策略
    return images.filter(image => {
      // 过滤掉太小的图片
      const dimensions = image.dimensions || { width: 0, height: 0 };
      return dimensions.width >= 50 && dimensions.height >= 50;
    });
  }

  // 批量处理优化
  async processInBatches(items, processFn, batchSize = 10) {
    const results = [];
    
    for (let i = 0; i < items.length; i += batchSize) {
      // 检查内存状态
      if (this.memoryWarning) {
        batchSize = Math.max(1, Math.floor(batchSize / 2));
      }
      
      const batch = items.slice(i, i + batchSize);
      const batchResults = await Promise.allSettled(
        batch.map(item => processFn(item))
      );
      
      results.push(...batchResults);
      
      // 批次之间添加延迟,避免UI冻结
      if (i + batchSize < items.length) {
        await this.delay(100);
      }
    }
    
    return results;
  }

  // 延迟函数
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // 节流函数
  throttle(func, limit) {
    let inThrottle;
    return function(...args) {
      if (!inThrottle) {
        func.apply(this, args);
        inThrottle = true;
        setTimeout(() => inThrottle = false, limit);
      }
    };
  }

  // 防抖函数
  debounce(func, wait) {
    let timeout;
    return function(...args) {
      clearTimeout(timeout);
      timeout = setTimeout(() => func.apply(this, args), wait);
    };
  }
}

// 初始化性能优化器
const performanceOptimizer = new PerformanceOptimizer();

6.2 高级下载功能

实现更强大的下载管理功能,支持各种特殊场景:

// advanced-download.js - 高级下载功能
class AdvancedDownloadFeatures {
  constructor() {
    this.downloadStrategies = new Map();
    this.setupDownloadStrategies();
  }

  // 设置下载策略
  setupDownloadStrategies() {
    // 标准HTTP下载
    this.downloadStrategies.set('http', this.downloadViaHTTP.bind(this));
    
    // Data URL下载
    this.downloadStrategies.set('data', this.downloadDataURL.bind(this));
    
    // Blob下载
    this.downloadStrategies.set('blob', this.downloadBlob.bind(this));
    
    // Canvas下载
    this.downloadStrategies.set('canvas', this.downloadCanvas.bind(this));
  }

  // 根据URL类型选择合适的下载策略
  async downloadWithStrategy(url, options = {}) {
    try {
      let strategy = 'http';
      
      if (url.startsWith('data:')) {
        strategy = 'data';
      } else if (url.startsWith('blob:')) {
        strategy = 'blob';
      } else if (options.element && options.element.tagName === 'CANVAS') {
        strategy = 'canvas';
      }
      
      const downloadFn = this.downloadStrategies.get(strategy);
      if (downloadFn) {
        return await downloadFn(url, options);
      }
      
      throw new Error(`没有适用的下载策略: ${strategy}`);
      
    } catch (error) {
      console.error('下载失败:', error);
      throw error;
    }
  }

  // 标准HTTP下载
  async downloadViaHTTP(url, options) {
    // 添加referrer信息
    const downloadOptions = {
      url: url,
      filename: options.filename,
      saveAs: false,
      conflictAction: 'uniquify'
    };
    
    if (options.referrer) {
      downloadOptions.referrer = options.referrer;
    }
    
    return await chrome.downloads.download(downloadOptions);
  }

  // Data URL下载
  async downloadDataURL(dataUrl, options) {
    // 将Data URL转换为Blob
    const response = await fetch(dataUrl);
    const blob = await response.blob();
    
    // 创建Blob URL并下载
    const blobUrl = URL.createObjectURL(blob);
    try {
      const downloadId = await chrome.downloads.download({
        url: blobUrl,
        filename: options.filename,
        saveAs: false
      });
      
      return downloadId;
    } finally {
      // 清理Blob URL
      URL.revokeObjectURL(blobUrl);
    }
  }

  // Blob URL下载
  async downloadBlob(blobUrl, options) {
    // 直接下载Blob URL
    return await chrome.downloads.download({
      url: blobUrl,
      filename: options.filename,
      saveAs: false
    });
  }

  // Canvas下载
  async downloadCanvas(canvas, options) {
    try {
      // 获取Canvas数据
      let dataUrl;
      try {
        dataUrl = canvas.toDataURL('image/png');
      } catch (error) {
        // 尝试其他格式
        try {
          dataUrl = canvas.toDataURL('image/jpeg');
        } catch (error) {
          throw new Error('Canvas无法转换为图片: ' + error.message);
        }
      }
      
      // 下载Data URL
      return await this.downloadDataURL(dataUrl, options);
    } catch (error) {
      console.error('Canvas下载失败:', error);
      throw error;
    }
  }

  // 批量下载优化
  async downloadBatch(images, options = {}) {
    const results = [];
    const batchSize = options.batchSize || 3;
    
    for (let i = 0; i < images.length; i += batchSize) {
      const batch = images.slice(i, i + batchSize);
      const batchPromises = batch.map((image, index) => 
        this.downloadImage(image, {
          ...options,
          index: i + index + 1
        })
      );
      
      const batchResults = await Promise.allSettled(batchPromises);
      results.push(...batchResults);
      
      // 批次间延迟,避免过度并发
      if (i + batchSize < images.length) {
        await new Promise(resolve => setTimeout(resolve, 500));
      }
    }
    
    return results;
  }

  // 下载单个图片
  async downloadImage(imageInfo, options = {}) {
    try {
      const filename = this.generateFilename(imageInfo, options);
      
      const downloadId = await this.downloadWithStrategy(imageInfo.url, {
        filename: filename,
        referrer: imageInfo.pageUrl,
        element: imageInfo.element
      });
      
      return {
        success: true,
        id: downloadId,
        url: imageInfo.url,
        filename: filename
      };
    } catch (error) {
      return {
        success: false,
        url: imageInfo.url,
        error: error.message
      };
    }
  }

  // 生成文件名
  generateFilename(imageInfo, options = {}) {
    const template = options.filenameTemplate || '{name}_{index}{extension}';
    const url = new URL(imageInfo.url);
    const extension = this.getFileExtension(imageInfo.url) || '.png';
    const name = this.sanitizeFilename(url.pathname.split('/').pop() || 'image');
    
    // 替换模板变量
    return template.replace('{name}', name)
                  .replace('{index}', options.index ? `_${options.index}` : '')
                  .replace('{timestamp}', Date.now())
                  .replace('{width}', imageInfo.dimensions?.width || 0)
                  .replace('{height}', imageInfo.dimensions?.height || 0)
                  .replace('{extension}', extension);
  }

  // 获取文件扩展名
  getFileExtension(url) {
    if (url.startsWith('data:')) {
      const match = url.match(/data:image\/(\w+);/);
      return match ? `.${match[1]}` : '.png';
    }
    
    const match = url.match(/\.(jpe?g|png|gif|bmp|webp|svg)(?:\?|$)/i);
    return match ? `.${match[1].toLowerCase()}` : null;
  }

  // 清理文件名
  sanitizeFilename(filename) {
    return filename.replace(/[<>:"/\\|?*]/g, '_')
                  .replace(/\s+/g, '_')
                  .replace(/^\.+/, '')
                  .replace(/\.+$/, '')
                  .substring(0, 200);
  }

  // 下载重试机制
  async downloadWithRetry(url, options, maxRetries = 3) {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        return await this.downloadWithStrategy(url, options);
      } catch (error) {
        if (attempt === maxRetries) {
          throw error;
        }
        
        // 指数退避延迟
        const delayMs = 1000 * Math.pow(2, attempt - 1);
        await new Promise(resolve => setTimeout(resolve, delayMs));
      }
    }
  }
}

七、安全性与隐私保护

7.1 内容安全策略

确保插件遵守浏览器的安全规范,保护用户隐私:

{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'",
    "sandbox": "sandbox allow-scripts; script-src 'self' 'https://apis.example.com'"
  }
}

7.2 权限最小化原则

只请求必要的权限,并在使用时向用户解释原因:

// permissions.js - 权限管理
class PermissionManager {
  constructor() {
    this.requiredPermissions = {
      downloads: '用于保存图片到您的设备',
      storage: '用于记住您的设置和偏好',
      activeTab: '用于访问当前页面的图片内容'
    };
  }

  // 检查并请求必要权限
  async checkAndRequestPermissions() {
    const missingPermissions = await this.getMissingPermissions();
    
    if (missingPermissions.length > 0) {
      const granted = await this.requestPermissions(missingPermissions);
      
      if (!granted) {
        throw new Error('需要权限才能使用完整功能');
      }
    }
  }

  // 获取缺失的权限
  async getMissingPermissions() {
    const missing = [];
    
    for (const [permission, reason] of Object.entries(this.requiredPermissions)) {
      const hasPermission = await chrome.permissions.contains({
        permissions: [permission]
      });
      
      if (!hasPermission) {
        missing.push(permission);
      }
    }
    
    return missing;
  }

  // 请求权限
  async requestPermissions(permissions) {
    try {
      return await chrome.permissions.request({
        permissions: permissions
      });
    } catch (error) {
      console.error('权限请求失败:', error);
      return false;
    }
  }

  // 解释权限用途
  explainPermission(permission) {
    return this.requiredPermissions[permission] || '用于提供基本功能';
  }

  // 检查特定权限
  async hasPermission(permission) {
    return await chrome.permissions.contains({
      permissions: [permission]
    });
  }
}

结论:打造卓越的图片抓取体验

通过本指南,我们详细探讨了如何构建一个功能全面、性能优异且用户友好的浏览器图片抓取与下载工具。从基础架构到高级功能,从用户体验到性能优化,我们覆盖了开发此类插件的所有关键方面。

核心价值与创新点

  1. 全面检测能力:不仅能检测标准img标签,还能发现CSS背景图片、Canvas内容、SVG图像和延迟加载图片
  2. 智能过滤系统:多维度过滤和排序功能,帮助用户快速定位所需图片
  3. 高效下载管理:支持断点续传、批量下载、错误恢复和并发控制
  4. 优雅的用户界面:直观的弹出页面和选项页面,提供实时反馈和状态更新
  5. 性能优化:内存管理、批量处理和节流防抖机制,确保插件高效稳定运行

未来发展方向

  1. AI增强功能:集成图像识别技术,实现基于内容的智能分类和搜索
  2. 云同步:支持将采集的图片同步到云存储服务
  3. 高级编辑功能:添加简单的图片编辑和处理能力
  4. 跨浏览器支持:扩展支持Firefox、Safari等其他浏览器
  5. 社交媒体集成:直接分享图片到社交媒体平台

浏览器插件开发是一个不断演进的过程,随着Web技术的发展和新API的出现,我们将有机会为用户提供更加强大和便捷的图片管理体验。通过遵循最佳实践、注重用户体验和持续优化改进,您可以打造出真正优秀的浏览器扩展作品。


参考资源

  1. Chrome扩展开发文档
  2. MDN浏览器扩展指南
  3. Web扩展API参考
  4. JavaScript高级程序设计
  5. 现代前端工程化实践
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐