一张跨域图的“四次换乘“:blob URL 与 Chrome 扩展架构里的工程艺术

一张跨域图的"四次换乘":blob URL 与 Chrome 扩展架构里的工程艺术

副标:当 WhatsApp Web 拒绝你的 OSS 图片,blob 是怎么把它"洗成同源"送进 DOM 的


〇 引子:从一行 URL 说起

有一天你在 WhatsApp Web 上跑着一个 Chrome 扩展,DevTools 里看到这样一个图片 src

复制代码
blob:https://web.whatsapp.com/7629f7ca-ca06-45fa-8038-3648281e7710

三个直觉性疑问:

  1. 长得像 URL,但是网络面板里没有任何请求------那图哪来的?
  2. web.whatsapp.com 不是我们的域名,怎么前缀偏偏挂这家?
  3. 我们的图明明在阿里云 OSS,为什么不直接 <img src="https://oss....jpg">

这三个问题分别牵出三块知识:

  • blob URL 机制(浏览器内存里的"门牌号")
  • Same-origin Policy + COEP 沙箱(现代浏览器的安全模型)
  • Chrome MV3 扩展架构(content script / service worker / page world 的三层世界)

这三块拼到一起,才能解释 Chrome 扩展为什么要拐这么大一个弯------本文要讲的就是这场"四次换乘"是如何被设计出来的。


一、Chrome MV3 扩展:你以为的一个"插件",其实是四块独立的代码

很多人以为 Chrome 扩展就是"一段 JS 注入到页面里运行"。错。它是四个互相隔离的执行上下文
#mermaid-svg-5tld75BQzDmG7iMt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5tld75BQzDmG7iMt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5tld75BQzDmG7iMt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5tld75BQzDmG7iMt .error-icon{fill:#552222;}#mermaid-svg-5tld75BQzDmG7iMt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5tld75BQzDmG7iMt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5tld75BQzDmG7iMt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5tld75BQzDmG7iMt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5tld75BQzDmG7iMt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5tld75BQzDmG7iMt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5tld75BQzDmG7iMt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5tld75BQzDmG7iMt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5tld75BQzDmG7iMt .marker.cross{stroke:#333333;}#mermaid-svg-5tld75BQzDmG7iMt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5tld75BQzDmG7iMt p{margin:0;}#mermaid-svg-5tld75BQzDmG7iMt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5tld75BQzDmG7iMt .cluster-label text{fill:#333;}#mermaid-svg-5tld75BQzDmG7iMt .cluster-label span{color:#333;}#mermaid-svg-5tld75BQzDmG7iMt .cluster-label span p{background-color:transparent;}#mermaid-svg-5tld75BQzDmG7iMt .label text,#mermaid-svg-5tld75BQzDmG7iMt span{fill:#333;color:#333;}#mermaid-svg-5tld75BQzDmG7iMt .node rect,#mermaid-svg-5tld75BQzDmG7iMt .node circle,#mermaid-svg-5tld75BQzDmG7iMt .node ellipse,#mermaid-svg-5tld75BQzDmG7iMt .node polygon,#mermaid-svg-5tld75BQzDmG7iMt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5tld75BQzDmG7iMt .rough-node .label text,#mermaid-svg-5tld75BQzDmG7iMt .node .label text,#mermaid-svg-5tld75BQzDmG7iMt .image-shape .label,#mermaid-svg-5tld75BQzDmG7iMt .icon-shape .label{text-anchor:middle;}#mermaid-svg-5tld75BQzDmG7iMt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5tld75BQzDmG7iMt .rough-node .label,#mermaid-svg-5tld75BQzDmG7iMt .node .label,#mermaid-svg-5tld75BQzDmG7iMt .image-shape .label,#mermaid-svg-5tld75BQzDmG7iMt .icon-shape .label{text-align:center;}#mermaid-svg-5tld75BQzDmG7iMt .node.clickable{cursor:pointer;}#mermaid-svg-5tld75BQzDmG7iMt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5tld75BQzDmG7iMt .arrowheadPath{fill:#333333;}#mermaid-svg-5tld75BQzDmG7iMt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5tld75BQzDmG7iMt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5tld75BQzDmG7iMt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5tld75BQzDmG7iMt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5tld75BQzDmG7iMt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5tld75BQzDmG7iMt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5tld75BQzDmG7iMt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5tld75BQzDmG7iMt .cluster text{fill:#333;}#mermaid-svg-5tld75BQzDmG7iMt .cluster span{color:#333;}#mermaid-svg-5tld75BQzDmG7iMt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5tld75BQzDmG7iMt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5tld75BQzDmG7iMt rect.text{fill:none;stroke-width:0;}#mermaid-svg-5tld75BQzDmG7iMt .icon-shape,#mermaid-svg-5tld75BQzDmG7iMt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5tld75BQzDmG7iMt .icon-shape p,#mermaid-svg-5tld75BQzDmG7iMt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5tld75BQzDmG7iMt .icon-shape .label rect,#mermaid-svg-5tld75BQzDmG7iMt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5tld75BQzDmG7iMt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5tld75BQzDmG7iMt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5tld75BQzDmG7iMt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Chrome 扩展进程拓扑
chrome.runtime.sendMessage
chrome.runtime.sendMessage
共享 DOM
script 注入
真实 window
window.postMessage
Background

