(3)从零开发 Chrome 插件:网页图片的批量下载

(1)从零开发 Chrome 插件:构建你的第一个插件
(3)从零开发 Chrome 插件:实现 API 登录与本地存储功能
(3)从零开发 Chrome 插件:网页图片的批量下载
(4)从零开发 Chrome 插件:Chrome插件调试全攻略

我们常常会遇到需要保存页面图片的情况。一张两张手动保存还好,但要是遇到图片数量较多的页面,那就比较痛苦。基于这样的痛点,我开发了一款 Chrome 图片下载插件,能实现网页图片的批量下载。下面就来详细分享这款插件的开发过程。

一 插件核心功能

当用户点击浏览器工具栏中的插件图标时,会弹出一个操作界面。在这个界面里,用户可以进行一系列设置,比如给要下载的图片设置文件名前缀,用正则表达式来过滤不需要的图片 URL,还能选择是否只下载当前页面中可见的图片。

完成设置后,用户点击 "开始下载图片" 按钮,插件就会启动内容脚本。内容脚本会注入到当前正在浏览的网页中,收集页面里所有 img 标签对应的图片资源。在收集过程中,它会对图片的 URL 格式进行处理,比如把相对路径转换成绝对路径,然后根据用户设置的过滤规则,筛选出符合条件的图片,再把这些图片的信息发送到后台脚本。。

二 程序流程深度解析

用户点击插件图标,弹出操作界面加载完成。用户在界面中输入文件名前缀、设置过滤规则等信息,接着点击确认下载按钮。此时,内容脚本启动,开始提取图片的 src 或者 data-src 属性值,将其中的相对路径转换为绝对路径。然后,根据用户设置的规则对图片进行筛选,筛选出符合条件的图片后,把图片列表发送到后台脚本。后台脚本接收图片列表后,调用 Chrome 下载 API,循环下载图片并保存到本地指定路径。最后,判断所有图片是否都下载完成,如果完成,就显示下载完成的通知,整个流程到此结束。

三 功能详细说明

1. 用户交互层

