简介:针对网页禁用复制、图片取字需跨应用来回切换两大高频痛点,本文基于现代浏览器扩展规范,实战开发一款图文提取插件:用内容脚本在捕获阶段拦截站点的限制逻辑,恢复文本选中、复制与右键菜单;再串联可见区截图、框选裁剪与云端 OCR 接口,把取字流程收敛到侧边栏一站式完成,附关键代码与工程化踩坑经验,可直接照搬落地。
在日常的文字工作中,有两类操作出现频率极高,却始终没有被浏览器原生能力很好地覆盖。
痛点一:网页禁止复制。 不少设计灵感站、素材站、文库类站点出于内容保护的目的,会禁用文本选中、屏蔽右键菜单,甚至在 copy 事件里直接 preventDefault()。用户明明“看得见”文字,却“摸不着”,只能逐字手敲,效率极低。
痛点二:图片取字链路太长。 想从一张图里提取文字,常规做法是:截图 → 切到自带 OCR 能力的聊天或办公工具 → 发起识别 → 等待结果 → 复制 → 切回浏览器粘贴。一次取字要在多个应用之间往返三次以上,思路频繁被打断。
这两个痛点的共同本质是:操作流被割裂在不同工具之间,而浏览器本身完全有能力把它们收敛到一处。于是这个周末项目的目标就定下来了——做一个浏览器插件,把“解锁复制”和“图片取字”都装进浏览器里,所有操作在侧边栏完成,不遮挡页面、不切换窗口。
插件基于当前主流的浏览器扩展规范(manifest.json 清单 + 多角色脚本)搭建,整体划分为四个角色:
两条能力线相互独立又共享同一套 UI:能力一解决“选不中、复制不了”,能力二解决“图里的字取不出来”。
业界常见的限制手段主要有三类:
document 上监听 copy、cut、contextmenu、selectstart、dragstart 等事件并调用 preventDefault(),直接掐断浏览器默认行为;oncopy="return false" 之类的属性;user-select: none 让文本根本无法被选中。少数站点还会叠加“透明遮罩层盖住正文”或将正文放进嵌套 iframe 等进阶手段。
第一层:在捕获阶段抢事件。 DOM 事件先经过捕获阶段、再进入目标与冒泡阶段。插件在 document 上以 capture: true 注册同名监听器并调用 stopPropagation(),事件就永远到不了站点自己的监听器手里:
const TYPES = ['copy', 'cut', 'contextmenu', 'selectstart', 'dragstart', 'mousedown'];TYPES.forEach(type => {document.addEventListener(type, e => e.stopPropagation(), true);});
第二层:清空内联属性。 对挂在内联属性上的限制逻辑直接置空:
['oncopy', 'oncut', 'oncontextmenu', 'onselectstart', 'ondragstart'].forEach(attr => {document.documentElement[attr] = null;if (document.body) document.body[attr] = null;});
第三层:样式覆盖。 注入一段高优先级样式,把 user-select 强制改回可选中:
* {user-select: text !important;-webkit-user-select: text !important;}
大量站点的内容是异步渲染的,解锁逻辑必须能“追着 DOM 跑”。这里借助 MutationObserver 监听整棵 DOM 树,一旦有新增节点就对其重复执行解锁处理:
const observer = new MutationObserver(() => unlock(document));observer.observe(document.documentElement, { childList: true, subtree: true });
解锁逻辑不能无脑全局常开:在线文档、富文本编辑器等场景本身就依赖这些事件的默认行为,强行解锁反而会破坏正常交互。因此插件把“解除限制”设计成按需开关:点击插件图标手动触发,状态按域名存入 storage,下次访问同站点自动生效,也可随时一键关闭。
针对不同来源的图片,插件提供四条入口,覆盖绝大多数取字场景:
| 识别方式 | 操作说明 | 适用场景 |
|---|---|---|
| 整页截图 | 一键截取当前可见区域并识别 | 快速识别整屏文字 |
| 框选区域 | 在页面上拖拽出矩形范围 | 只取长页面中的局部文字 |
| 点击上传 | 从本地选择图片文件 | 识别已保存的图片 |
| 拖拽上传 | 将图片拖入侧边栏 | 零步骤快速识别 |
所有入口最终都汇入同一条流水线:取图 → 预览 → 送 OCR → 结果展示 → 一键复制/下载。
可见区截图调用扩展平台提供的截图接口(如 captureVisibleTab 一类的能力),拿到当前标签页的 DataURL。框选则由 content script 在页面里铺一层半透明遮罩,监听 mousedown / mousemove / mouseup 记录矩形坐标,再按坐标从截图上裁出目标区域:
function crop(dataUrl, rect, dpr) {const img = new Image();img.src = dataUrl;return new Promise(resolve => {img.onload = () => {const canvas = document.createElement('canvas');canvas.width = rect.width * dpr;canvas.height = rect.height * dpr;canvas.getContext('2d').drawImage(img,rect.x * dpr, rect.y * dpr, rect.width * dpr, rect.height * dpr,0, 0, canvas.width, canvas.height);resolve(canvas.toDataURL('image/png'));};});}
这里有一个极容易踩的坑:devicePixelRatio。截图的物理像素尺寸等于 CSS 尺寸乘以设备像素比,在高分屏上如果直接用 CSS 坐标裁剪,裁出来的区域会整体偏移。所以矩形坐标与裁剪尺寸都必须乘以 dpr 再处理。此外,按 Esc 要能随时取消选框;选框过程中页面若发生滚动,坐标应以视口为基准并刷新遮罩。
input[type=file],用 FileReader 读成 DataURL;dragover / drop 事件,从 DataTransfer 中取文件。顺带监听 paste 事件支持直接粘贴剪贴板截图,几乎零成本多覆盖一个高频场景。识别能力接入云端 OCR 服务(例如百度智能云的文字识别服务)。这类服务的调用流程高度一致:鉴权 → 传图(Base64 或图片地址)→ 返回结构化 JSON → 按行拼接文本。主流云厂商的 OCR 服务普遍提供一定的免费调用额度,个人日常使用完全够用。
async function recognize(imageBase64) {const resp = await fetch(OCR_API, {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ image: imageBase64 })});const data = await resp.json(); // 字段名以所选 OCR 服务的文档为准return data.wordsResult.map(w => w.words).join('\n');}
识别结果在侧边栏以可编辑文本呈现:先手动修掉个别错字,再一键复制或下载为 txt,形成完整闭环。
侧边栏是体验上的关键决策:
activeTab 就不要申请全站 host 权限。activeTab 只在用户主动触发插件时授予当前标签页的临时权限,截图与脚本注入都够用,用户也更放心。iframe 的站点,需在清单中声明 all_frames 才能把内容脚本注入进去;SPA 路由切换后要重新执行解锁逻辑,避免“首页能用、翻页失效”。实际使用反馈很直接:禁复制的站点一键恢复选中与右键,复制如常;取字从“截图—切应用—识别—复制—切回”五步压缩为“点一下、框一下”两步,全程不离开浏览器。
在这个骨架上还有许多低成本的延伸:识别结果即时翻译、连续截图的历史记录、多图批量识别、全局快捷键触发、识别文本直接追加到笔记等。本质上,浏览器扩展最大的价值在于把散落各处的碎片化操作,收敛成一条不被打断的工作流——这也正是这类小工具最值得亲手实现的原因。