Service Worker

无 DOM·按需唤醒
Content Script

共享页面 DOM

但独立 JS 全局
Popup / Options Page

独立 HTML
宿主页面

web.whatsapp.com
Page Script / MAIN world

页面真实 window

四块的能力差异:

上下文 跑在哪 能不能跨域 fetch 能不能访问 DOM 能不能访问 page.window
Background SW 扩展进程 ✅(带 host_permissions) ❌ 无 DOM
Content Script 页面进程 ⚠️ 受 CSP/CORS 影响 ❌(隔离世界)
Page Script 页面进程 ⚠️ 受 CSP/CORS 影响
Popup 扩展页 自己的 DOM

关键含义 :跨域加载图片这件事,只有 Background SW 能干------content script 在 web.whatsapp.com 上,受同源策略 + COEP 双重压制,根本拿不到 OSS 资源。


二、拦路虎一号:同源策略(SOP)与 CORS

最古早的浏览器安全模型:
#mermaid-svg-ar1P9ZqMte5LZ4ra{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ar1P9ZqMte5LZ4ra .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ar1P9ZqMte5LZ4ra .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ar1P9ZqMte5LZ4ra .error-icon{fill:#552222;}#mermaid-svg-ar1P9ZqMte5LZ4ra .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ar1P9ZqMte5LZ4ra .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ar1P9ZqMte5LZ4ra .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ar1P9ZqMte5LZ4ra .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ar1P9ZqMte5LZ4ra .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ar1P9ZqMte5LZ4ra .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ar1P9ZqMte5LZ4ra .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ar1P9ZqMte5LZ4ra .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ar1P9ZqMte5LZ4ra .marker.cross{stroke:#333333;}#mermaid-svg-ar1P9ZqMte5LZ4ra svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ar1P9ZqMte5LZ4ra p{margin:0;}#mermaid-svg-ar1P9ZqMte5LZ4ra .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ar1P9ZqMte5LZ4ra .cluster-label text{fill:#333;}#mermaid-svg-ar1P9ZqMte5LZ4ra .cluster-label span{color:#333;}#mermaid-svg-ar1P9ZqMte5LZ4ra .cluster-label span p{background-color:transparent;}#mermaid-svg-ar1P9ZqMte5LZ4ra .label text,#mermaid-svg-ar1P9ZqMte5LZ4ra span{fill:#333;color:#333;}#mermaid-svg-ar1P9ZqMte5LZ4ra .node rect,#mermaid-svg-ar1P9ZqMte5LZ4ra .node circle,#mermaid-svg-ar1P9ZqMte5LZ4ra .node ellipse,#mermaid-svg-ar1P9ZqMte5LZ4ra .node polygon,#mermaid-svg-ar1P9ZqMte5LZ4ra .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ar1P9ZqMte5LZ4ra .rough-node .label text,#mermaid-svg-ar1P9ZqMte5LZ4ra .node .label text,#mermaid-svg-ar1P9ZqMte5LZ4ra .image-shape .label,#mermaid-svg-ar1P9ZqMte5LZ4ra .icon-shape .label{text-anchor:middle;}#mermaid-svg-ar1P9ZqMte5LZ4ra .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ar1P9ZqMte5LZ4ra .rough-node .label,#mermaid-svg-ar1P9ZqMte5LZ4ra .node .label,#mermaid-svg-ar1P9ZqMte5LZ4ra .image-shape .label,#mermaid-svg-ar1P9ZqMte5LZ4ra .icon-shape .label{text-align:center;}#mermaid-svg-ar1P9ZqMte5LZ4ra .node.clickable{cursor:pointer;}#mermaid-svg-ar1P9ZqMte5LZ4ra .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ar1P9ZqMte5LZ4ra .arrowheadPath{fill:#333333;}#mermaid-svg-ar1P9ZqMte5LZ4ra .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ar1P9ZqMte5LZ4ra .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ar1P9ZqMte5LZ4ra .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ar1P9ZqMte5LZ4ra .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ar1P9ZqMte5LZ4ra .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ar1P9ZqMte5LZ4ra .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ar1P9ZqMte5LZ4ra .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ar1P9ZqMte5LZ4ra .cluster text{fill:#333;}#mermaid-svg-ar1P9ZqMte5LZ4ra .cluster span{color:#333;}#mermaid-svg-ar1P9ZqMte5LZ4ra div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ar1P9ZqMte5LZ4ra .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ar1P9ZqMte5LZ4ra rect.text{fill:none;stroke-width:0;}#mermaid-svg-ar1P9ZqMte5LZ4ra .icon-shape,#mermaid-svg-ar1P9ZqMte5LZ4ra .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ar1P9ZqMte5LZ4ra .icon-shape p,#mermaid-svg-ar1P9ZqMte5LZ4ra .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ar1P9ZqMte5LZ4ra .icon-shape .label rect,#mermaid-svg-ar1P9ZqMte5LZ4ra .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ar1P9ZqMte5LZ4ra .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ar1P9ZqMte5LZ4ra .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ar1P9ZqMte5LZ4ra :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} fetch('https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Foss.aliyuncs.com%2Fa.jpg\&pos_id=img-NiASEo3s-1781143098785)')
OSS 没回 Access-Control-Allow-Origin
OSS 配了 ACAO: *
页面 origin = web.whatsapp.com
CORS 检查
请求发出,但响应不可读
fetch 成功

