简介:速记超人记事本V1.0是一款面向学生、研究人员与程序员等需频繁处理网页文本人群的浏览器自动复制插件,旨在解决手动复制粘贴繁琐、跨应用切换耗时的问题。插件可自动捕获网页选中的文本并存入内部,支持一键导出为txt或Excel格式,界面直观、易于上手,并可通过管理页面进行个性化设置。资源包共8个文件,以3个js脚本、1个json配置、1个html页面和3个png图标为主,分别承担弹出窗口交互、后台事件监听、网页内容脚本、插件元数据配置及图标展示等职责,压缩包仅18KB,轻量易部署。目前已有244人学习下载。通过该插件,读者可获得一套完整的浏览器扩展实现范例,理解自动复制与数据导出的核心逻辑,并直接用于日常资料搜集与归档,减少重复操作、提升信息整理效率。
1. 速记超人记事本V1.0:浏览器里那个“选中就自动进记事本”的插件到底怎么落地
你有没有过这种体验:在网页上查资料,看到一段关键参数或一句结论,选中、Ctrl+C、切到记事本、Ctrl+V,来回折腾十几遍,最后自己都不知道哪段是从哪个页面抄来的。浏览器自动复制插件要解决的就是这个断点——选中文字后自动把内容送进一个内置记事本,不用切窗口、不用手动粘贴。速记超人记事本V1.0 这个标题背后,核心是三件事:浏览器选中事件的监听、剪贴板或选区数据的捕获、以及一个能持久化存储的轻量记事本面板。它适合经常做竞品调研、资料摘录、代码片段收集的从业者,也适合想入门浏览器扩展开发的新手——因为功能边界清晰,一个周末就能跑通最小可用版本。我下面按 Manifest V3 的标准做法拆,Chrome、Edge 以及基于 Chromium 的国产浏览器都能直接加载调试。
2. 从选中到入库:自动复制插件的三个核心机制
2.1 为什么用 content script 监听 mouseup 而不是 copy 事件
浏览器扩展里监听用户选中文字,最直觉的做法是监听copy事件。但这里有个坑:copy只在用户主动按 Ctrl+C 或右键复制时触发,而“自动复制”的诉求是选中即捕获,用户根本没按复制键。所以正确入口是mouseup加selectionchange的组合。mouseup负责在鼠标松开时读取当前选区,selectionchange负责在键盘 Shift+方向键选中时也能兜住。我一般会在 content script 里做 200ms 的防抖,因为拖动选择过程中selectionchange会高频触发,不防抖会把半截选区也写进去。
// content.js —— 监听选中并防抖后发送消息 let debounceTimer = null; function captureSelection() { const selection = window.getSelection(); const text = selection ? selection.toString().trim() : ''; // 过滤空选区和过短内容,避免误触 if (!text || text.length < 2) return; // 200ms 防抖:拖动选择过程中不触发 clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { chrome.runtime.sendMessage({ type: 'SAVE_SNIPPET', payload: { text: text, url: location.href, title: document.title, ts: Date.now() } }); }, 200); } document.addEventListener('mouseup', captureSelection); document.addEventListener('selectionchange', captureSelection);这段代码的逻辑说明:window.getSelection()拿到当前选区对象,toString()转成纯文本。trim()去掉首尾空白,因为用户拖动时经常多选到空格。长度小于 2 的直接丢弃,这是防止单击误触产生空记录。防抖时间设 200ms 是经验值——太短会在拖动中途触发,太长用户选完要等。发送的消息里带上url和title,是为了后面在记事本里能追溯来源,这个字段在资料整理场景里比内容本身还重要。
参数方面,text.length < 2这个阈值可以按需调整。如果你主要摘录英文单词,可以降到 1;如果摘录的是中文短句,2 比较合适。防抖时间如果用户反馈“选完没反应”,先检查是不是设成了 500ms 以上。
2.2 消息传递:content script 和 background 的分工边界
Manifest V3 里,content script 运行在页面上下文,background 是 service worker,两者通过chrome.runtime.sendMessage通信。这里有个容易翻车的地方:service worker 随时可能被浏览器回收,所以不能在 background 里用全局变量存数据。我的做法是 background 只做消息中转和存储写入,所有状态都落到chrome.storage.local。
// background.js —— 接收消息并写入存储 chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'SAVE_SNIPPET') { const { text, url, title, ts } = message.payload; // 从存储读取现有记录,追加新条目 chrome.storage.local.get({ snippets: [] }, (result) => { const snippets = result.snippets; // 去重:相同文本 5 秒内不重复写入 const isDuplicate = snippets.some( s => s.text === text && Math.abs(s.ts - ts) < 5000 ); if (isDuplicate) { sendResponse({ ok: false, reason: 'duplicate' }); return; } snippets.unshift({ text, url, title, ts }); // 限制最多保留 500 条,防止存储膨胀 if (snippets.length > 500) snippets.length = 500; chrome.storage.local.set({ snippets }, () => { sendResponse({ ok: true, count: snippets.length }); }); }); return true; // 异步响应必须返回 true } });逻辑说明:chrome.storage.local.get的第二个参数是回调,第一个参数传默认值{ snippets: [] },这样首次运行时不会拿到 undefined。去重逻辑用文本内容加时间窗口判断,因为用户可能因为防抖边界重复触发。unshift把新记录放最前面,记事本面板展示时最新的在最上面。500 条上限是拍脑袋定的,实际测试下来每条记录平均 200 字节,500 条约 100KB,远低于chrome.storage.local的 5MB 配额。
关键参数:return true这行不能省。MV3 里如果onMessage的回调里用了异步操作,必须返回true告诉浏览器“我会异步调 sendResponse”,否则消息通道会提前关闭,content script 那边收到 undefined。这个坑我踩过不止一次,表现是“偶尔保存成功偶尔失败”,排查半天才发现是漏了返回值。
2.3 记事本面板:popup 还是 side panel
记事本界面放哪里,直接影响使用体验。popup 是点扩展图标弹出的小窗口,优点是实现简单,缺点是点击页面其他地方就关闭,没法边看网页边对照。side panel 是 Chrome 114 之后支持的侧边栏,可以常驻在浏览器右侧,适合速记场景。V1.0 我建议先做 popup,因为兼容性最好,等核心流程跑通再迁移到 side panel。
<!-- popup.html —— 记事本面板结构 --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style> body { width: 360px; margin: 0; font-family: system-ui, sans-serif; } #list { max-height: 400px; overflow-y: auto; padding: 8px; } .item { border-bottom: 1px solid #eee; padding: 8px 4px; } .item .text { font-size: 13px; line-height: 1.5; } .item .meta { font-size: 11px; color: #999; margin-top: 4px; } .item .meta a { color: #1a73e8; text-decoration: none; } #toolbar { padding: 8px; border-bottom: 1px solid #ddd; display: flex; gap: 8px; } button { font-size: 12px; padding: 4px 10px; cursor: pointer; } </style> </head> <body> <div id="toolbar"> <button id="copyAll">复制全部</button> <button id="clearAll">清空</button> <span id="count" style="font-size:12px;color:#666;line-height:24px;"></span> </div> <div id="list"></div> <script src="popup.js"></script> </body> </html>// popup.js —— 渲染记事本列表 function render() { chrome.storage.local.get({ snippets: [] }, (result) => { const list = document.getElementById('list'); const count = document.getElementById('count'); list.innerHTML = ''; count.textContent = `共 ${result.snippets.length} 条`; result.snippets.forEach((item) => { const div = document.createElement('div'); div.className = 'item'; // 用 textContent 防止 XSS,不要用 innerHTML 拼用户内容 const textEl = document.createElement('div'); textEl.className = 'text'; textEl.textContent = item.text; const metaEl = document.createElement('div'); metaEl.className = 'meta'; const time = new Date(item.ts).toLocaleString('zh-CN'); metaEl.textContent = `${time} · `; const link = document.createElement('a'); link.href = item.url; link.target = '_blank'; link.textContent = item.title || item.url; metaEl.appendChild(link); div.appendChild(textEl); div.appendChild(metaEl); list.appendChild(div); }); }); } document.getElementById('copyAll').addEventListener('click', () => { chrome.storage.local.get({ snippets: [] }, (result) => { const all = result.snippets.map(s => s.text).join('\n---\n'); navigator.clipboard.writeText(all); }); }); document.getElementById('clearAll').addEventListener('click', () => { chrome.storage.local.set({ snippets: [] }, render); }); render();逻辑说明:渲染时用textContent而不是innerHTML,因为摘录内容来自网页,可能包含<script>等标签,用 innerHTML 会引入 XSS 风险。来源链接用target="_blank"新标签打开,方便回溯。复制全部用\n---\n分隔,粘到文档里能看出条目边界。清空操作直接覆盖存储再重新渲染。
参数说明:popup 宽度设 360px 是 Chrome 扩展弹窗的常见尺寸,太宽会超出屏幕,太窄内容折行严重。max-height: 400px配合overflow-y: auto让列表可滚动,避免弹窗被撑高。时间格式化用toLocaleString('zh-CN'),显示成本地时间格式。
3. 避坑指南:自动复制插件最容易翻车的五个地方
3.1 现象:某些网站选中后没反应
原因:部分网站用user-select: none的 CSS 禁止了文本选择,或者用 iframe 嵌套内容,content script 默认不注入 iframe。解决:在 manifest 里配置all_frames: true让脚本注入所有 frame,同时在 CSS 层面不要强行覆盖网站的user-select,因为那会破坏网站本身的交互。如果确实需要,可以在捕获时判断selection.toString()是否为空,为空就跳过。
3.2 现象:保存的内容重复出现两三次
原因:mouseup和selectionchange同时触发,防抖时间设得太短,或者 background 的去重窗口太小。解决:确保两个事件走同一个防抖函数,去重窗口从 5 秒适当放大到 10 秒。另外检查是不是在 iframe 和主页面各注入了一次脚本,导致同一次选择被两个上下文各发一次消息。
3.3 现象:service worker 休眠后消息丢失
原因:MV3 的 service worker 在空闲 30 秒后会被回收,如果消息到达时 worker 正在启动,可能错过。解决:不要在 background 里维护内存状态,所有数据读写都走chrome.storage。如果遇到消息丢失,可以在 content script 发送前先chrome.runtime.sendMessage探测一下,失败就重试一次。更稳妥的做法是把存储逻辑直接放在 content script 里调用chrome.storage.local,绕过 background 中转。
3.4 现象:存储写满后新记录不保存
原因:chrome.storage.local默认配额 5MB,如果不做条数限制,长期使用会写满。解决:在 background 里加条数上限(比如 500 条),超出时从尾部截断。同时可以在 popup 里显示当前占用,让用户有感知。如果确实需要存更多,改用chrome.storage.local的unlimitedStorage权限,但要注意这会影响扩展审核。
3.5 现象:复制全部时格式乱掉
原因:多条记录直接 join 时没有分隔符,或者内容里本身包含分隔符。解决:用\n---\n这种不太可能出现在正文里的分隔符,或者在每条记录前加序号和来源。如果用户要粘到 Markdown 编辑器,可以做成> 来源:xxx\n\n内容\n\n的引用格式,可读性更好。
4. 进阶技巧:让速记超人记事本真正融入工作流
4.1 用快捷键触发面板而不是点图标
点扩展图标再选内容,操作链路太长。可以在 manifest 里注册commands,绑定Ctrl+Shift+S直接打开 popup 或者把当前选中内容静默保存。
{ "commands": { "save-selection": { "suggested_key": { "default": "Ctrl+Shift+S" }, "description": "保存当前选中内容到记事本" } } }然后在 background 里监听chrome.commands.onCommand,收到命令后向当前标签页发送消息,让 content script 执行一次捕获。这样用户选中后按快捷键,内容直接入库,连鼠标都不用移。
4.2 导出格式按用途分三种
记事本的价值在于内容能流转出去。我一般做三个导出按钮:纯文本(\n---\n分隔)、Markdown(每条带来源链接和引用格式)、JSON(保留完整字段,方便导入其他工具)。导出用Blob加URL.createObjectURL触发下载,不依赖任何后端。
function exportAs(format) { chrome.storage.local.get({ snippets: [] }, (result) => { let content = ''; if (format === 'markdown') { content = result.snippets.map(s => `> 来源:[${s.title}](${s.url})\n\n${s.text}\n` ).join('\n---\n\n'); } else if (format === 'json') { content = JSON.stringify(result.snippets, null, 2); } else { content = result.snippets.map(s => s.text).join('\n---\n'); } const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `snippets-${Date.now()}.${format === 'json' ? 'json' : 'txt'}`; a.click(); URL.revokeObjectURL(url); }); }Markdown 格式里把来源做成链接,粘到笔记软件里能直接跳回原页面,这个细节在资料回溯时特别有用。JSON 格式保留ts字段,方便按时间排序或做统计分析。
4.3 验证插件是否正常工作的三个检查点
装完扩展后,按这个顺序验证:第一,打开任意网页选中一段文字,点扩展图标看列表里有没有出现;第二,刷新页面再选一段,确认新记录追加而不是覆盖;第三,关掉浏览器重开,确认记录还在。如果第一步就没反应,打开chrome://extensions找到扩展,点“Service Worker”看控制台有没有报错,常见的是 manifest 权限没声明storage或者activeTab。如果第二步覆盖了,说明存储读取时默认值没设对,get的时候传了空对象而不是{ snippets: [] }。
我自己的习惯是每次改完 manifest 都重新加载一次扩展,因为权限变更不会热更新。另外 content script 的改动需要刷新目标页面才生效,这个也经常忘。希望帮到你。
本文还有配套的精品资源,点击获取