简介:网页禁止选中、右键失效、图片文字要反复切换工具才能识别,这些细节正在悄悄消耗内容工作者的效率。本文从真实痛点出发,拆解网页禁用复制的三层实现原理,给出样式覆盖与事件拦截的解除方案,并完整梳理浏览器内图片文字识别的四种触发方式与坐标换算、引擎选型等关键细节,最后落地为侧边栏交互形态,为有同类需求的开发者提供一条可复用的完整实现路径。
先看一个典型场景:一位做内容策划的朋友,每天的工作是在海量网页和素材图之间摘抄文案。她的日常被两类高频摩擦反复打断:
从技术视角看,这两个问题的本质并不复杂:前者是页面脚本与样式对浏览器默认行为的劫持;后者是“截图 + OCR”两步流程没有被内联到浏览器的操作链路里。把它们封装成一个浏览器扩展,就能把“复制一段文字”的成本从十几秒降到一次点击。这也是很多开发者入门扩展开发的绝佳练手项目——体量小、链路完整、收益立竿见影。
要解除限制,先得理解限制的常见手段。梳理下来,主流方案大致分三层:
1. CSS 层:禁用文本选中
body {user-select: none;-webkit-user-select: none;}
user-select: none 会让元素内的文字无法通过鼠标拖拽选中,是最常见的“看得见摸不着”元凶。
2. 事件层:拦截并取消默认行为
document.addEventListener('contextmenu', e => e.preventDefault());document.addEventListener('copy', e => e.preventDefault());document.addEventListener('selectstart', e => e.preventDefault());document.addEventListener('keydown', e => {if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'c') e.preventDefault();});
通过监听右键菜单、复制、选区开始、快捷键等事件并调用 preventDefault(),直接掐断浏览器的默认能力。
3. 属性层:内联处理器与剪贴板改写
部分页面在元素上直接写 oncopy="return false"、oncontextmenu="return false";更隐蔽的做法是在 copy 事件中改写剪贴板数据,把用户复制的内容替换成空串或版权声明。此外还有站点用透明遮罩层盖住正文,或干脆用 canvas 渲染文本——后者的“文字”本质上是图片,需要走 OCR 路线,复制解锁对它无能为力。
由内容脚本向页面注入一条高优先级样式:
* {user-select: text !important;-webkit-user-select: text !important;}
三个注意点:样式必须带 !important 并尽量晚插入,以压过站点规则;user-select 是可继承属性,对影子 DOM 内的节点同样生效;对 input、textarea 等天然可编辑区域不要额外干预,避免影响正常表单行为。
在捕获阶段注册监听并用 stopPropagation 把事件“吞掉”:
['contextmenu', 'copy', 'cut', 'paste', 'selectstart','mousedown', 'mouseup', 'keydown', 'dragstart'].forEach(type => {document.addEventListener(type, e => e.stopPropagation(), true);});
原理在于事件传播顺序:捕获阶段先于目标阶段和冒泡阶段执行,而站点脚本通常把监听挂在后两者上。我们在捕获阶段先拦截并阻断传播,站点的 preventDefault 就没有机会执行,浏览器默认行为得以保留。第三个参数 true 是整个方案的关键。
对文档级和已渲染元素上的内联处理器做一次清扫:
const blocked = ['oncopy', 'oncut', 'oncontextmenu', 'onselectstart', 'ondragstart'];blocked.forEach(name => {document.body.removeAttribute(name);document.querySelectorAll(`[${name}]`).forEach(el => el.removeAttribute(name));});
现代页面大量依赖动态渲染,一次性清理很快会被新内容“打回原形”。标准做法是用 MutationObserver 持续监听 DOM 变更,新节点进入时重复执行样式注入与属性清理:
const observer = new MutationObserver(() => {ensureStyleInjected();cleanInlineHandlers();});observer.observe(document.documentElement, { childList: true, subtree: true });
工程上要两点取舍:回调需要节流,避免高频 DOM 变动拖慢页面;更稳妥的产品形态是做成“开关式”解除,只在用户主动点亮的标签页内生效,用完即关,把影响面控制到最小。
| 方式 | 说明 | 适用场景 |
|---|---|---|
| 整页截图 | 一键截取当前可见区域 | 快速识别整屏文字 |
| 框选区域 | 在页面上拖拽出任意矩形 | 只识别目标段落 |
| 点击上传 | 选择本地图片文件 | 识别已有素材图 |
| 拖拽上传 | 把图片拖入侧边栏 | 随手、批量识别 |
四个入口最终收敛到同一条“图像预处理 → 识别 → 结果输出”管线,是保持代码整洁的关键。
position: fixed 的 canvas 或 div),并临时锁定滚动;mousedown / mousemove / mouseup,实时绘制选区矩形并显示尺寸;
const dpr = window.devicePixelRatio || 1;const crop = {x: Math.min(sx, ex) * dpr,y: Math.min(sy, ey) * dpr,w: Math.abs(ex - sx) * dpr,h: Math.abs(ey - sy) * dpr,};
本地推理:使用以 WebAssembly 方式移植到浏览器的开源 OCR 引擎,数据不出本地、无网络依赖、隐私友好;代价是首次加载模型体积较大,对长图与复杂版式的准确率有限。适合隐私敏感、识别频率适中的场景。
云端识别:调用云服务商的文字识别 API,例如百度智能云的文字识别服务,这类通用 OCR 能力对中文、表格、手写等复杂版式的支持更完善,通常按调用量计费。集成时要注意两点:密钥不能硬编码进扩展包,建议经自有后端转发;要处理网络延迟与失败重试。
识别环节的引擎无关示意流程如下:
async function recognize(imageBitmap) {const result = await ocr.recognize(imageBitmap, { languages: ['zh', 'en'] });// result: [{ text, confidence, box }]return result.map(r => r.text).join('\n');}
输出侧建议做两个细节:结果按行拼接后提供“一键复制”按钮;对置信度偏低的行做高亮提示,方便人工校对。
点击上传走 input[type=file] 配合 FileReader 读取图像;拖拽上传则在投放区监听 dragover(需 preventDefault 并高亮反馈)与 drop 事件,从 dataTransfer.files 中取文件,校验 MIME 类型与大小后进入统一识别管线。
弹窗面板在点击页面其他区域时就会收起,独立窗口又会打断浏览节奏,两者都不适合“边看边摘”的场景。主流浏览器近年提供的扩展侧边栏能力恰好补上了这块:面板与页面并排常驻,框选时遮罩在页面、结果在侧栏,互不遮挡;识别结果可以在侧栏里先编辑再复制,贴合摘抄整理的工作流;再保留最近若干条历史记录,方便回溯。
这个扩展的技术含量不算高,却是一个很好的“全链路”练习:内容脚本注入、事件模型、DOM 观察、跨上下文通信、图像坐标换算、OCR 引擎集成、侧边栏交互,一圈走下来正好串起浏览器扩展开发的主要知识点。更重要的是它解决了真实痛点——当“复制一段文字”从十几秒的三步操作变成一次点击,被节省下来的不只是时间,还有被打断的工作心流。如果你身边也有被禁止复制和图片识别折磨的内容工作者,不妨照着这条路径动手实现一个。