<img src=...>fetch 宽松得多------图片标签是"不可读但可显示"的:浏览器会请求并渲染,只是 JS 无法通过 canvas 读它的像素。

所以早年 Chrome 扩展加跨域图,往往可以直接 <img src="https://oss.../a.jpg">,渲染没问题。

直到 COEP 出现。


三、拦路虎二号:COEP/CORP------后 Spectre 时代的新沙箱

2018 年 Spectre 爆出后,浏览器收紧了"跨域资源能不能进我的进程"。WhatsApp Web 启用了:

http 复制代码
Cross-Origin-Embedder-Policy: require-corp

含义:当前文档加载的所有跨域资源,必须显式声明 Cross-Origin-Resource-Policy: cross-origin,否则浏览器拒绝把它放进当前页面的进程。
#mermaid-svg-kFlqBTrwRasM3oY7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kFlqBTrwRasM3oY7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kFlqBTrwRasM3oY7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kFlqBTrwRasM3oY7 .error-icon{fill:#552222;}#mermaid-svg-kFlqBTrwRasM3oY7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kFlqBTrwRasM3oY7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kFlqBTrwRasM3oY7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kFlqBTrwRasM3oY7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kFlqBTrwRasM3oY7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kFlqBTrwRasM3oY7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kFlqBTrwRasM3oY7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kFlqBTrwRasM3oY7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kFlqBTrwRasM3oY7 .marker.cross{stroke:#333333;}#mermaid-svg-kFlqBTrwRasM3oY7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kFlqBTrwRasM3oY7 p{margin:0;}#mermaid-svg-kFlqBTrwRasM3oY7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kFlqBTrwRasM3oY7 .cluster-label text{fill:#333;}#mermaid-svg-kFlqBTrwRasM3oY7 .cluster-label span{color:#333;}#mermaid-svg-kFlqBTrwRasM3oY7 .cluster-label span p{background-color:transparent;}#mermaid-svg-kFlqBTrwRasM3oY7 .label text,#mermaid-svg-kFlqBTrwRasM3oY7 span{fill:#333;color:#333;}#mermaid-svg-kFlqBTrwRasM3oY7 .node rect,#mermaid-svg-kFlqBTrwRasM3oY7 .node circle,#mermaid-svg-kFlqBTrwRasM3oY7 .node ellipse,#mermaid-svg-kFlqBTrwRasM3oY7 .node polygon,#mermaid-svg-kFlqBTrwRasM3oY7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kFlqBTrwRasM3oY7 .rough-node .label text,#mermaid-svg-kFlqBTrwRasM3oY7 .node .label text,#mermaid-svg-kFlqBTrwRasM3oY7 .image-shape .label,#mermaid-svg-kFlqBTrwRasM3oY7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-kFlqBTrwRasM3oY7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kFlqBTrwRasM3oY7 .rough-node .label,#mermaid-svg-kFlqBTrwRasM3oY7 .node .label,#mermaid-svg-kFlqBTrwRasM3oY7 .image-shape .label,#mermaid-svg-kFlqBTrwRasM3oY7 .icon-shape .label{text-align:center;}#mermaid-svg-kFlqBTrwRasM3oY7 .node.clickable{cursor:pointer;}#mermaid-svg-kFlqBTrwRasM3oY7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kFlqBTrwRasM3oY7 .arrowheadPath{fill:#333333;}#mermaid-svg-kFlqBTrwRasM3oY7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kFlqBTrwRasM3oY7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kFlqBTrwRasM3oY7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kFlqBTrwRasM3oY7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kFlqBTrwRasM3oY7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kFlqBTrwRasM3oY7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kFlqBTrwRasM3oY7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kFlqBTrwRasM3oY7 .cluster text{fill:#333;}#mermaid-svg-kFlqBTrwRasM3oY7 .cluster span{color:#333;}#mermaid-svg-kFlqBTrwRasM3oY7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kFlqBTrwRasM3oY7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kFlqBTrwRasM3oY7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-kFlqBTrwRasM3oY7 .icon-shape,#mermaid-svg-kFlqBTrwRasM3oY7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kFlqBTrwRasM3oY7 .icon-shape p,#mermaid-svg-kFlqBTrwRasM3oY7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kFlqBTrwRasM3oY7 .icon-shape .label rect,#mermaid-svg-kFlqBTrwRasM3oY7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kFlqBTrwRasM3oY7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kFlqBTrwRasM3oY7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kFlqBTrwRasM3oY7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} OSS 响应头没有 CORP
OSS 响应头 CORP: cross-origin

