前端技巧:用 Bookmarklet 给网页临时挂载一个图片调试面板

17 阅读4分钟

前端技巧:用 Bookmarklet 给网页临时挂载一个图片调试面板

在前端开发中,我们经常需要分析页面中的资源情况,比如:

  • 页面实际加载了哪些图片
  • 不同分辨率资源的分布
  • 是否存在重复图片请求
  • UI 还原时如何快速定位原始素材

这些事情当然可以通过 DevTools 完成,但在某些场景下效率并不高。例如批量浏览图片、筛选大图、快速预览等操作,都需要在多个面板之间来回切换。

于是可以换一个思路:

不扩展 DevTools,而是用一段 Bookmarklet,在任意网页上临时挂载一个“图片调试面板”。

这篇文章的重点不在工具本身,而在于这种实现方式背后的前端技术思路。

image.png


一、为什么是 Bookmarklet

Bookmarklet 的本质,是一段运行在当前页面上下文中的 JavaScript。

javascript:(()=>{ /* your code */ })()

它有几个非常关键的特性:

  • 直接运行在页面环境中,可以访问 DOM
  • 不需要构建或发布浏览器插件
  • 无需侵入页面代码
  • 可以在任意网站使用

这使它非常适合做“临时调试能力注入”。

可以把它理解为:

一种轻量级的“运行时工具扩展机制”


二、核心问题:如何获取真实图片资源

最直接的方式是:

[...document.images]

但这只是第一步,真正需要解决的是两个问题:

1. 图片是否已经加载完成

i.naturalWidth

只有当图片完成加载后,naturalWidthnaturalHeight 才是有效的。


2. 如何获取真实资源地址

i.currentSrc || i.src

这里的关键是 currentSrc

在响应式图片场景中:

<img src="small.jpg" srcset="large.jpg 2x">

浏览器实际使用的资源并不一定是 src,而是 currentSrc

如果忽略这一点,拿到的数据很可能是不准确的。


三、数据建模:不仅仅是收集

收集图片之后,需要对数据做一层结构化处理:

{
  s: src,
  w: width,
  h: height,
  m: max(width, height)
}

这里的关键字段是:

  • w / h:用于展示尺寸信息
  • m:用于排序和筛选

为什么使用最大边?

因为在实际使用中:

  • 横图和竖图不好直接比较
  • 最大边可以作为统一尺度
  • 更适合做“是否为大图”的判断

四、去重策略:Map 比 Set 更合适

const map = new Map()
imgs.forEach(i => !map.has(i.s) && map.set(i.s, i))

这里选择 Map 而不是 Set 的原因是:

  • 去重依据是 URL
  • 但我们需要保留完整对象
  • Map 可以同时解决“唯一性 + 数据存储”

这是一个很典型的前端数据处理模式。


五、筛选与排序:面向使用场景设计

base.filter(i => !v || i.m >= v)

筛选逻辑围绕一个实际需求:

快速找到大图资源

配合排序:

first ? b.m - a.m : a.m - b.m

首屏优先展示大图,可以显著提升信息获取效率。

这其实是一个“数据展示策略”的问题,而不仅仅是代码实现。


六、为什么在新窗口中渲染 UI

const w = open()
w.document.write(...)

这是整个实现中一个很关键的设计点。

如果直接在当前页面插入 UI,会遇到几个问题:

  • 样式冲突(CSS 污染)
  • z-index 竞争
  • 可能被页面脚本影响

而新窗口的优势是:

  • 完全隔离运行环境
  • 样式可控
  • 生命周期独立

可以理解为:

用浏览器原生能力实现了一种“轻量沙箱”

七、实现代码

完整实现如下(Bookmarklet 版本):

