background.js中可通过注入脚本的方法实现获取blobUrl实现自定义下载

2025-09-22 00:19:17 64 分享链接 开发笔记 浏览器扩展

manifest.json 添加 scripting 权限才能注入临时脚本

{
    "manifest_version": 3,
    "name": "Blob 下载重命名工具",
    "version": "1.0",
    "permissions": [
        "downloads",
        "scripting",
        "activeTab"
    ],
    "background": {
        "service_worker": "background.js"
    }
}

background.js 监听所有下载并重写方法

chrome.downloads.onCreated.addListener(async (downloadItem) => {
    console.log('检测到新下载:', downloadItem.url);
    
    // 只处理 blob 类型的下载
    if (!downloadItem.url.startsWith('blob:')) {
        return;
    }

    // 1. 先取消当前下载
    chrome.downloads.cancel(downloadItem.id, async () => {
        const cancelError = chrome.runtime.lastError;
        if (cancelError) {
            console.error('取消下载失败:', cancelError);
            return;
        }

        try {
            // 2. 生成新文件名(自定义命名规则)
            const timestamp = new Date().getTime();
            let newFileName = `custom_${timestamp}`;
            
            // 尝试从原文件名提取扩展名
            if (downloadItem.filename) {
                const ext = downloadItem.filename.split('.').pop();
                newFileName = `custom_${timestamp}.${ext}`;
            }

            // 3. 获取当前活跃标签页,用于处理 blob
            const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
            if (!tab) {
                console.error('未找到活跃标签页');
                return;
            }

            // 4. 通过 content script 获取 blob 数据
            const results = await chrome.scripting.executeScript({
                target: { tabId: tab.id },
                func: (blobUrl) => {
                    return new Promise(async (resolve) => {
                        try {
                            // 在页面上下文获取 blob 数据
                            const response = await fetch(blobUrl);
                            const blob = await response.blob();
                            
                            // 转换为可下载的 data URL
                            const fileReader = new FileReader();
                            fileReader.onload = () => {
                                resolve({
                                    dataUrl: fileReader.result,
                                    mimeType: blob.type
                                });
                            };
                            fileReader.readAsDataURL(blob);
                        } catch (error) {
                            console.error('获取 blob 数据失败:', error);
                            resolve(null);
                        }
                    });
                },
                args: [downloadItem.url] // 传递 blob URL 给 content script
            });

            const blobData = results[0].result;
            if (!blobData) {
                console.error('无法获取 blob 数据');
                return;
            }

            // 5. 用新文件名重新下载
            chrome.downloads.download({
                url: blobData.dataUrl, // 使用转换后的 data URL
                filename: newFileName,
                conflictAction: 'uniquify'
            }, (downloadId) => {
                const downloadError = chrome.runtime.lastError;
                if (downloadError) {
                    console.error('重新下载失败:', downloadError);
                } else {
                    console.log(`已用新文件名下载:${newFileName},ID:${downloadId}`);
                }
            });

        } catch (error) {
            console.error('处理 blob 下载时出错:', error);
        }
    });
});

处理 blob 类型的下载并修改文件名需要特殊处理,因为 blob URL 是临时的且仅在创建它的页面上下文有效。以下是实现方案:

实现说明

  1. 核心原理

    • blob URL 只能在创建它的页面上下文中访问,所以需要通过 content script 在页面环境中处理
    • 将 blob 数据转换为 data URL(可以跨上下文使用),再用新文件名重新下载
  2. 关键步骤

    • 检测到 blob 类型下载时先取消原下载
    • 通过 chrome.scripting.executeScript 向当前页面注入临时脚本
    • 在页面上下文中使用 fetch 获取 blob 数据并转换为 data URL
    • 使用转换后的 data URL 和自定义文件名重新发起下载
  3. 自定义文件名

    • 目前使用 custom_时间戳.扩展名 的格式
    • 你可以修改 newFileName 的生成逻辑,例如:

      // 示例:根据原文件名和日期生成
      const date = new Date().toISOString().split('T')[0];
      newFileName = `${date}_${baseName}.${ext}`;
  4. 权限要求

    • 必须添加 scripting 权限才能注入临时脚本
    • activeTab 权限用于获取当前活跃标签页

这种方法可以有效处理 blob 类型的下载并重命名,解决了 blob URL 跨上下文访问的限制。

详细给我讲一下注入临时脚本

在 Chrome 扩展开发中,“注入临时脚本”指的是通过扩展的后台脚本(如 service worker)向当前活跃的网页中动态插入一段 JavaScript 代码,这段代码会在网页的上下文(而非扩展的上下文)中执行。这种技术常用于解决跨上下文访问限制(比如你遇到的 blob URL 访问问题)。

为什么处理 blob 下载需要注入临时脚本?

  • 上下文隔离:Chrome 扩展的后台脚本(background.js)运行在独立的扩展上下文,与网页的上下文完全隔离。
  • blob URL 限制blob: 协议的 URL 是临时的,且仅在创建它的网页上下文内有效,后台脚本无法直接访问。
  • 权限边界:网页内的资源(如 blob 数据)受浏览器安全策略限制,只能在网页自身的上下文内读取。

因此,必须通过“临时脚本注入”的方式,让代码在网页上下文内执行,才能获取 blob 数据并重新处理。