COEP=require-corp 检查
❌ 阻断

NS_ERROR_DOM_CORP_FAILED
✅ 渲染
COEP 检查

blob origin = 当前 origin
✅ 同源,直接放行

我们的 OSS 桶没配 CORP (也不该配,是 WhatsApp 单方面强加的标准)。所以直接 <img src="oss..."> 在 WhatsApp Web 里全部挂掉

这才是"为什么要绕一圈"的根因------不是 CORS,不是 SOP,是 COEP。


四、blob URL:浏览器内存里的"门牌号"

4.1 它长什么样、是什么

复制代码
blob:                                    ← URL scheme(伪协议)
https://web.whatsapp.com/                ← origin(创建者文档的 origin)
7629f7ca-ca06-45fa-8038-3648281e7710     ← UUID(指向内存里的 Blob)

它不是网络 URL ------浏览器看到 blob: scheme 不会发起任何请求,而是直接去文档绑定的 Blob URL Store (一张 <UUID, Blob 引用> 的 map)里查内容。

4.2 URL.createObjectURL 内部三步

浏览器堆(Blob 字节) documentBlob URL Store JavaScript 浏览器堆(Blob 字节) documentBlob URL Store JavaScript #mermaid-svg-dpXVnP83USTfUtN0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dpXVnP83USTfUtN0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dpXVnP83USTfUtN0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dpXVnP83USTfUtN0 .error-icon{fill:#552222;}#mermaid-svg-dpXVnP83USTfUtN0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dpXVnP83USTfUtN0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dpXVnP83USTfUtN0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dpXVnP83USTfUtN0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dpXVnP83USTfUtN0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dpXVnP83USTfUtN0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dpXVnP83USTfUtN0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dpXVnP83USTfUtN0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dpXVnP83USTfUtN0 .marker.cross{stroke:#333333;}#mermaid-svg-dpXVnP83USTfUtN0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dpXVnP83USTfUtN0 p{margin:0;}#mermaid-svg-dpXVnP83USTfUtN0 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dpXVnP83USTfUtN0 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-dpXVnP83USTfUtN0 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dpXVnP83USTfUtN0 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-dpXVnP83USTfUtN0 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-dpXVnP83USTfUtN0 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-dpXVnP83USTfUtN0 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-dpXVnP83USTfUtN0 .sequenceNumber{fill:white;}#mermaid-svg-dpXVnP83USTfUtN0 #sequencenumber{fill:#333;}#mermaid-svg-dpXVnP83USTfUtN0 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-dpXVnP83USTfUtN0 .messageText{fill:#333;stroke:none;}#mermaid-svg-dpXVnP83USTfUtN0 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dpXVnP83USTfUtN0 .labelText,#mermaid-svg-dpXVnP83USTfUtN0 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-dpXVnP83USTfUtN0 .loopText,#mermaid-svg-dpXVnP83USTfUtN0 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-dpXVnP83USTfUtN0 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dpXVnP83USTfUtN0 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-dpXVnP83USTfUtN0 .noteText,#mermaid-svg-dpXVnP83USTfUtN0 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-dpXVnP83USTfUtN0 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dpXVnP83USTfUtN0 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dpXVnP83USTfUtN0 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dpXVnP83USTfUtN0 .actorPopupMenu{position:absolute;}#mermaid-svg-dpXVnP83USTfUtN0 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-dpXVnP83USTfUtN0 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dpXVnP83USTfUtN0 .actor-man circle,#mermaid-svg-dpXVnP83USTfUtN0 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-dpXVnP83USTfUtN0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} new Blob(bytes, {type})Blob 引用URL.createObjectURL(blob)1. 生成 UUID2. 注册 <UUID → Blob>'blob:<origin>/<UUID>'

