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读取缓存的图片数据构造响应返回即可,全程无感知自动执行。
具体实现步骤:
- 配置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>"] } ] }
- 编写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 }, '*'); });
- 编写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的方案:
- 配置
declarativeNetRequest规则,将页面域下指定的SW路径(如/ext-cache-sw.js)重定向到扩展内的web可访问SW脚本 - 内容脚本在页面首次加载时注册该重定向后的SW
- SW内注册fetch事件监听,拦截匹配的图片请求,通过
postMessage和扩展后台通信获取IndexedDB中存储的图片数据,构造Response返回
该方案的缺点是SW首次注册后需要第二次加载页面才能生效,且部分站点对SW注册路径有严格的同源校验,容易被站点CSP策略拦截,适用场景比方案1窄。
内容的提问来源于stack exchange,提问作者Basil
相关产品推荐
相关产品推荐

