You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Manifest V3 Chrome扩展如何动态覆盖图片请求响应体

如何在Manifest V3规范的Chrome扩展中使用动态值覆盖图片请求的响应体

问题描述

  • 需求要求覆盖操作在后台自动执行,无需附加调试器,无需用户在页面每次加载时手动触发
  • 扩展核心逻辑为将图片存储到扩展自身的IndexedDB中,客户端发起特定图片请求时,直接使用存储的图片覆盖对应响应体
  • 此前在Firefox Manifest V2环境下,已通过browser.webRequest.onBeforeRequest配合filterResponseData实现重定向到自定义图片数据的效果,对应实现代码如下:
browser.webRequest.onBeforeRequest.addListener(
    (details) => {
        const request = browser.webRequest.filterResponseData(details.requestId);
        request.onstart = async () => {
            request.write(racetrack);
            request.disconnect();
        };
    },
    {
        urls: ['https://www.example.com/image.png'],
    },
    ['requestBody', 'blocking']
);
  • 目前Chrome MV3已废弃browser.webRequest的阻塞式能力,替代的declarativeNetRequest仅支持重定向、修改请求/响应头操作,无法直接修改响应体内容
  • 已尝试的无效方案:
    • 通过xhook编写用户脚本修改DOM图片元素内容,未达到预期效果
    • 直接重定向到data URI或外部图片地址,重定向动作本身执行正常,但目标站点会抛出资源加载错误
  • 目前猜测可能需要通过内容脚本注入页面Service Worker实现,但不确定具体实现流程、SW与扩展侧的通信方案,也不确定该方案是否可落地

实现方案

Chrome MV3环境下没有直接提供修改响应体的原生API,目前有两种可落地的实现路径,优先选择第一种:

方案1:页面早期注入脚本拦截资源加载(实现成本最低,兼容性最好)

不需要注入Service Worker,直接在document_start阶段向页面上下文注入脚本,重写原生fetch方法和HTMLImageElement的src属性访问逻辑,匹配到目标图片URL时,直接从扩展IndexedDB读取缓存的图片数据构造响应返回即可,全程无感知自动执行。

具体实现步骤:

  1. 配置manifest.json权限与资源声明
{
  "manifest_version": 3,
  "permissions": ["scripting"],
  "host_permissions": ["<all_urls>"],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_start"
    }
  ],
  "web_accessible_resources": [
    {
      "resources": ["page-inject.js"],
      "matches": ["<all_urls>"]
    }
  ]
}
  1. 编写content.js,在页面资源加载前注入页面上下文脚本
// content.js
const scriptEl = document.createElement('script');
scriptEl.src = chrome.runtime.getURL('page-inject.js');
scriptEl.onload = () => scriptEl.remove();
(document.head || document.documentElement).prepend(scriptEl);

// 接收页面脚本的图片查询请求,从扩展IndexedDB读取数据返回
window.addEventListener('message', async (event) => {
  if (event.data?.type !== 'REQ_CACHED_IMG') return;
  // 自行实现扩展侧IndexedDB读取逻辑,获取对应URL的图片二进制数据
  const imgBuffer = await getImageFromExtensionIDB(event.data.url);
  event.source.postMessage({
    type: 'RESP_CACHED_IMG',
    requestId: event.data.requestId,
    buffer: imgBuffer
  }, '*');
});
  1. 编写page-inject.js,在页面上下文拦截资源加载逻辑
// page-inject.js
const nativeFetch = window.fetch;
const nativeImgSrcDesc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src');
const pendingRequest = new Map();

// 拦截fetch发起的图片请求
window.fetch = async function(...args) {
  const reqUrl = typeof args[0] === 'string' ? args[0] : args[0]?.url;
  if (isTargetImageUrl(reqUrl)) {
    const imgBlob = await getStoredImage(reqUrl);
    if (imgBlob) return new Response(imgBlob, {
      status: 200,
      headers: { 'Content-Type': imgBlob.type }
    });
  }
  return nativeFetch.apply(this, args);
};

// 拦截img标签的src赋值
Object.defineProperty(HTMLImageElement.prototype, 'src', {
  set(value) {
    const fullUrl = new URL(value, location.href).toString();
    if (isTargetImageUrl(fullUrl)) {
      getStoredImage(fullUrl).then(blob => {
        if (blob) {
          const blobUrl = URL.createObjectURL(blob);
          nativeImgSrcDesc.set.call(this, blobUrl);
          return;
        }
        nativeImgSrcDesc.set.call(this, value);
      });
      return;
    }
    nativeImgSrcDesc.set.call(this, value);
  },
  get: nativeImgSrcDesc.get
});

// 跨上下文获取扩展存储的图片数据
function getStoredImage(url) {
  return new Promise(resolve => {
    const requestId = Math.random().toString(36).slice(2);
    pendingRequest.set(requestId, resolve);
    window.postMessage({ type: 'REQ_CACHED_IMG', url, requestId }, '*');
    // 3秒超时降级,走原请求逻辑
    setTimeout(() => {
      if (pendingRequest.has(requestId)) {
        pendingRequest.delete(requestId);
        resolve(null);
      }
    }, 3000);
  });
}

// 接收扩展返回的图片数据
window.addEventListener('message', (event) => {
  if (event.data?.type !== 'RESP_CACHED_IMG') return;
  const resolve = pendingRequest.get(event.data.requestId);
  if (resolve) {
    pendingRequest.delete(event.data.requestId);
    resolve(event.data.buffer ? new Blob([event.data.buffer], { type: 'image/png' }) : null);
  }
});

// 自行实现目标图片URL的匹配逻辑
function isTargetImageUrl(url) {
  return url === 'https://www.example.com/image.png';
}

注意:必须将内容脚本的注入时机设置为document_start,确保在页面任何资源加载逻辑执行前完成拦截,避免出现资源先加载原地址再被替换的问题。之前重定向data URI/外部地址触发的站点资源校验错误在该方案下不会出现,因为拦截逻辑运行在页面原生上下文,返回的响应和原生请求的响应上下文完全一致,不会被站点安全策略识别为异常资源。

方案2:注入页面Service Worker(适合拦截全类型图片资源的场景)

如果需要拦截非img标签、非fetch发起的图片请求(比如CSS中引用的背景图、iframe内的图片资源),可以采用注入页面Service Worker的方案:

  1. 配置declarativeNetRequest规则,将页面域下指定的SW路径(如/ext-cache-sw.js)重定向到扩展内的web可访问SW脚本
  2. 内容脚本在页面首次加载时注册该重定向后的SW
  3. SW内注册fetch事件监听,拦截匹配的图片请求,通过postMessage和扩展后台通信获取IndexedDB中存储的图片数据,构造Response返回
    该方案的缺点是SW首次注册后需要第二次加载页面才能生效,且部分站点对SW注册路径有严格的同源校验,容易被站点CSP策略拦截,适用场景比方案1窄。

内容的提问来源于stack exchange,提问作者Basil

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 05:36:34