注意:注册的是引用,不复制数据 。Blob 还在内存中,URL 就有效;map 里那一条被 revokeObjectURL 删除,URL 立刻失效。

4.3 origin 锁定规则

blob URL 的 origin = 调用 createObjectURL 时所在 document 的 origin

所以即便扩展是我们写的,只要 createObjectURL 在 WhatsApp Web 主页面调用,前缀就永远是 blob:https://web.whatsapp.com/

这是好事 :正因为 origin 是 web.whatsapp.com,COEP 把它当同源资源,直接放行

也是坏事(隐含约束):

操作 能否成功
content script 里创建 → content script 里渲染
content script 里创建 → 复制到另一个 tab ❌ origin 不属于那个 document
在 background SW 里 URL.createObjectURL ⚠️ SW 没有 document,只能拿到 SW 域内的 blob URL,content script 用不了

这就回答了开篇第三个疑问:为什么不让 background 直接返回 blob URL? 因为它会带 SW 的 origin,传到页面就废了。只有 content script 能"代页面创建" blob URL。

4.4 data: vs blob::内存模型对比

维度 data:image/jpeg;base64,... blob:https://.../UUID
体积膨胀 base64 多 33%,字符串常驻 V8 堆 二进制 1:1 存 Blob 池
DOM 体积 每个 src 几百 KB 永远 ~50 字节 UUID
DevTools 大列表会卡死 完全不卡
内存释放 字符串引用计数,难精控 revokeObjectURL 立即释放
解码缓存 每个 <img> 重新解码 引擎认 UUID,复用解码结果
序列化 可 JSON.stringify 不可(UUID 是本地的)
跨上下文传递 ✅ 可以 ❌ 不可以

结论 :跨上下文(SW ↔ CS)只能传 base64 / data URL;进了 DOM 之前必须转成 blob URL。


五、四次换乘:把这些拼到一起

一张 OSS 图从源头到屏幕,要经历四个上下文:

每一次"换乘"解决一个具体问题:

步骤 解决的问题 关键技术
① CS → SW content script 受 COEP 阻断,自己 fetch 不到 chrome.runtime.sendMessage
② SW → CS SW 拿到二进制后必须传回页面世界 base64 序列化
③ CS 内 base64 → blob data URL 喂 DOM 太重;blob URL origin 锁定为 web.whatsapp.com URL.createObjectURL
④ DOM 渲染 blob 的 origin 与文档同源,直接绕过 COEP 浏览器原生机制

核心代码就这十来行useProxiedImage.ts 第 71--82 行):

ts 复制代码
function dataUrlToBlobUrl(dataUrl: string): string {
  const [header, data] = dataUrl.split(',');
  const mime = header.match(/:(.*?);/)?.[1] || 'image/jpeg';
  const binary = atob(data);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  const blob = new Blob([bytes], { type: mime });
  return URL.createObjectURL(blob);
}

10 行代码背后,是 SOP / CORS / COEP / MV3 隔离 / Blob URL Store 五个机制的协同。


六、不为人知的边界与坑

6.1 blob URL 不会自动回收