注入临时脚本的完整流程(结合你的需求)

以下是针对“blob 下载重命名”场景的注入脚本实现,包含详细步骤说明:

1. 后台脚本触发注入(background.js

当检测到 blob 类型的下载时,取消原下载,然后向当前网页注入脚本:

chrome.downloads.onCreated.addListener((downloadItem) => {
  console.log('检测到下载:', downloadItem.url);

  // 只处理 blob 类型的下载
  if (!downloadItem.url.startsWith('blob:')) return;

  // 步骤1:取消原下载
  chrome.downloads.cancel(downloadItem.id, () => {
    const cancelError = chrome.runtime.lastError;
    if (cancelError) {
      console.error('取消下载失败:', cancelError);
      return;
    }

    // 步骤2:获取当前活跃标签页(blob 所在的页面)
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      const targetTab = tabs[0];
      if (!targetTab) {
        console.error('未找到活跃标签页');
        return;
      }

      // 步骤3:注入临时脚本到目标标签页
      chrome.scripting.executeScript({
        target: { tabId: targetTab.id }, // 注入到哪个标签页
        func: fetchBlobAndConvert,       // 要注入的函数(核心逻辑)
        args: [downloadItem.url]         // 传递给注入函数的参数(blob URL)
      }).then((results) => {
        // 步骤5:处理注入脚本的返回结果
        if (chrome.runtime.lastError) {
          console.error('注入脚本失败:', chrome.runtime.lastError);
          return;
        }

        // 注入函数的返回值在 results[0].result 中
        const { dataUrl, originalName } = results[0].result;
        if (!dataUrl) {
          console.error('获取 blob 数据失败');
          return;
        }

        // 步骤6:用新文件名重新下载
        const newFileName = `custom_${originalName || 'download_' + Date.now()}.pdf`; // 自定义文件名
        chrome.downloads.download({
          url: dataUrl,
          filename: newFileName,
          conflictAction: 'uniquify'
        }, (newId) => {
          if (chrome.runtime.lastError) {
            console.error('重新下载失败:', chrome.runtime.lastError);
          } else {
            console.log('已用新文件名下载,ID:', newId);
          }
        });
      });
    });
  });
});

2. 注入到网页的临时脚本(核心逻辑)

fetchBlobAndConvert 是要注入到网页的函数,在网页上下文执行,负责读取 blob 数据并转换为可跨上下文使用的 data URL:

// 这个函数会被注入到网页中执行
function fetchBlobAndConvert(blobUrl) {
  return new Promise((resolve) => {
    // 步骤4:在网页上下文内请求 blob 数据
    fetch(blobUrl)
      .then(response => {
        // 尝试从响应头获取原始文件名
        const contentDisposition = response.headers.get('Content-Disposition');
        let originalName = null;
        if (contentDisposition) {
          // 解析文件名(处理浏览器兼容)
          const match = contentDisposition.match(/filename="?([^";]+)"?/);
          if (match) originalName = match[1];
        }

        // 将响应转换为 blob
        return response.blob().then(blob => ({ blob, originalName }));
      })
      .then(({ blob, originalName }) => {
        // 将 blob 转换为 data URL(可在扩展上下文使用)
        const reader = new FileReader();
        reader.onloadend = () => {
          // 转换后的 data URL 格式:data:application/pdf;base64,...
          resolve({ dataUrl: reader.result, originalName });
        };
        reader.readAsDataURL(blob);
      })
      .catch(error => {
        console.error('网页内处理 blob 失败:', error);
        resolve({ dataUrl: null });
      });
  });
}

关键技术点解析

  1. chrome.scripting.executeScript 参数

    • target.tabId:指定注入的目标标签页 ID(必须是当前活跃页,否则可能无权限)。
    • func:要注入的函数(注意:函数体内不能直接引用扩展上下文的变量,只能通过 args 传递参数)。
    • args:传递给注入函数的参数(必须是可序列化的数据,如字符串、数字、简单对象)。
  2. 注入脚本的权限范围

    • 注入的脚本运行在网页上下文,可访问网页的 windowdocument 等对象。
    • 不能直接调用 Chrome 扩展 API(如 chrome.downloads),必须通过返回值将数据传递给后台脚本。
  3. 数据转换逻辑

    • fetch(blobUrl):在网页上下文内请求 blob 数据(此时 URL 有效)。
    • readAsDataURL(blob):将 blob 转换为 data URL(一种可跨上下文传递的 base64 编码字符串)。
  4. 安全性注意

    • 注入的脚本受网页的 CSP(内容安全策略)限制,若网页禁止 eval 或内联脚本,可能需要调整 manifest.jsoncontent_security_policy
    • 避免注入未验证的代码,防止 XSS 风险。

总结

注入临时脚本的核心作用是“突破上下文隔离”,让代码在网页环境中执行以访问受限制的资源(如 blob)。在你的场景中,通过这种方式可以将网页内的 blob 数据转换为扩展可处理的 data URL,最终实现自定义文件名下载。

如果需要更复杂的命名规则(如根据网页标题、时间戳等),只需修改 newFileName 的生成逻辑即可。

background.js中可通过注入脚本的方法实现获取blobUrl实现自定义下载