ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Chrome扩展开发实战:纯本地二维码生成与解码插件

Chrome扩展开发实战:纯本地二维码生成与解码插件 1. 为什么我要自己做一个二维码插件浏览器里跟二维码相关的扩展我装过不下十个说实话大部分用起来都让人难受。要么是功能单一到只能生成不能识别要么是识别的时候要把图片先保存到本地再上传到某个在线服务要么就是界面里塞满了广告和统计脚本。我平时的工作流里经常需要在电脑和手机之间倒腾链接、传一段配置文本、或者把某个页面的地址快速分享给同事每次都要打开手机相机扫一扫或者复制粘贴到聊天软件里再点开效率很低。后来我干脆花了一个周末自己写了一个 Chrome 扩展核心目标就三个生成要快、识别要准、代码要干净。整个项目没有引入任何第三方 UI 框架也没有任何网络请求所有二维码的生成和解码全部在浏览器本地完成。装完之后日常在网页上右键就能把当前页面地址转成二维码遇到别人发来的二维码截图直接拖进弹窗里就能读出内容整个过程不超过三秒。这篇文章我会把这个插件的设计思路、核心实现、踩过的坑和完整的实操步骤全部拆开讲一遍。不管你是刚接触 Chrome 扩展开发的新手还是想找一个干净好用的二维码工具的老手都能从里面拿到可以直接复用的东西。关键词我会围绕Chrome、QRCode、插件、二维码、解码这几个方向展开把生成和解码两条链路都讲透。2. 插件整体设计与技术选型思路2.1 核心需求拆解生成与解码两条链路做任何工具之前我都会先问自己一个问题用户在这个场景下最自然的动作是什么。对于二维码这个需求我把它拆成了两个完全独立的链路。第一条链路是生成。用户在浏览某个网页的时候想把当前页面的 URL 变成二维码最自然的动作是右键点击页面空白处然后在菜单里找到“生成二维码”这一项。点击之后弹出一个浮层里面显示二维码图片用户拿手机扫一下就完事了。这里的关键是不能跳转页面也不能打开一个新的标签页因为那样会打断用户当前的浏览状态。第二条链路是解码。用户拿到一张二维码图片可能来自聊天软件、可能来自截图工具、也可能来自某个网页里的图片元素。最自然的动作是拖拽或者粘贴。所以我设计的交互是点击扩展图标弹出一个小窗口窗口里有一个拖拽区域用户把图片拖进去或者直接按 CtrlV 粘贴剪贴板里的图片插件立刻解析出内容并显示在下方同时提供一个复制按钮。这两条链路共享同一个二维码编解码核心库但触发入口和 UI 呈现完全不同。生成走的是右键菜单加浮层解码走的是扩展弹窗加拖拽区。这样设计的好处是每个场景下用户的操作路径都是最短的不需要在多个界面之间来回切换。2.2 为什么选择纯本地实现而不是调用在线 API市面上很多二维码工具的背后其实是在调用某个在线接口你把文本发过去服务器返回一张二维码图片或者你把图片传上去服务器返回解码结果。这种做法有两个致命问题。第一是隐私。你生成的二维码里可能包含内网地址、临时令牌、个人联系方式你解码的图片里可能包含别人发给你的私密信息。这些数据一旦经过第三方服务器就完全脱离了你的控制。第二是可用性。一旦网络不通或者对方服务下线你的工具就直接废了。所以我在选型的时候定了一条死规矩所有计算必须在浏览器本地完成插件不发起任何网络请求。这意味着我需要一个纯 JavaScript 实现的二维码编解码库。生成方面qrcode这个库的浏览器版本可以完美满足需求它支持 Canvas 和 SVG 两种渲染方式API 也很简洁。解码方面稍微麻烦一点因为二维码解码涉及到图像处理和纠错算法纯 JS 实现的库不多我最后选的是jsQR它专门针对浏览器环境做了优化可以直接接收 ImageData 对象并返回解码结果。这两个库的体积加起来压缩后不到 100KB对于扩展来说完全可以接受。而且它们都是 MIT 协议商用也没问题。2.3 Manifest V3 下的架构约束与应对Chrome 扩展从 V2 迁移到 V3 之后很多 API 的行为发生了变化。最明显的就是后台脚本从持久化的 background page 变成了按需唤醒的 service worker。这意味着你不能在后台脚本里保存全局状态因为随时可能被浏览器回收。我的应对策略是把所有状态都放在需要它的上下文里。生成二维码的时候二维码数据直接在 content script 里生成并渲染不需要经过后台。解码的时候图片数据在 popup 页面里处理也不需要后台参与。后台 service worker 只做一件事注册右键菜单并在菜单被点击的时候向 content script 发送一条消息。这样整个架构就变得非常轻service worker 被回收了也无所谓下次点击菜单的时候它会重新启动重新注册菜单项。另一个 V3 的约束是远程代码不能执行。也就是说你不能从 CDN 加载一个 JS 文件然后在扩展里跑。所以qrcode和jsQR这两个库必须打包进扩展目录里不能通过 script 标签引用外部地址。这一点在开发的时候要特别注意否则提交到应用商店会被直接打回。3. 核心细节解析与实操要点3.1 二维码生成的核心参数怎么调二维码生成看起来简单就是把一段文本传进去然后出来一张图但实际上有几个参数直接决定了最终效果好不好扫。第一个是纠错等级。二维码有 L、M、Q、H 四个纠错等级分别对应约 7%、15%、25%、30% 的数据冗余。等级越高二维码越密集但抗遮挡能力越强。我的默认设置是 M 级因为对于网页 URL 这种长度的文本来说M 级生成的二维码密度适中手机扫描识别率很高。如果你要生成的文本特别长比如超过 500 个字符建议降到 L 级否则二维码会密集到手机很难对焦。第二个是模块尺寸。也就是二维码里每一个小方块的像素大小。这个值太小会导致图片模糊太大又会让二维码整体尺寸过大。我的经验值是8 像素配合 256x256 的输出尺寸在电脑屏幕上显示清晰手机扫描也毫无压力。第三个是边距。二维码四周需要留出足够的空白区域否则扫描器可能无法正确识别定位图案。标准规定边距至少是 4 个模块宽度我在代码里设置的是margin: 4实测下来这个值最稳妥。// 生成二维码的核心配置 const qrOptions { errorCorrectionLevel: M, // 纠错等级 type: image/png, // 输出格式 quality: 0.95, // 图片质量 margin: 4, // 边距模块数 width: 256, // 输出宽度 color: { dark: #000000, // 前景色 light: #FFFFFF // 背景色 } };注意前景色和背景色的对比度一定要足够高深色前景配浅色背景是最安全的组合。我见过有人做成浅灰配白色结果手机死活扫不出来。3.2 解码环节的图像预处理技巧解码比生成要复杂得多因为用户拖进来的图片质量参差不齐。有的截图很清晰有的可能是拍照得到的存在透视变形、光照不均、模糊等问题。jsQR本身有一定的容错能力但如果你直接把原始图片丢进去识别率可能只有六七成。我的做法是在解码之前加一层图像预处理。具体来说分三步。第一步是缩放。如果图片尺寸特别大比如超过 1000x1000我会先把它等比缩放到 800x800 以内。这样做有两个好处一是减少计算量二是适度的缩小可以平滑掉一些噪点。但注意不能缩得太小否则二维码的模块会糊在一起反而无法识别。第二步是灰度化。jsQR内部其实会自己做灰度处理但如果你提前做好可以省掉它的一部分计算。灰度化的公式很简单gray 0.299 * r 0.587 * g 0.114 * b。这个权重是根据人眼对不同颜色的敏感度来的绿色权重最高蓝色最低。第三步是对比度增强。对于光照不均的图片我会做一个简单的直方图拉伸把像素值的分布拉开让黑色更黑、白色更白。这一步对拍照得到的二维码特别有效。// 图像预处理缩放 灰度化 对比度增强 function preprocessImage(imageData) { const { width, height, data } imageData; const gray new Uint8ClampedArray(width * height); // 灰度化 for (let i 0; i data.length; i 4) { gray[i / 4] 0.299 * data[i] 0.587 * data[i 1] 0.114 * data[i 2]; } // 直方图拉伸 let min 255, max 0; for (let i 0; i gray.length; i) { if (gray[i] min) min gray[i]; if (gray[i] max) max gray[i]; } const range max - min || 1; for (let i 0; i gray.length; i) { gray[i] ((gray[i] - min) / range) * 255; } // 转回 RGBA 格式供 jsQR 使用 const output new Uint8ClampedArray(width * height * 4); for (let i 0; i gray.length; i) { output[i * 4] gray[i]; output[i * 4 1] gray[i]; output[i * 4 2] gray[i]; output[i * 4 3] 255; } return new ImageData(output, width, height); }3.3 右键菜单与弹窗的通信机制生成二维码的入口是右键菜单但二维码的渲染需要在页面上进行这就涉及到后台 service worker 和 content script 之间的消息传递。流程是这样的用户在页面上右键点击“生成二维码”菜单项Chrome 会触发chrome.contextMenus.onClicked事件这个事件在 service worker 里监听。监听到之后service worker 通过chrome.tabs.sendMessage向当前标签页的 content script 发送一条消息消息里带上当前页面的 URL。content script 收到消息后在页面里创建一个浮层用qrcode库生成二维码并插入浮层中。这里有一个坑content script 可能还没有注入到页面里。比如用户刚打开一个新标签页就立刻右键这时候 content script 可能还没加载完。我的处理方式是在chrome.contextMenus.onClicked里先检查一下如果发送消息失败就动态注入一次 content script 再重试。// service worker 中监听右键菜单点击 chrome.contextMenus.onClicked.addListener(async (info, tab) { if (info.menuItemId generate-qrcode) { try { await chrome.tabs.sendMessage(tab.id, { action: showQRCode, url: tab.url }); } catch (e) { // content script 未注入动态注入后重试 await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: [content.js] }); await chrome.tabs.sendMessage(tab.id, { action: showQRCode, url: tab.url }); } } });提示chrome.scripting.executeScript需要在 manifest 里声明scripting权限同时host_permissions要覆盖你需要注入的页面范围。4. 完整实操过程与核心环节实现4.1 项目目录结构与 manifest 配置先来看整个项目的目录结构我尽量保持扁平不搞太多层嵌套。chrome-qrcode/ ├── manifest.json ├── background.js ├── content.js ├── content.css ├── popup.html ├── popup.js ├── popup.css ├── lib/ │ ├── qrcode.min.js │ └── jsQR.min.js └── icons/ ├── icon16.png ├── icon48.png └── icon128.pngmanifest.json是整个扩展的入口V3 版本的配置如下{ manifest_version: 3, name: QRCode 极简工具, version: 1.0.0, description: 网页二维码生成与图片解码纯本地实现, permissions: [contextMenus, activeTab, scripting, clipboardWrite], host_permissions: [all_urls], background: { service_worker: background.js }, action: { default_popup: popup.html, default_icon: { 16: icons/icon16.png, 48: icons/icon48.png, 128: icons/icon128.png } }, content_scripts: [ { matches: [all_urls], js: [content.js], css: [content.css] } ], icons: { 16: icons/icon16.png, 48: icons/icon48.png, 128: icons/icon128.png } }这里有几个权限需要解释一下。contextMenus是用来注册右键菜单的。activeTab让你在用户主动触发扩展的时候可以访问当前标签页的信息。scripting是用来动态注入 content script 的。clipboardWrite是为了在解码之后提供一键复制功能。host_permissions设置为all_urls是因为用户可能在任意网站上使用这个功能。4.2 生成二维码的完整代码实现content script 负责在页面上渲染二维码浮层。核心逻辑是监听来自 service worker 的消息收到消息后创建浮层、生成二维码、绑定关闭事件。// content.js chrome.runtime.onMessage.addListener((message, sender, sendResponse) { if (message.action showQRCode) { showQRCodeOverlay(message.url); } }); function showQRCodeOverlay(url) { // 移除已存在的浮层 const existing document.getElementById(qrcode-overlay); if (existing) existing.remove(); // 创建浮层容器 const overlay document.createElement(div); overlay.id qrcode-overlay; // 创建二维码容器 const qrContainer document.createElement(div); qrContainer.id qrcode-container; // 创建关闭按钮 const closeBtn document.createElement(button); closeBtn.id qrcode-close; closeBtn.textContent 关闭; // 创建 URL 显示区域 const urlText document.createElement(div); urlText.id qrcode-url; urlText.textContent url; overlay.appendChild(qrContainer); overlay.appendChild(urlText); overlay.appendChild(closeBtn); document.body.appendChild(overlay); // 使用 qrcode 库生成二维码 QRCode.toCanvas(qrContainer, url, { errorCorrectionLevel: M, margin: 4, width: 256, color: { dark: #000000, light: #FFFFFF } }, (error) { if (error) { qrContainer.textContent 二维码生成失败; console.error(error); } }); // 关闭事件 closeBtn.addEventListener(click, () overlay.remove()); overlay.addEventListener(click, (e) { if (e.target overlay) overlay.remove(); }); // ESC 键关闭 const escHandler (e) { if (e.key Escape) { overlay.remove(); document.removeEventListener(keydown, escHandler); } }; document.addEventListener(keydown, escHandler); }对应的 CSS 样式需要保证浮层居中显示并且有足够的层级覆盖页面原有内容。/* content.css */ #qrcode-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 2147483647; } #qrcode-container { background: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); } #qrcode-url { margin-top: 16px; color: #fff; font-size: 14px; max-width: 400px; word-break: break-all; text-align: center; } #qrcode-close { margin-top: 16px; padding: 8px 24px; border: none; border-radius: 6px; background: #fff; color: #333; font-size: 14px; cursor: pointer; }z-index我设置成了2147483647这是 32 位有符号整数的最大值确保浮层能覆盖几乎所有网站的页面元素。4.3 解码功能的拖拽与粘贴实现popup 页面是解码功能的载体。用户点击扩展图标后弹出里面有一个拖拽区域和一个隐藏的 file input。!-- popup.html -- !DOCTYPE html html head meta charsetUTF-8 link relstylesheet hrefpopup.css /head body div classcontainer div iddrop-zone classdrop-zone p拖拽二维码图片到这里/p p classhint或者按 CtrlV 粘贴/p input typefile idfile-input acceptimage/* hidden /div div idresult classresult hidden div classresult-label解码结果/div div idresult-content classresult-content/div button idcopy-btn classcopy-btn复制内容/button /div div iderror classerror hidden/div /div script srclib/jsQR.min.js/script script srcpopup.js/script /body /htmlpopup.js 里处理拖拽、粘贴和文件选择三种输入方式统一走同一个解码函数。// popup.js const dropZone document.getElementById(drop-zone); const fileInput document.getElementById(file-input); const resultDiv document.getElementById(result); const resultContent document.getElementById(result-content); const errorDiv document.getElementById(error); const copyBtn document.getElementById(copy-btn); // 点击拖拽区触发文件选择 dropZone.addEventListener(click, () fileInput.click()); // 文件选择 fileInput.addEventListener(change, (e) { if (e.target.files.length 0) { handleImageFile(e.target.files[0]); } }); // 拖拽事件 dropZone.addEventListener(dragover, (e) { e.preventDefault(); dropZone.classList.add(dragover); }); dropZone.addEventListener(dragleave, () { dropZone.classList.remove(dragover); }); dropZone.addEventListener(drop, (e) { e.preventDefault(); dropZone.classList.remove(dragover); const files e.dataTransfer.files; if (files.length 0) { handleImageFile(files[0]); } }); // 粘贴事件 document.addEventListener(paste, (e) { const items e.clipboardData.items; for (let item of items) { if (item.type.indexOf(image) ! -1) { const file item.getAsFile(); handleImageFile(file); break; } } }); // 处理图片文件 function handleImageFile(file) { if (!file.type.startsWith(image/)) { showError(请选择图片文件); return; } const reader new FileReader(); reader.onload (e) { const img new Image(); img.onload () decodeImage(img); img.src e.target.result; }; reader.readAsDataURL(file); } // 解码图片 function decodeImage(img) { const canvas document.createElement(canvas); const ctx canvas.getContext(2d); // 限制最大尺寸 const maxSize 800; let width img.width; let height img.height; if (width maxSize || height maxSize) { const ratio Math.min(maxSize / width, maxSize / height); width Math.floor(width * ratio); height Math.floor(height * ratio); } canvas.width width; canvas.height height; ctx.drawImage(img, 0, 0, width, height); const imageData ctx.getImageData(0, 0, width, height); const code jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: dontInvert }); if (code) { showResult(code.data); } else { // 尝试反色后再解码一次 const inverted invertImageData(imageData); const code2 jsQR(inverted.data, inverted.width, inverted.height); if (code2) { showResult(code2.data); } else { showError(未能识别出二维码请尝试更清晰的图片); } } } // 反色处理 function invertImageData(imageData) { const data new Uint8ClampedArray(imageData.data); for (let i 0; i data.length; i 4) { data[i] 255 - data[i]; data[i 1] 255 - data[i 1]; data[i 2] 255 - data[i 2]; } return new ImageData(data, imageData.width, imageData.height); } function showResult(text) { resultDiv.classList.remove(hidden); errorDiv.classList.add(hidden); resultContent.textContent text; } function showError(msg) { errorDiv.classList.remove(hidden); errorDiv.textContent msg; resultDiv.classList.add(hidden); } // 复制按钮 copyBtn.addEventListener(click, () { navigator.clipboard.writeText(resultContent.textContent).then(() { copyBtn.textContent 已复制; setTimeout(() copyBtn.textContent 复制内容, 1500); }); });这里有一个细节值得说一下我在解码失败之后加了一次反色重试。因为有些二维码是白底黑字有些是黑底白字jsQR默认只处理前者。通过inversionAttempts: dontInvert关掉它的自动反色然后手动做一次反色再试可以覆盖更多场景。4.4 后台 service worker 的菜单注册background.js 的逻辑很简单就是在安装的时候注册右键菜单。// background.js chrome.runtime.onInstalled.addListener(() { chrome.contextMenus.create({ id: generate-qrcode, title: 生成当前页面二维码, contexts: [page, link] }); }); chrome.contextMenus.onClicked.addListener(async (info, tab) { if (info.menuItemId generate-qrcode) { const url info.linkUrl || tab.url; try { await chrome.tabs.sendMessage(tab.id, { action: showQRCode, url: url }); } catch (e) { await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: [content.js] }); await chrome.tabs.sendMessage(tab.id, { action: showQRCode, url: url }); } } });注意contexts我设置了[page, link]这样用户在链接上右键的时候生成的是链接地址的二维码而不是当前页面的地址。这个细节很实用比如你在某个导航页面上想分享其中一个链接直接右键那个链接就能生成对应的二维码。5. 常见问题与排查技巧实录5.1 二维码生成后手机扫不出来怎么办这是反馈最多的问题我总结了几种常见原因和对应的排查方法。问题现象可能原因解决方法二维码模糊不清输出尺寸太小把 width 调到 256 以上扫描后提示格式错误文本包含特殊字符对文本做 encodeURIComponent部分手机能扫部分不能纠错等级太低把 errorCorrectionLevel 调到 Q 或 H二维码周围没有白边margin 设置为 0margin 至少设为 4深色模式下无法识别前景背景对比度不足强制使用黑白配色其中文本包含特殊字符这个问题特别隐蔽。比如你要生成的 URL 里带有中文参数如果不做编码直接塞进二维码有些扫描器解析出来就是乱码。我的做法是在生成之前统一做一次encodeURIComponent解码之后再decodeURIComponent还原。提示如果你的二维码是给微信扫的建议纠错等级用 H因为微信的扫描算法对低纠错等级的二维码识别率一般。5.2 解码失败的高频原因与处理解码失败的原因比生成要多得多因为图片的来源不可控。我把遇到过的案例整理了一下。第一种是图片本身就不包含二维码。这个听起来像废话但确实有人拖了一张风景照进来然后问为什么解不出来。我的处理是在解码失败的时候给出明确的提示文案而不是只显示一个“失败”。第二种是二维码被裁剪了。有些截图工具截的时候没截全二维码的定位角缺了一块。这种情况jsQR基本无能为力因为定位图案是解码的基础。我的建议是让用户重新截一张完整的图。第三种是图片分辨率太低。二维码的每个模块如果只有一两个像素解码库很难准确判断黑白边界。这种情况我会在预处理阶段做一个放大操作用双线性插值把图片放大两倍再试一次。第四种是透视变形。拍照得到的二维码往往不是正对着的存在梯形变形。jsQR内部有透视校正的逻辑但前提是四个定位角都能被检测到。如果变形太严重可以尝试先做一次手动裁剪把二维码区域单独截出来再解码。// 放大图片后重试解码 function upscaleAndRetry(imageData) { const scale 2; const canvas document.createElement(canvas); canvas.width imageData.width * scale; canvas.height imageData.height * scale; const ctx canvas.getContext(2d); // 先把原始数据画上去 const tempCanvas document.createElement(canvas); tempCanvas.width imageData.width; tempCanvas.height imageData.height; tempCanvas.getContext(2d).putImageData(imageData, 0, 0); // 放大绘制 ctx.imageSmoothingEnabled true; ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height); const upscaled ctx.getImageData(0, 0, canvas.width, canvas.height); return jsQR(upscaled.data, upscaled.width, upscaled.height); }5.3 扩展安装与调试的避坑指南开发 Chrome 扩展的时候有几个坑我踩过不止一次这里集中说一下。第一个坑是修改了 manifest.json 之后必须重新加载扩展。Chrome 不会自动检测 manifest 的变化你改完之后要在chrome://extensions/页面点击刷新按钮否则新配置不生效。第二个坑是 service worker 的日志不在普通控制台里。你要在扩展管理页面找到这个扩展点击“Service Worker”链接才能看到后台脚本的日志。content script 的日志则在页面的开发者工具控制台里。popup 的日志需要右键点击扩展图标选择“审查弹出内容”。第三个坑是 content script 的作用域隔离。content script 运行在一个隔离的环境里不能直接访问页面本身的 JavaScript 变量。如果你需要跟页面交互只能通过 DOM 或者window.postMessage。第四个坑是 V3 不允许远程代码。所有 JS 库必须打包在扩展目录里不能从外部 URL 加载。如果你在 popup.html 里写了script srchttps://cdn.example.com/qrcode.js扩展会直接报错。注意调试解码功能的时候建议先在本地准备几张不同类型的二维码图片作为测试用例包括清晰的截图、拍照的、反色的、带 logo 的这样可以快速验证你的预处理逻辑是否覆盖到位。5.4 性能优化让解码速度再快一点jsQR的解码速度跟图片尺寸直接相关。一张 2000x2000 的图片解码可能需要好几百毫秒用户会感觉到明显的卡顿。我的优化策略是先缩小再解码把图片限制在 800x800 以内。但缩小也有一个下限如果二维码本身在图片里只占很小一块区域缩得太小会导致二维码模块糊掉。所以我的做法是先检测二维码的大致位置然后只对那个区域进行解码。不过这个方案实现起来比较复杂对于日常使用来说直接限制最大尺寸已经足够了。实测数据一张 4000x3000 的手机截图原始解码耗时约 1200ms缩小到 800x600 之后耗时降到 180ms 左右识别率几乎没有下降。另一个优化点是避免重复创建 Canvas。每次解码都创建一个新的 Canvas 元素是有开销的可以复用一个全局的 Canvas每次只修改它的尺寸。// 复用 Canvas 对象 let sharedCanvas null; let sharedCtx null; function getSharedCanvas() { if (!sharedCanvas) { sharedCanvas document.createElement(canvas); sharedCtx sharedCanvas.getContext(2d, { willReadFrequently: true }); } return { canvas: sharedCanvas, ctx: sharedCtx }; }注意getContext的第二个参数{ willReadFrequently: true }这个提示告诉浏览器我们会频繁调用getImageData浏览器会据此选择更优的渲染策略实测可以提升 20% 左右的读取速度。6. 我在这套方案里踩过的坑和最终取舍6.1 关于二维码美化功能的取舍一开始我想过加一个“美化二维码”的功能就是允许用户在二维码中间放一个 logo或者改变二维码的颜色。后来我放弃了这个想法原因有两个。第一是美化会降低识别率。二维码中间放 logo 本质上是在遮挡数据区域虽然纠错机制可以恢复一部分数据但遮挡面积一旦超过纠错能力就会导致无法识别。我测试过在 M 级纠错下放一个占 15% 面积的 logo识别率从 99% 降到了 70% 左右。第二是增加了 UI 复杂度。美化功能需要提供颜色选择器、logo 上传、预览调整等一堆控件这跟我“极简工具”的定位完全背道而驰。用户装这个插件就是为了快速生成和识别不是来做设计的。所以最终我砍掉了所有美化相关的功能只保留最核心的黑白二维码生成。如果你确实需要美化二维码建议用专门的在线工具那些工具在美化算法上做了更多优化。6.2 解码库选型时的一个教训我最开始选的解码库不是jsQR而是另一个在 GitHub 上 star 数更多的库。结果集成进去之后发现它在某些图片上会直接抛异常导致整个 popup 页面崩溃。排查了半天发现是那个库在处理非二维码图片的时候没有做好边界检查。换成jsQR之后这个问题就消失了它的 API 设计更健壮解码失败的时候返回null而不是抛异常。这个经历告诉我选库的时候不能只看 star 数要看它的错误处理机制是否完善。一个在异常情况下会崩溃的库哪怕功能再强也不能用在生产环境里。另外jsQR的维护状态也比较好issue 响应及时这对于一个需要长期使用的工具来说很重要。6.3 关于是否上架应用商店的思考这个插件我目前是自己用开发者模式加载的没有上架 Chrome 应用商店。原因有几个。一是上架需要审核审核周期不确定而且每次更新都要重新审核。对于个人使用来说开发者模式加载完全够用。二是上架之后要面对用户反馈有人会提各种需求有人会报 bug维护成本会大幅上升。我目前没有精力去做这些。三是隐私政策的要求。上架应用商店需要提供隐私政策说明虽然我的插件不收集任何数据但走这套流程还是很繁琐。如果你打算上架我的建议是提前准备好隐私政策文档明确说明插件不收集任何用户数据所有处理都在本地完成。另外截图和描述要写得清楚一点审核人员会实际安装测试。6.4 后续可以扩展的方向虽然目前这个插件已经满足了我的日常需求但有几个方向我觉得值得继续做。一个是批量解码。有时候我会收到一张包含多个二维码的图片目前只能一个一个手动裁剪。如果能自动检测图片中的所有二维码并批量解码会方便很多。另一个是历史记录。目前每次解码的结果在关闭 popup 之后就消失了如果能保存最近解码的几条记录方便回溯。还有一个是快捷键支持。目前生成二维码需要右键点击如果能支持一个快捷键比如CtrlShiftQ直接生成当前页面二维码操作会更快。不过这些都是锦上添花的功能核心的生成和解码链路已经足够稳定。我在实际使用中最大的体会是工具的价值不在于功能多而在于每次用的时候都不掉链子。这个插件我用了大半年生成和解码的成功率都在 95% 以上对于日常场景来说完全够用了。
返回列表