#mermaid-svg-Emg4dreDJNtro4BD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Emg4dreDJNtro4BD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Emg4dreDJNtro4BD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Emg4dreDJNtro4BD .error-icon{fill:#552222;}#mermaid-svg-Emg4dreDJNtro4BD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Emg4dreDJNtro4BD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Emg4dreDJNtro4BD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Emg4dreDJNtro4BD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Emg4dreDJNtro4BD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Emg4dreDJNtro4BD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Emg4dreDJNtro4BD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Emg4dreDJNtro4BD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Emg4dreDJNtro4BD .marker.cross{stroke:#333333;}#mermaid-svg-Emg4dreDJNtro4BD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Emg4dreDJNtro4BD p{margin:0;}#mermaid-svg-Emg4dreDJNtro4BD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Emg4dreDJNtro4BD .cluster-label text{fill:#333;}#mermaid-svg-Emg4dreDJNtro4BD .cluster-label span{color:#333;}#mermaid-svg-Emg4dreDJNtro4BD .cluster-label span p{background-color:transparent;}#mermaid-svg-Emg4dreDJNtro4BD .label text,#mermaid-svg-Emg4dreDJNtro4BD span{fill:#333;color:#333;}#mermaid-svg-Emg4dreDJNtro4BD .node rect,#mermaid-svg-Emg4dreDJNtro4BD .node circle,#mermaid-svg-Emg4dreDJNtro4BD .node ellipse,#mermaid-svg-Emg4dreDJNtro4BD .node polygon,#mermaid-svg-Emg4dreDJNtro4BD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Emg4dreDJNtro4BD .rough-node .label text,#mermaid-svg-Emg4dreDJNtro4BD .node .label text,#mermaid-svg-Emg4dreDJNtro4BD .image-shape .label,#mermaid-svg-Emg4dreDJNtro4BD .icon-shape .label{text-anchor:middle;}#mermaid-svg-Emg4dreDJNtro4BD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Emg4dreDJNtro4BD .rough-node .label,#mermaid-svg-Emg4dreDJNtro4BD .node .label,#mermaid-svg-Emg4dreDJNtro4BD .image-shape .label,#mermaid-svg-Emg4dreDJNtro4BD .icon-shape .label{text-align:center;}#mermaid-svg-Emg4dreDJNtro4BD .node.clickable{cursor:pointer;}#mermaid-svg-Emg4dreDJNtro4BD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Emg4dreDJNtro4BD .arrowheadPath{fill:#333333;}#mermaid-svg-Emg4dreDJNtro4BD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Emg4dreDJNtro4BD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Emg4dreDJNtro4BD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Emg4dreDJNtro4BD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Emg4dreDJNtro4BD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Emg4dreDJNtro4BD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Emg4dreDJNtro4BD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Emg4dreDJNtro4BD .cluster text{fill:#333;}#mermaid-svg-Emg4dreDJNtro4BD .cluster span{color:#333;}#mermaid-svg-Emg4dreDJNtro4BD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Emg4dreDJNtro4BD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Emg4dreDJNtro4BD rect.text{fill:none;stroke-width:0;}#mermaid-svg-Emg4dreDJNtro4BD .icon-shape,#mermaid-svg-Emg4dreDJNtro4BD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Emg4dreDJNtro4BD .icon-shape p,#mermaid-svg-Emg4dreDJNtro4BD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Emg4dreDJNtro4BD .icon-shape .label rect,#mermaid-svg-Emg4dreDJNtro4BD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Emg4dreDJNtro4BD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Emg4dreDJNtro4BD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Emg4dreDJNtro4BD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} blob: URL
失效条件
document 卸载

页面关闭/刷新
手动调用

URL.revokeObjectURL
Blob 对象 + Store 引用

同时不可达
整张 Store 一次性清空
只删那条 UUID
GC 时机不可控

*如果想严控内存,需要:

  1. 给 cache 加 LRU 上限
  2. 淘汰时调用 URL.revokeObjectURL(oldUrl)
  3. 或者改用 chrome.storage / caches API 持久化

6.2 Service Worker 的"短寿命"问题

MV3 的 SW 是事件驱动 + 30 秒空闲卸载。如果在 SW 里:

ts 复制代码
// ❌ 反模式
const swBlobCache = new Map();   // SW 一卸载就没了

所以缓存层只能放在 content script------只要页面还开着,CS 就还在。

6.3 跨上下文不能传 blob URL

