前端技巧:用 Bookmarklet 给网页临时挂载一个图片调试面板
在前端开发中,我们经常需要分析页面中的资源情况,比如:
- 页面实际加载了哪些图片
- 不同分辨率资源的分布
- 是否存在重复图片请求
- UI 还原时如何快速定位原始素材
这些事情当然可以通过 DevTools 完成,但在某些场景下效率并不高。例如批量浏览图片、筛选大图、快速预览等操作,都需要在多个面板之间来回切换。
于是可以换一个思路:
不扩展 DevTools,而是用一段 Bookmarklet,在任意网页上临时挂载一个“图片调试面板”。
这篇文章的重点不在工具本身,而在于这种实现方式背后的前端技术思路。
一、为什么是 Bookmarklet
Bookmarklet 的本质,是一段运行在当前页面上下文中的 JavaScript。
javascript:(()=>{ /* your code */ })()
它有几个非常关键的特性:
- 直接运行在页面环境中,可以访问 DOM
- 不需要构建或发布浏览器插件
- 无需侵入页面代码
- 可以在任意网站使用
这使它非常适合做“临时调试能力注入”。
可以把它理解为:
一种轻量级的“运行时工具扩展机制”
二、核心问题:如何获取真实图片资源
最直接的方式是:
[...document.images]
但这只是第一步,真正需要解决的是两个问题:
1. 图片是否已经加载完成
i.naturalWidth
只有当图片完成加载后,naturalWidth 和 naturalHeight 才是有效的。
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 扩展。
理解这一点,比代码本身更重要。
本文仅用于前端开发调试与技术研究,请勿用于侵犯他人版权或违反网站使用协议的行为。