这一层是用户与插件进行交互的主要部分。当用户点击浏览器工具栏中的插件图标时,会触发弹出界面(由 popup.html 实现)的加载。用户在这个界面中设置下载参数,比如文件名前缀、过滤规则等,然后确认下载操作,这一系列动作都属于用户交互层的范畴。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片下载器</title>
    <script src="popup.js"></script>
    <style>
        body {
            width: 350px;
            padding: 10px;
            font-family: Arial, sans-serif;
            margin: 0;
        }
        .container {
            padding: 10px;
        }
        h3 {
            margin-top: 0;
            color: #333;
        }
        .section {
            margin-bottom: 15px;
        }
        .section label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        .section input, .section textarea {
            width: 100%;
            padding: 8px;
            box-sizing: border-box;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .checkbox-group {
            margin-bottom: 10px;
        }
        .checkbox-group label {
            font-weight: normal;
            margin-left: 5px;
        }
        button {
            background-color: #4CAF50;
            color: white;
            padding: 10px 15px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            width: 100%;
            font-size: 16px;
        }
        button:hover {
            background-color: #45a049;
        }
        .status {
            margin-top: 10px;
            padding: 8px;
            border-radius: 4px;
            display: none;
        }
        .status.success {
            background-color: #d4edda;
            color: #155724;
        }
        .status.error {
            background-color: #f8d7da;
            color: #721c24;
        }
        .image-count {
            margin-top: 5px;
            font-size: 12px;
            color: #666;
        }
    </style>
</head>
<body>
    <div class="container">
        <h3>图片下载器</h3>
        <div class="section">
            <label for="prefix">文件名前缀 (可选):</label>
            <input type="text" id="prefix" placeholder="例如: mysite_">
        </div>
        
        <div class="section">
            <label for="filter">图片URL过滤 (正则表达式, 可选):</label>
            <input type="text" id="filter" placeholder="例如: \.(jpg|png|gif)$">
        </div>
        
        <div class="checkbox-group">
            <input type="checkbox" id="onlyVisible" checked>
            <label for="onlyVisible">仅下载可见图片</label>
        </div>
        
        <button id="downloadBtn">开始下载图片</button>
        
        <div id="status" class="status"></div>
        
        <div class="image-count">
            找到 <span id="imageCount">0</span> 张图片
        </div>
    </div>
</body>
</html>    
2. 用户交互层脚 popup.js
javascript 复制代码
document.addEventListener('DOMContentLoaded', () => {
    const downloadBtn = document.getElementById('downloadBtn');
    const status = document.getElementById('status');
    const imageCount = document.getElementById('imageCount');
    const prefixInput = document.getElementById('prefix');
    const filterInput = document.getElementById('filter');
    const onlyVisibleCheckbox = document.getElementById('onlyVisible');
    // 新增目录输入元素
    const directoryInput = document.getElementById('directory');
    // 初始获取图片数量
    updateImageCount();

    // 监听下载按钮点击事件
    downloadBtn.addEventListener('click', () => {
        // 显示加载状态
        showStatus('正在收集图片...', 'success');

        // 获取当前活动标签页
        chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
            const activeTab = tabs[0];

            // 向content script发送消息获取图片
            chrome.tabs.sendMessage(activeTab.id, {
                action: 'getImages',
                filter: filterInput.value,
                onlyVisible: onlyVisibleCheckbox.checked
            }, (response) => {
                if (chrome.runtime.lastError) {
                    showStatus('无法获取图片: ' + chrome.runtime.lastError.message, 'error');
                    return;
                }

                if (!response || !response.images || response.images.length === 0) {
                    showStatus('没有找到符合条件的图片', 'error');
                    return;
                }

                const images = response.images;
                const prefix = prefixInput.value;

                // 显示下载信息
                showStatus(`找到 ${images.length} 张图片,开始下载...`, 'success');

                // 发送消息到background.js进行下载
                chrome.runtime.sendMessage({
                    action: 'downloadImages',
                    images: images,
                    prefix: prefix
                });

                // 关闭弹出窗口
                window.close();
            });
        });
    });

    // 监听过滤条件变化,更新图片数量
    filterInput.addEventListener('input', updateImageCount);
    onlyVisibleCheckbox.addEventListener('change', updateImageCount);

    // 更新图片数量
    function updateImageCount() {
        // 获取当前活动标签页
        chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
            const activeTab = tabs[0];

            // 向content script发送消息获取图片
            chrome.tabs.sendMessage(activeTab.id, {
                action: 'getImages',
                filter: filterInput.value,
                onlyVisible: onlyVisibleCheckbox.checked
            }, (response) => {
                if (!chrome.runtime.lastError && response && response.images) {
                    imageCount.textContent = response.images.length;
                } else {
                    imageCount.textContent = '?';
                }
            });
        });
    }

    // 显示状态信息
    function showStatus(message, type) {
        status.textContent = message;
        status.className = `status ${type}`;
        status.style.display = 'block';
    }
});
3.配置文件
json 复制代码
{
  "manifest_version": 3,
  "name": "图片下载器",
  "version": "1.0",
  "description": "一键下载当前页面上的所有图片",
  "permissions": [
    "activeTab",
    "downloads",
    "storage",
    "tabs",
    "scripting"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icon16.png"

    }
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"]
    }
  ],
  "icons": {
    "16": "icon16.png"

  }
}

这个一定要配置

**downloads ** 下载权限

**storage ** 存储权限

4.连接层context
复制代码
是连接网页和后台脚本的桥梁。它会被注入到当前正在浏览的网页中,负责提取页面中图片的 src 或者 data-src 属性值。在提取过程中,它会处理 URL 路径,把相对路径转换为绝对路径,以确保图片能够被正确访问。之后,它会根据用户设置的过滤规则,筛选出符合条件的图片,并把这些图片的列表发送给后台脚本。
javascript 复制代码
// 监听来自popup的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.action === 'getImages') {
        const { filter, onlyVisible } = message;

        // 收集页面上的所有图片
        let images = Array.from(document.querySelectorAll('img')).map(img => {
            let src = img.src;
             debugger;
            // 处理data-src等替代属性
            if (!src && img.dataset.src) src = img.dataset.src;
            if (!src && img.dataset.origSrc) src = img.dataset.origSrc;
            if (!src && img.dataset.lazySrc) src = img.dataset.lazySrc;

            // 跳过没有有效src的图片
            if (!src) return null;

            // 处理相对URL
            if (!src.startsWith('http') && !src.startsWith('data:')) {
                try {
                    src = new URL(src, window.location.href).href;
                } catch (e) {
                    return null;
                }
            }

            // 跳过data:URL的图片(通常是小图标或临时图片)
            if (src.startsWith('data:')) return null;

            return {
                src: src,
                width: img.naturalWidth || img.offsetWidth,
                height: img.naturalHeight || img.offsetHeight,
                alt: img.alt || '',
                isVisible: isElementInViewport(img)
            };
        }).filter(img => img !== null);

        // 过滤可见图片
        if (onlyVisible) {
            images = images.filter(img => img.isVisible);
        }

        // 应用正则表达式过滤
        if (filter && filter.trim() !== '') {
            try {
                const regex = new RegExp(filter, 'i');
                images = images.filter(img => regex.test(img.src) || regex.test(img.alt));
            } catch (e) {
                console.error('无效的正则表达式:', filter);
            }
        }

        // 返回图片列表
        sendResponse({ images });
    }
    return true; // 保持消息通道打开,直到sendResponse被调用
});