ts 复制代码
// content script
const blobUrl = URL.createObjectURL(blob);
chrome.runtime.sendMessage({ url: blobUrl });   // ⚠️ background 收到也用不了

// background
const blobUrl = URL.createObjectURL(blob);
chrome.tabs.sendMessage(tabId, { url: blobUrl }); // ⚠️ content script 收到也用不了

跨上下文传二进制只有三条路:

  1. base64 / data URL(简单,体积 +33%)
  2. Transferable ArrayBuffer(高效但要 worker 通信,扩展消息通道不支持)
  3. 建立 MessageChannel(成本最高)

本项目走的是 ①------简单可靠。

6.4 web_accessible_resources 是另一回事

很多人会把 web_accessible_resources 和 blob URL 混淆。它解决的是"页面能不能访问扩展打包的静态资源",比如本项目 wxt.config.ts 里:

ts 复制代码
web_accessible_resources: [
  { resources: ['wppconnect-wa.js', 'page-wpp.js', 'assets/*'],
    matches: ['https://web.whatsapp.com/*'] },
]

这让页面能用 chrome-extension://<id>/page-wpp.js 路径取到扩展资源------和 blob URL 是平行的两套机制。


七、blob 的其他妙用(举一反三)

blob URL 不止用来加跨域图,类似场景还有:

场景 用法
前端导出 CSV / PDF / 图片下载 URL.createObjectURL(blob)<a download>
Web Worker 的 inline 创建 new Worker(URL.createObjectURL(new Blob([code], {type:'application/javascript'})))
视频流播放 MediaSource API + URL.createObjectURL
本地图片预览(File API) <input type=file> → File 对象 → createObjectURL
Service Worker 拦截后再生 Workbox 缓存 → blob → 喂 fetch handler
富文本/邮件正文里的内联图 base64 内联 vs blob 引用

判断什么时候用 blob 的口诀

"需要给 DOM 一个 src,但内容来自 JS 内存(fetch / 解密 / 拼接 / WebRTC),且不希望走网络也不希望塞 base64"------用 blob。


八、设计反思:什么时候不该用 blob

不是所有跨域图都得这么折腾。先问三个问题再决定要不要绕代理
#mermaid-svg-yVPE04XAbfqJrMYH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yVPE04XAbfqJrMYH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yVPE04XAbfqJrMYH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yVPE04XAbfqJrMYH .error-icon{fill:#552222;}#mermaid-svg-yVPE04XAbfqJrMYH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yVPE04XAbfqJrMYH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yVPE04XAbfqJrMYH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yVPE04XAbfqJrMYH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yVPE04XAbfqJrMYH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yVPE04XAbfqJrMYH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yVPE04XAbfqJrMYH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yVPE04XAbfqJrMYH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yVPE04XAbfqJrMYH .marker.cross{stroke:#333333;}#mermaid-svg-yVPE04XAbfqJrMYH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yVPE04XAbfqJrMYH p{margin:0;}#mermaid-svg-yVPE04XAbfqJrMYH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yVPE04XAbfqJrMYH .cluster-label text{fill:#333;}#mermaid-svg-yVPE04XAbfqJrMYH .cluster-label span{color:#333;}#mermaid-svg-yVPE04XAbfqJrMYH .cluster-label span p{background-color:transparent;}#mermaid-svg-yVPE04XAbfqJrMYH .label text,#mermaid-svg-yVPE04XAbfqJrMYH span{fill:#333;color:#333;}#mermaid-svg-yVPE04XAbfqJrMYH .node rect,#mermaid-svg-yVPE04XAbfqJrMYH .node circle,#mermaid-svg-yVPE04XAbfqJrMYH .node ellipse,#mermaid-svg-yVPE04XAbfqJrMYH .node polygon,#mermaid-svg-yVPE04XAbfqJrMYH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yVPE04XAbfqJrMYH .rough-node .label text,#mermaid-svg-yVPE04XAbfqJrMYH .node .label text,#mermaid-svg-yVPE04XAbfqJrMYH .image-shape .label,#mermaid-svg-yVPE04XAbfqJrMYH .icon-shape .label{text-anchor:middle;}#mermaid-svg-yVPE04XAbfqJrMYH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yVPE04XAbfqJrMYH .rough-node .label,#mermaid-svg-yVPE04XAbfqJrMYH .node .label,#mermaid-svg-yVPE04XAbfqJrMYH .image-shape .label,#mermaid-svg-yVPE04XAbfqJrMYH .icon-shape .label{text-align:center;}#mermaid-svg-yVPE04XAbfqJrMYH .node.clickable{cursor:pointer;}#mermaid-svg-yVPE04XAbfqJrMYH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yVPE04XAbfqJrMYH .arrowheadPath{fill:#333333;}#mermaid-svg-yVPE04XAbfqJrMYH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yVPE04XAbfqJrMYH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yVPE04XAbfqJrMYH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yVPE04XAbfqJrMYH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yVPE04XAbfqJrMYH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yVPE04XAbfqJrMYH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yVPE04XAbfqJrMYH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yVPE04XAbfqJrMYH .cluster text{fill:#333;}#mermaid-svg-yVPE04XAbfqJrMYH .cluster span{color:#333;}#mermaid-svg-yVPE04XAbfqJrMYH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yVPE04XAbfqJrMYH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yVPE04XAbfqJrMYH rect.text{fill:none;stroke-width:0;}#mermaid-svg-yVPE04XAbfqJrMYH .icon-shape,#mermaid-svg-yVPE04XAbfqJrMYH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yVPE04XAbfqJrMYH .icon-shape p,#mermaid-svg-yVPE04XAbfqJrMYH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yVPE04XAbfqJrMYH .icon-shape .label rect,#mermaid-svg-yVPE04XAbfqJrMYH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yVPE04XAbfqJrMYH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yVPE04XAbfqJrMYH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yVPE04XAbfqJrMYH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否


