简介:本文从内容摘抄场景的真实痛点出发,剖析网页禁止选中、禁用右键的常见实现原理,讲解如何用浏览器扩展在捕获阶段拦截事件并覆盖样式解除限制;同时给出页面截图、框选区域、本地上传等OCR触发方式的完整实现思路与工程优化建议,帮助开发者掌握一套可落地的浏览器插件开发方案。
做文案策划、内容运营、资料整理类工作的用户,每天都要和网页文本、素材图片打交道,高频动作无非两个:摘抄网页文案、提取图片里的文字。但实际体验中,这两件事经常被卡住:
这两个痛点的本质都是上下文切换成本:能力其实早已存在(浏览器能渲染文字、OCR 技术非常成熟),只是没有被封装进最顺手的操作路径里。因此本文的目标,是用一个浏览器扩展把两件事收拢到浏览器内完成:一键解除页面复制限制 + 在浏览器内直接框选识别图片文字,所有交互通过侧边栏承载,不打断当前页面。
要解除限制,先要理解限制。常见的反复制手段大体分三层:
最简单的方式是给元素设置 user-select: none(含私有前缀 -webkit-user-select),使文本无法通过鼠标拖拽选中,再配合 -webkit-touch-callout: none 禁掉移动端长按菜单。这类限制纯样式层面,覆盖成本最低。
更常见也更强硬的方式,是在 copy、cut、contextmenu(右键)、selectstart、mousedown、keydown 等事件上注册监听器,并调用 preventDefault() 或 return false,从源头让复制、右键、选中动作失效。有的站点还会监听键盘事件阻止复制组合键,甚至在 copy 事件里篡改或清空剪贴板内容。由于事件先于默认行为触发,只要监听器注册得足够早,页面就能“抢跑”。
少数站点会再叠加手段:在正文上盖一层透明元素拦截鼠标事件、把内容放进 iframe 并在父页面拦截事件、动态渲染正文并频繁重绑监听器等。这类手段出现的概率不高,但决定了“解除方案”必须具备对动态内容的持续覆盖能力。
浏览器扩展的内容脚本运行在与页面隔离的环境中,但共享同一份 DOM 与事件流,且页面代码无法移除我们注册的监听器,这给了我们“先于页面处理事件”的能力。
DOM 事件按“捕获 → 目标 → 冒泡”的顺序传播,页面代码通常在冒泡阶段注册监听。只要内容脚本在 document_start 时机、以捕获方式抢先注册监听,就能在页面代码之前拿到事件,并通过 stopImmediatePropagation() 阻断其继续传播。注意:我们不调用 preventDefault(),因此浏览器默认的复制、右键行为不受影响——这正是“解除”而非“禁用”的关键。
// unblock.js —— content_scripts, run_at: document_startconst stop = (e) => e.stopImmediatePropagation();['copy', 'cut', 'contextmenu', 'selectstart','mousedown', 'mouseup', 'dblclick', 'keydown', 'dragstart'].forEach(type => window.addEventListener(type, stop, { capture: true }));
对 CSS 类限制,注入一条高优先级样式即可恢复文本可选:
const style = document.createElement('style');style.textContent = `*, *::before, *::after {user-select: text !important;-webkit-user-select: text !important;-webkit-touch-callout: default !important;}`;document.documentElement.appendChild(style);
部分站点不使用事件监听接口,而是直接给元素赋值 oncopy、oncontextmenu 等属性,或在交互过程中反复重新赋值。可以对原型上的相关属性做拦截,使后续赋值失效。这属于“激进模式”,可能影响站点正常交互,建议做成可开关的能力,按站点启用。
['oncopy', 'oncut', 'oncontextmenu', 'onselectstart'].forEach(name => {Object.defineProperty(HTMLElement.prototype, name, {configurable: true,get() { return null; },set() { /* 忽略页面的限制性赋值 */ }});});
单页应用与无限加载页面的 DOM 持续变化,新插入的节点可能携带 user-select: none 的内联样式。用 MutationObserver 监听子树变化并对新增节点做样式修正,即可实现动态内容也解除限制。为控制开销,回调里只检查内联 style,避免高频调用 getComputedStyle。
const observer = new MutationObserver(mutations => {for (const m of mutations) {m.addedNodes.forEach(node => {if (node.nodeType !== 1) return;if (node.style?.userSelect === 'none') {node.style.setProperty('user-select', 'text', 'important');}});}});observer.observe(document.documentElement, { childList: true, subtree: true });
工程上还有一个重要取舍:全局拦截 mousedown、keydown 这类高频事件,可能干扰依赖它们的站点功能(自定义下拉、快捷键体系等)。更稳妥的设计是把“解除限制”做成按站点开关:默认关闭,用户点击扩展图标时对当前站点启用并记忆选择。
OCR 部分的重点在于“如何把图拿到手”和“用什么引擎识别”。围绕场景可设计四种触发方式:
| 方式 | 交互 | 适用场景 |
|---|---|---|
| 页面截图 | 一键截取当前可见区域 | 快速识别整页文字 |
| 框选区域 | 在页面上拖拽画框 | 只识别关心的局部文字 |
| 点击上传 | 文件选择器选图 | 识别本地已有图片 |
| 拖拽上传 | 把图片拖进侧边栏 | 顺手上传、批量识别 |
扩展平台提供截取当前标签页可见区域的接口(如 tabs.captureVisibleTab),返回一张 PNG 的 Data URL。配合 activeTab 权限,在用户主动点击扩展时触发即可满足最小权限原则。“框哪里识别哪里”的核心是一个全屏遮罩层:内容脚本注入 position: fixed 的覆盖层,监听鼠标按下、移动、抬起记录选区,再用 canvas 按坐标从整页截图中裁出选区。必须注意设备像素比(dpr):截图的物理像素是 CSS 尺寸乘以 dpr,忽略这点会导致裁剪偏移或模糊。
async function cropRegion(dataUrl, rect, dpr) {const img = new Image();img.src = dataUrl;await img.decode();const canvas = document.createElement('canvas');canvas.width = rect.width * dpr;canvas.height = rect.height * dpr;canvas.getContext('2d').drawImage(img,rect.left * dpr, rect.top * dpr, rect.width * dpr, rect.height * dpr,0, 0, canvas.width, canvas.height);return canvas.toDataURL('image/png');}
点击上传与拖拽上传可收敛到同一条链路:拿到 File/Blob 后转成图像源,绘制到 canvas 统一处理(缩放、预处理),再交给识别模块。把拖放目标放在侧边栏上,配合高亮反馈,即可获得接近本地应用的体验。
// 通用文字识别接口调用示意async function recognize(base64Image) {const resp = await fetch('https://your-ocr-endpoint', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ image: base64Image })});const data = await resp.json();return data.words_result.map(w => w.words).join('\n');}
个人工具可采用“本地兜底 + 云端精识别”的折中策略:轻量场景本地快速出结果,重要素材再走云端拿高准确率。
截图与上传图常存在对比度低、背景复杂的问题,识别前做轻量预处理能明显提升准确率:等比缩放控制长边、小字放大 1.5~2 倍;灰度化后按阈值二值化突出笔画;框选时选区略向内收,避免把背景纹理送进引擎。
function binarize(canvas, threshold = 160) {const ctx = canvas.getContext('2d');const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);const d = imageData.data;for (let i = 0; i < d.length; i += 4) {const gray = 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2];const v = gray > threshold ? 255 : 0;d[i] = d[i + 1] = d[i + 2] = v;}ctx.putImageData(imageData, 0, 0);}
{"manifest_version": 3,"name": "图文解锁助手","version": "1.0.0","permissions": ["activeTab", "scripting", "sidePanel", "storage"],"content_scripts": [{"matches": ["<all_urls>"],"js": ["unblock.js"],"run_at": "document_start"}],"action": { "default_title": "图文解锁" },"background": { "service_worker": "background.js" }}
权限遵循最小化原则:activeTab 让截图与注入只在用户主动触发的标签页生效;storage 保存站点开关与识别历史;不申请宽泛的后台读取权限,降低安装信任成本。
把截图、框选、上传、结果展示都收进侧边栏,好处有三:不遮挡页面,框选时视野完整;结果常驻,切换标签页不丢失;“识别 → 复制”在同一面板内闭环,点击结果卡片即可写回系统剪贴板:
await navigator.clipboard.writeText(text);
回顾整个方案:解除复制限制的本质是“在事件流与样式层抢到先手”,核心是 document_start 注入、捕获阶段拦截与样式覆盖三件事;OCR 的本质是“把图正确地拿到并交给引擎”,核心是截图接口、canvas 裁剪与预处理。两者技术门槛都不高,难点在于把交互收敛成一条不打断心流的路径。按此思路落地后,“看到文字就复制、看到图片文字就提取”都变成浏览器内一两步的操作,内容摘抄类工作的效率提升立竿见影。读者还可继续扩展:识别结果结构化导出、快捷键触发、表格版式识别等,把工具打磨成真正贴合自己工作流的形态。