// 检查元素是否在视口中
function isElementInViewport(el) {
    const rect = el.getBoundingClientRect();
    return (
        rect.top >= 0 &&
        rect.left >= 0 &&
        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
        rect.right <= (window.innerWidth || document.documentElement.clientWidth)
    );
}
5. 后台处理层

后台脚本(background.js)是插件的核心处理部分。它接收内容脚本发送过来的图片列表后,会调用 Chrome 浏览器的 downloads API,循环对列表中的图片进行下载操作,并将图片保存到本地用户指定的路径中。在下载过程中,它还会监控下载进度,判断所有图片是否都下载完成。

javascript 复制代码
// background.js
// 监听来自content script的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.action === 'downloadImages') {
        const { images, prefix } = message;
        let downloaded = 0;
        let failed = 0;
        
        // 逐个下载图片
        images.forEach((image, index) => {
            // 生成唯一的文件名
            const filename = generateFilename(image.src, prefix, index);
            debugger;
            // 创建下载任务
            chrome.downloads.download({
                url: image.src,
                filename: filename,
                saveAs: false
            }, (downloadId) => {
                if (!downloadId) {
                    console.error('下载失败:', chrome.runtime.lastError, image.src);
                } else {
                    downloaded++;
                }
                
                // 当所有下载任务都完成后,发送通知
                if (index === images.length - 1) {
                    sendNotification(downloaded, failed);
                }
            });
        });
    }
});

// 生成文件名
function generateFilename(url, prefix, index) {
    try {
        // 尝试从URL中提取文件名
        const urlObj = new URL(url);
        let filename = urlObj.pathname.split('/').pop();
        
        // 如果没有扩展名,添加.jpg
        if (!filename.includes('.')) {
            filename += '.jpg';
        }
        
        // 移除不合法的文件名字符
        filename = filename.replace(/[\\/:*?"<>|]/g, '_');
        
        // 添加前缀
        if (prefix && prefix.trim() !== '') {
            return `${prefix.trim()}_${filename}`;
        }
        
        return filename;
    } catch (e) {
        // 如果URL解析失败,生成一个通用文件名
        return `${prefix || 'image'}_${index}.jpg`;
    }
}

// 发送下载完成通知
function sendNotification(downloaded, failed) {
    chrome.notifications.create({
        type: 'basic',
        iconUrl: 'icon128.png',
        title: '图片下载完成',
        message: `成功下载 ${downloaded} 张图片,${failed} 张失败。`
    });
}    
6.反馈层

当后台脚本判断所有图片都下载完成后,会通过 Chrome 浏览器的 notifications API 向用户发送下载完成的通知,让用户及时了解下载状态,这就是反馈层的功能。

相关推荐
LJianK119 分钟前
Java和JavaScript的&&和||
java·javascript·python
RealmElysia27 分钟前
java反射
java·开发语言
野蛮人6号37 分钟前
黑马点评系列问题之p63unlock.lua不知道怎么整
java·redis·黑马点评
死也不注释40 分钟前
【第一章编辑器开发基础第一节绘制编辑器元素_6滑动条控件(6/7)】
android·编辑器
新酱爱学习43 分钟前
前端海报生成的几种方式:从 Canvas 到 Skyline
前端·javascript·微信小程序
Raners_1 小时前
【Java代码审计(2)】MyBatis XML 注入审计
xml·java·安全·网络安全·mybatis
BillKu1 小时前
Java读取Excel日期内容
java·开发语言·excel
ai小鬼头2 小时前
如何重装旁路由系统并优化AIStarter部署:一步步教程
java·css·github
笑衬人心。2 小时前
Hashtable 与 HashMap 的区别笔记
java·数据结构·笔记
金心靖晨2 小时前
消息中间件优化高手笔记
java·数据库·笔记