1. 从“草料之外”说起:为什么我还要自己折腾一个二维码插件
做前端和运营的朋友大概都有过这种体验:临时要把一段链接、一段配置文本、一个 Wi-Fi 密码或者一张名片信息转成二维码,第一反应是打开某个在线二维码网站,粘贴、生成、下载,一套流程下来少说也要切三四个标签页。更麻烦的是,很多在线工具会在二维码中间塞一个巨大的 Logo,或者生成的图片带水印,扫出来还经常因为容错率设置不当而识别失败。我自己就踩过这个坑——给客户做线下物料时,用某在线工具生成的二维码印在易拉宝上,结果现场光线一暗,十个人里有三个人扫不出来,场面一度非常尴尬。
后来我开始琢磨,既然 Chrome 本身就是我每天待得最久的工具,为什么不把“生成二维码”和“解码二维码”这两件事直接塞进浏览器里?这就是Chrome-QRCode这类极简插件的核心思路:不依赖任何外部服务,所有计算都在本地完成,点一下图标就能把当前页面 URL 变成二维码,右键图片就能把二维码里的内容解出来。整个过程不需要联网上传数据,也不会有隐私泄露的顾虑。
这篇文章适合三类人看:一是想快速上手、三分钟就能用起来的普通用户;二是想自己动手改插件、加功能的前端开发者;三是想理解二维码生成与解码底层原理、避免踩坑的技术爱好者。我会从插件的工作机制讲起,把生成和解码两条链路拆开,再补上我实际使用中遇到的坑和优化技巧。你不需要有很深的密码学或图形学背景,只要会装 Chrome 插件、能看懂基本的 JavaScript,就能跟着走完。
提示:本文讨论的是浏览器扩展形态的二维码工具,所有操作均在本地完成,不涉及任何数据外传,适合对隐私敏感的场景。
2. Chrome-QRCode 到底在浏览器里做了什么
2.1 插件的三个核心能力边界
很多人以为二维码插件就是“调用一个库生成图片”,其实远不止。一个能打的 Chrome 二维码插件,至少要覆盖三个场景:
- 当前页面 URL 生成:点击工具栏图标,直接把
location.href转成二维码,这是最高频的需求。 - 选中文本生成:在页面上选中一段文字,右键菜单里出现“生成二维码”,适合分享一段配置、一个地址、一串密钥。
- 图片二维码解码:在任意网页的二维码图片上右键,选择“解码此二维码”,插件读取图片像素并还原出原始文本。
这三个能力背后对应的是两条完全不同的技术链路:生成是“文本 → 编码 → 矩阵 → 渲染”,解码是“图像 → 定位 → 采样 → 纠错 → 还原”。Chrome-QRCode 这类极简插件的价值就在于,它把这两条链路都封装进了浏览器扩展的content script和background service worker里,用户感知不到任何中间过程。
2.2 为什么选择本地计算而不是调用在线 API
我见过不少“二维码插件”其实是把文本发给某个在线接口,拿回一张图片 URL 再显示。这种做法有三个致命问题:第一,你的 URL 或文本会经过第三方服务器,隐私完全不可控;第二,一旦接口挂了或者被限流,插件直接废掉;第三,网络延迟会让“点一下出码”变成“等两秒出码”,体验断崖式下降。
Chrome-QRCode 走的是纯本地路线,生成用qrcode这类纯 JS 库,解码用jsQR或zxing-js,全部跑在浏览器沙箱里。代价是插件体积会大一点(通常几百 KB),但换来的是离线可用、零延迟、零隐私风险。对于经常在弱网环境或者内网环境工作的人来说,这个取舍非常值得。
2.3 插件的最小权限模型
一个设计克制的二维码插件,manifest.json里通常只需要这几个权限:
{ "manifest_version": 3, "name": "Chrome-QRCode", "version": "1.0.0", "permissions": ["contextMenus", "activeTab", "scripting"], "background": { "service_worker": "background.js" }, "action": { "default_popup": "popup.html" } }注意这里没有host_permissions的宽泛声明,也没有tabs全量权限。activeTab只在用户主动点击插件时才授予当前标签页的访问权,contextMenus用来注册右键菜单,scripting用来在需要时注入解码脚本。这种最小权限模型的好处是:安装时 Chrome 不会弹出吓人的“读取你所有网站数据”警告,用户信任度更高,审核也更容易过。
注意:如果你在开发时发现右键菜单不出现,先检查
contextMenus是否声明,再检查background.js里chrome.runtime.onInstalled是否真的调用了chrome.contextMenus.create。这两个地方漏一个,菜单就是不出来。
3. 生成链路:从一段文本到一张可扫的二维码
3.1 文本编码:UTF-8 与字节模式的取舍
二维码标准(ISO/IEC 18004)支持多种编码模式:数字、字母数字、字节(8-bit)、汉字等。Chrome-QRCode 这类插件通常默认走字节模式,因为要兼容任意 UTF-8 文本,包括中文、emoji、特殊符号。字节模式的好处是通用,坏处是同样长度的内容,字节模式生成的矩阵比数字模式大。
举个例子,字符串1234567890用数字模式只需要很少的模块,但用字节模式会占用更多空间。如果你生成的是纯数字的订单号或者纯大写的激活码,其实可以手动指定模式来缩小二维码尺寸。不过对于“当前页面 URL”这种场景,URL 里通常包含://、/、?、=等字符,已经超出字母数字模式的范围,所以字节模式是唯一稳妥选择。
我在实际项目里做过对比:同一个 80 字符左右的 URL,字节模式生成的版本是 6(41×41 模块),如果强行拆成字母数字模式,能压到版本 5,但代码复杂度上升不少。对于插件这种追求“三分钟上手”的工具,统一走字节模式是最省心的。
3.2 纠错等级:L/M/Q/H 到底怎么选
二维码有四个纠错等级:
| 等级 | 可恢复比例 | 适用场景 |
|---|---|---|
| L | 约 7% | 屏幕显示、干净环境 |
| M | 约 15% | 通用默认,平衡尺寸与容错 |
| Q | 约 25% | 印刷物料、可能磨损 |
| H | 约 30% | 中间带 Logo、恶劣环境 |
Chrome-QRCode 默认一般用 M,这是最平衡的选择。但如果你要把二维码印在易拉宝、包装盒或者户外广告上,我强烈建议手动切到 Q 甚至 H。原因很简单:印刷品的对比度、纸张反光、油墨扩散都会影响识别,M 级在屏幕上没问题,到了纸上就可能翻车。
还有一个细节:如果你要在二维码中间放 Logo,必须用 H 级,因为中间那块区域实际上是被“挖掉”的,只有 30% 的纠错能力才能补回来。很多在线工具默认给你加 Logo 却不告诉你纠错等级,这就是为什么有些带 Logo 的码特别难扫。
3.3 渲染:Canvas 还是 SVG
生成二维码最后一步是渲染。Chrome-QRCode 通常用 Canvas,因为 Canvas 可以直接toDataURL()导出 PNG,方便用户右键保存或复制到剪贴板。SVG 的优势是矢量、无限放大不糊,适合印刷,但导出和复制不如 Canvas 方便。
我的做法是:插件弹窗里用 Canvas 显示,同时提供一个“下载 SVG”的按钮给有印刷需求的人。Canvas 渲染时要注意像素比问题——在高 DPI 屏幕上,如果只按 CSS 像素画,二维码会糊。正确做法是:
const scale = window.devicePixelRatio || 1; const size = 256; canvas.width = size * scale; canvas.height = size * scale; canvas.style.width = size + 'px'; canvas.style.height = size + 'px'; const ctx = canvas.getContext('2d'); ctx.scale(scale, scale);这样在 Retina 屏上生成的二维码边缘依然锐利,扫码识别率明显提升。这个细节很多简易插件都忽略了,导致用户在 Mac 上生成的码看起来“有点虚”。
3.4 一个容易忽略的坑:静默区(Quiet Zone)
二维码四周必须留出至少 4 个模块宽度的空白区域,这叫静默区。没有静默区,扫码器无法定位二维码边界,识别率会大幅下降。有些插件为了“好看”,把二维码画得贴边,结果用户截图后发到微信里就扫不出来。
Chrome-QRCode 在渲染时应该默认留出静默区。如果你自己改代码,记得在计算 Canvas 尺寸时把(modules + 8) * scale作为总宽高,其中 8 就是两侧各 4 个模块的静默区。这个数字不是随便定的,是标准里写死的。
4. 解码链路:把一张图片还原成文本的完整过程
4.1 图像预处理:为什么直接丢给解码库经常失败
很多人以为解码就是“把图片传给 jsQR 就完事了”,实际上一张网页上的二维码图片,可能带着背景色、可能被 CSS 缩放、可能是半透明的、可能周围有一堆干扰元素。直接解码的成功率并不高。
Chrome-QRCode 在解码前通常会做几步预处理:
- 绘制到离屏 Canvas:把
<img>或背景图绘制到一个干净的 Canvas 上,拿到ImageData。 - 灰度化:把 RGB 转成灰度,减少计算量,同时消除颜色干扰。
- 二值化:根据阈值把灰度图变成黑白,突出模块边界。
- 定位图案检测:找到三个角上的“回”字形定位点,确定二维码的位置和旋转角度。
jsQR 这类库内部已经做了大部分工作,但如果你传入的图片本身分辨率太低(比如小于 100×100),或者压缩得太厉害,定位图案就会糊成一团,解码必然失败。我的经验是:解码前先把图片放大到至少 300×300,用 Canvas 的drawImage做双线性插值,成功率会明显提升。
4.2 定位与透视校正:二维码歪着也能解
二维码最巧妙的设计之一就是三个定位图案。解码器通过这三个点可以算出二维码的旋转角度和透视变形,然后做反向变换,把歪的、斜的、甚至有点梯形变形的二维码“拉正”。这就是为什么你斜着扫二维码也能识别。
但透视校正有极限。如果拍摄角度太偏(比如超过 45 度),或者二维码被折叠、弯曲,校正就会失败。Chrome-QRCode 处理的是网页上的平面图片,通常不存在这个问题,但如果你截图的是一张实物照片,就要注意拍摄角度尽量正对。
4.3 纠错与还原:Reed-Solomon 在背后干活
二维码的纠错用的是 Reed-Solomon 码,这是一种在通信和存储领域广泛使用的纠错算法。简单说,它把原始数据分成若干块,每块附加一些冗余校验码。当部分模块损坏或读错时,校验码可以反推出原始数据。
这也是为什么二维码被挡住一角还能扫出来。但要注意,纠错能力是有限的,而且损坏区域不能覆盖定位图案。如果你把二维码中间挖掉一大块,只要不超过纠错比例,数据还能恢复;但如果你把左上角的定位图案挡住,解码器连位置都找不到,直接放弃。
4.4 右键解码的完整实现思路
在 Chrome 扩展里实现“右键图片解码”,大致流程是:
// background.js chrome.contextMenus.create({ id: "decode-qr", title: "解码此二维码", contexts: ["image"] }); chrome.contextMenus.onClicked.addListener((info, tab) => { if (info.menuItemId === "decode-qr") { chrome.scripting.executeScript({ target: { tabId: tab.id }, func: decodeImageAtPoint, args: [info.srcUrl] }); } });然后在页面里用fetch拿到图片、画到 Canvas、调用 jsQR。这里有个坑:如果图片是跨域的,fetch可能被 CORS 拦住。解决办法是用chrome.scripting注入的脚本直接读取页面上已经渲染的<img>元素,通过drawImage绘制,这样就不受 CORS 限制,因为图片已经在页面里了。
提示:如果解码结果是一串乱码,先检查图片是不是被 CSS 旋转过。
drawImage不会自动应用 CSS transform,需要手动读取getComputedStyle里的transform矩阵并做相应旋转。
5. 三分钟上手:安装、配置与日常使用
5.1 从源码加载到本地
如果你拿到的是源码而不是商店版本,加载步骤是:
- 打开
chrome://extensions/。 - 右上角打开“开发者模式”。
- 点击“加载已解压的扩展程序”,选择插件根目录。
- 确认工具栏出现二维码图标。
如果加载时报错,最常见的原因是manifest.json格式不对,比如多了逗号、少了引号,或者manifest_version写成了 2 但代码用的是 3 的 API。Chrome 会给出具体行号,照着改就行。
5.2 日常使用的三个快捷入口
装好之后,我常用的三个入口是:
- 点图标:生成当前页面 URL 的二维码,弹窗里直接显示,右键可保存。
- 选中文字右键:生成选中文本的二维码,适合分享一段配置。
- 图片上右键:解码二维码,结果会以通知或弹窗形式显示,可一键复制。
这三个入口覆盖了我 95% 的使用场景。剩下的 5% 是批量生成,那种情况我会直接用 Node.js 脚本调qrcode库,不走插件。
5.3 弹窗尺寸与交互的取舍
插件弹窗(popup)的尺寸是有限的,Chrome 规定最大 800×600。二维码如果生成得太大,弹窗里显示不全;太小又影响扫码。我的建议是弹窗里显示 256×256 的二维码,同时提供“在新标签页打开大图”的按钮。这样既保证了弹窗的简洁,又给了需要大图的用户出口。
另外,弹窗里最好加一个“复制图片到剪贴板”的按钮。Chrome 扩展可以用navigator.clipboard.write配合ClipboardItem写入 PNG,但要注意这个 API 需要用户手势触发,不能在页面加载时自动调用。
6. 我踩过的坑与对应的修复方案
6.1 中文乱码:编码模式没选对
早期我自己写的一个版本,生成中文二维码后扫出来是乱码。排查了半天,发现是库默认用了 Latin-1 编码。修复方法是在生成时显式指定toSJISFunc或者确保输入字符串先经过encodeURIComponent处理。对于纯 UTF-8 场景,直接用qrcode库的QRCode.toCanvas并传入字符串即可,现代版本默认就是 UTF-8。
6.2 高 DPI 屏幕上的模糊问题
前面提过,Canvas 不处理devicePixelRatio就会糊。我第一次在 MacBook 上测试时,生成的二维码在弹窗里看着还行,但保存下来的 PNG 放大后边缘全是锯齿。加上devicePixelRatio缩放后,问题解决。这个坑在 Windows 的 125% 缩放下也会出现,属于跨平台必踩项。
6.3 右键解码跨域图片失败
网页上很多二维码图片来自 CDN,直接fetch会被 CORS 拦截。我最初的方案是让 background 去请求,结果一样失败。后来改成在 content script 里找到页面上对应的<img>元素,直接drawImage到 Canvas,因为图片已经渲染在页面里,浏览器认为它是“同源可见”的,不受 CORS 限制。这个思路转换花了我不少时间,但一旦想通就非常简单。
6.4 二维码扫不出来的排查清单
如果你生成的二维码扫不出来,按这个顺序排查:
| 排查项 | 检查方法 | 修复 |
|---|---|---|
| 静默区 | 看二维码四周是否有空白 | 增加 4 模块边距 |
| 纠错等级 | 是否被 Logo 遮挡 | 切到 H 级 |
| 对比度 | 前景色和背景色是否接近 | 用纯黑纯白 |
| 尺寸 | 是否小于 2cm | 放大到至少 2.5cm |
| 屏幕反光 | 是否在强光下扫描 | 调整角度或换哑光材质 |
这张表我打印出来贴在工位上,每次做线下物料前都过一遍,基本不会再翻车。
7. 进阶玩法:把二维码能力接进你的工作流
7.1 与剪贴板联动:一键生成当前复制内容
Chrome 扩展可以监听navigator.clipboard.readText(),但需要用户授权。一个更顺手的做法是:在弹窗里放一个“从剪贴板生成”按钮,用户点一下,插件读取剪贴板文本并生成二维码。这样复制一段 URL 后,不用切标签页,点插件图标再点一下按钮就出码。
7.2 批量生成:用 Node.js 脚本替代插件
如果你要一次性生成几百个二维码(比如给每个商品生成一个),插件就不合适了。这时候用 Node.js:
const QRCode = require('qrcode'); const items = ['https://example.com/1', 'https://example.com/2']; items.forEach(async (url, i) => { await QRCode.toFile(`./qrcode-${i}.png`, url, { errorCorrectionLevel: 'H', width: 512, margin: 4 }); });这个脚本我用了两年,稳定可靠。关键参数是errorCorrectionLevel: 'H'和margin: 4,前者保证容错,后者保证静默区。
7.3 解码结果的二次利用
解码出来的文本,除了复制,还可以做很多事:如果是 URL,自动在新标签页打开;如果是 Wi-Fi 配置(WIFI:S:...),解析出 SSID 和密码;如果是 vCard,直接导入通讯录。Chrome-QRCode 这类插件如果只做“显示文本”,就浪费了解码结果的价值。我在自己的版本里加了一个简单的识别逻辑:检测到http开头就显示“打开链接”按钮,检测到WIFI:就显示“复制密码”按钮,体验提升非常明显。
8. 关于二维码工具选型的一点个人看法
用了这么多年二维码工具,我的结论是:在线工具适合偶尔用一次,浏览器插件适合每天用,脚本适合批量用。Chrome-QRCode 这类极简插件的定位非常清晰——它不追求大而全,不搞花哨的样式定制,就是把“生成”和“解码”两件事做到最快、最稳、最私密。
如果你只是偶尔生成一个二维码,打开草料或者随便一个在线网站完全够用。但如果你像我一样,每天要处理几十个链接、经常需要把当前页面分享给手机、又不想让 URL 经过第三方服务器,那花三分钟装一个本地二维码插件,长期回报率极高。
最后分享一个小技巧:生成二维码时,如果内容超过 200 个字符,考虑用短链接先压缩一下,否则二维码会变得非常密集,手机扫描时对焦时间明显变长。这个细节在文档里通常不会写,但实际用起来差别很大。