不能
没有

宿主页面

有 COEP=require-corp?
直接

啥都不用做
后端 OSS

能改 CORP 头?
加 CORP: cross-origin

这是首选 ⭐
扩展有

host_permissions?
改不了,劝退
走 blob 代理方案

本文方案

优先级:改后端 > 改 OSS Bucket 配置 > 走代理。

代理方案的代价:

  • 多一跳消息往返(content → SW → OSS)
  • 内存里多一份 Blob
  • 缓存策略要自己写
  • 可调试性变差(DevTools Network 里看不到原始请求)

只有当后端/OSS 配置不可控时,才该祭出 blob 大杀器。


九、写在最后:blob 是浏览器送给扩展开发者的"借尸还魂"

回看四次换乘,你会发现 blob URL 这个机制有一种几何美感

它不是为了解决跨域设计的,只是恰好解决了------因为它的 origin 锁定规则刚好让"内容来自远端,但呈现给页面时披着同源外衣"成为可能。

Chrome 扩展开发者借着这个"恰好",构造出整套跨域代理范式:

  • SW 解 COEP(用 host_permissions 越过 web 安全模型)
  • base64 解上下文隔离(用文本越过隔离世界)
  • blob 解 DOM 沙箱(用同源外衣越过 COEP)

三个机制叠在一起,才把一张 OSS 图送进了 WhatsApp Web 的 DOM。


TL;DR

blob:https://web.whatsapp.com/UUID = 浏览器内存里 Blob 对象的本地引用,origin 锁定为创建时的 document。在 Chrome MV3 扩展里,它是穿透 COEP 沙箱的关键------SW 拿数据、CS 转 blob、DOM 同源渲染------三个上下文协奏完成一次跨域图加载。
这不是黑魔法,这是 Web 平台的几个独立机制恰巧拼出的一条工程通路。理解它们的边界,比记住"怎么写"更重要。

相关推荐
程序员黑豆1 小时前
AI全栈开发 - Java:基本数据类型 vs 引用数据类型的内存存储
java·前端·ai编程
FserSuN1 小时前
Chrome CORS / PNA / LNA 问题排查与解决方案
前端·chrome
小小小小宇1 小时前
Claude Code 自动运行方法大全
前端
道友可好1 小时前
AI 测试全绿,代码却是错的
前端·人工智能·后端
国科安芯1 小时前
商业航天通信载荷数字处理单元供电架构研究——基于ASP7A84AS的高精度低压差线性稳压器技术分析
前端·单片机·嵌入式硬件·fpga开发·架构·安全性测试
zwh12984540602 小时前
【 Fast-DDS 源码分析(一):架构总览与模块介绍】
中间件·架构
春天花会开1312 小时前
影像上传前置机网络架构设计模板(含VPN)
后端·架构
hypoy2 小时前
先拷问,再开工:grill-me + Trellis 重塑我的 Claude Code 工作流
架构·ai编程