javascript:(()=>{const imgs=[...document.images].filter(i=>i.naturalWidth).map(i=>({s:i.currentSrc||i.src,w:i.naturalWidth,h:i.naturalHeight,m:Math.max(i.naturalWidth,i.naturalHeight)}));const map=new Map();imgs.forEach(i=>!map.has(i.s)&&map.set(i.s,i));const base=[...map.values()];const sizes=[...new Set(base.map(i=>i.m))].sort((a,b)=>b-a);const w=open();w.document.write(`<!doctype html><meta charset=utf-8><title>页面图片资源(${base.length})</title><style>*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto;background:#0f1115;color:#e6e6e6}header{position:sticky;top:0;z-index:10;background:#161a20;padding:12px 16px;font-size:14px;display:flex;gap:12px;align-items:center;box-shadow:0 6px 20px rgba(0,0,0,.4)}select{margin-left:auto;background:#0f1115;color:#e6e6e6;border:1px solid #333;border-radius:6px;padding:4px 8px;font-size:12px}.grid{padding:16px;display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px}.card{background:#161a20;border-radius:12px;overflow:hidden;position:relative;transition:transform .18s ease-out,opacity .18s ease-out}.card.enter{transform:scale(.96);opacity:0}.card img{width:100%;height:180px;object-fit:contain;background:#0b0d11;cursor:zoom-in}.badge{position:absolute;top:8px;right:8px;font-size:11px;padding:2px 6px;border-radius:6px;background:rgba(0,0,0,.65)}.tools{position:absolute;bottom:8px;right:8px;display:flex;gap:6px}.tools button{background:rgba(0,0,0,.7);color:#fff;border:none;padding:5px 7px;font-size:12px;border-radius:6px;cursor:pointer}.toast{position:fixed;top:14px;left:50%;transform:translateX(-50%);background:#222;padding:8px 14px;border-radius:20px;font-size:12px;opacity:0;transition:.2s;z-index:20}.toast.show{opacity:1}.preview{position:fixed;inset:0;background:rgba(0,0,0,.9);display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:.2s;z-index:30}.preview.show{opacity:1;pointer-events:auto}.preview img{max-width:90%;max-height:90%}</style><header>页面图片资源(${base.length})<select id=f><option value=0>全部</option></select></header><div class=toast id=t></div><div class=grid id=g></div><div class=preview id=p><img></div>%60);const d=w.document,G=d.getElementById("g"),F=d.getElementById("f"),T=d.getElementById("t");sizes.forEach(s=>{const o=d.createElement("option");o.value=s;o.textContent="≥ "+s+"px";F.appendChild(o)});const toast=m=>{T.textContent=m;T.className="toast show";setTimeout(()=>T.className="toast",1200)};const draw=(v,first)=>{const arr=base.filter(i=>!v||i.m>=v).sort((a,b)=>first?b.m-a.m:a.m-b.m);G.innerHTML="";arr.forEach(i=>{const c=d.createElement("div");c.className="card enter";c.innerHTML='<span class="badge">'+i.w+' × '+i.h+'</span><img src="'+i.s+'"><div class="tools"><button data-copy="'+i.s+'">复制</button><button data-open="'+i.s+'">打开</button></div>';G.appendChild(c);setTimeout(()=>c.classList.remove("enter"),0)})};draw(0,true);F.onchange=e=>draw(+e.target.value,false);d.onclick=e=>{if(e.target.dataset.copy){const a=d.createElement("textarea");a.value=e.target.dataset.copy;d.body.appendChild(a);a.select();d.execCommand("copy");a.remove();toast("已复制")}if(e.target.dataset.open)w.open(e.target.dataset.open,"_blank","noopener");if(e.target.tagName==="IMG"){d.getElementById("p").classList.add("show");d.querySelector("#p img").src=e.target.src}if(e.target.id==="p")e.target.classList.remove("show")};d.onkeydown=e=>e.key==="Escape"&&d.getElementById("p").classList.remove("show");d.close()})();

八、可以延展的方向

这个思路可以进一步扩展为一类能力:

  • CSS 调试面板(查看覆盖关系)
  • 字体分析工具
  • 网络请求监控(结合 fetch hook)
  • DOM 结构可视化

也就是说:

Bookmarklet 不只是“小工具”,而是一种可以快速构建调试能力的前端模式。


九、总结

这段代码的价值不在于“提取图片”,而在于它体现了几个重要的前端思路:

  • 如何在运行时扩展页面能力
  • 如何做轻量级的数据建模与处理
  • 如何在无框架环境下构建完整交互
  • 如何利用浏览器原生能力实现隔离

如果换一个角度看,它更像是一个无需安装的临时 DevTools 扩展。

理解这一点,比代码本身更重要。

本文仅用于前端开发调试与技术研究,请勿用于侵犯他人版权或违反网站使用协议的行为。